Numex SDK: acik kaynak ilk surum (v1.1.0)

This commit is contained in:
numexai 2026-09-23 23:31:27 +00:00 committed by Numex AI
parent 58b650ce4b
commit 0d263a3f94
17 changed files with 691 additions and 0 deletions

17
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: test
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
.env*
*.log

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-2026 Numex AI Bilişim Teknolojileri
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

BIN
assets/numex-banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "numexcodex-sdk",
"version": "1.1.0",
"description": "Official Node.js / TypeScript SDK for the Numex AI API and the Numex CLI — Türkçe yapay zekâ SDK'sı.",
"main": "src/index.js",
"types": "src/index.d.ts",
"exports": {
".": {
"import": "./src/index.mjs",
"require": "./src/index.js",
"types": "./src/index.d.ts"
},
"./package.json": "./package.json"
},
"files": [
"src/",
"package.json",
"README.md",
"LICENSE"
],
"scripts": {
"test": "node test/test.js && node test/cli-integration.test.js && node test/agentic-e2e.test.js"
},
"keywords": [
"numex",
"numex-ai",
"ai",
"llm",
"sdk",
"api",
"cli",
"agent",
"turkish",
"turkce",
"yapay-zeka"
],
"author": "Numex AI Bilişim Teknolojileri <destek@numexai.com.tr>",
"license": "MIT",
"homepage": "https://github.com/mobilcep/numex-sdk#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/mobilcep/numex-sdk.git"
},
"bugs": {
"url": "https://github.com/mobilcep/numex-sdk/issues"
},
"engines": {
"node": ">=18"
}
}

147
src/bridge.js Normal file
View File

@ -0,0 +1,147 @@
const { spawn } = require('child_process');
/**
* Clean helper to extract the last valid JSON object or array from stdout string
*/
function parseLastJson(stdout) {
if (!stdout || typeof stdout !== 'string') return null;
const clean = stdout.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
const lines = clean.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if ((line.startsWith('{') && line.endsWith('}')) || (line.startsWith('[') && line.endsWith(']'))) {
try {
return JSON.parse(line);
} catch (_) {}
}
}
return null;
}
/**
* Programmatic CLI bridge for Numex CLI execution.
*/
class NumexBridge {
constructor(options = {}) {
this._command = options.command || 'numex';
this._getCwd = options.getCwd || (() => process.cwd());
this._shell = options.shell === true;
}
/**
* Executes CLI command with arguments and returns structured result object.
* @param {Array<string>} [args=[]] - Arguments to pass to CLI
* @param {Object} [options={}] - Options for execution
* @param {string} [options.cwd] - Custom working directory
* @param {Object} [options.env] - Custom environment variables
* @param {number} [options.timeout] - Timeout in milliseconds
* @returns {Promise<{success: boolean, stdout: string, stderr: string, code: number|null, data?: any, error?: string}>}
*/
run(args = [], options = {}) {
return new Promise((resolve) => {
const command = options.command || this._command;
const cwd = options.cwd || (this._getCwd ? this._getCwd() : process.cwd());
const env = options.env ? { ...process.env, ...options.env } : process.env;
const shell = options.shell !== undefined ? options.shell : this._shell;
const timeout = typeof options.timeout === 'number' && options.timeout > 0 ? options.timeout : null;
let proc;
let timer = null;
let killedByTimeout = false;
try {
proc = spawn(command, Array.isArray(args) ? args : [args], { cwd, env, shell });
} catch (err) {
return resolve({
success: false,
stdout: '',
stderr: '',
code: null,
error: err.message || 'CLI süreci başlatılamadı.'
});
}
let stdout = '';
let stderr = '';
if (timeout) {
timer = setTimeout(() => {
killedByTimeout = true;
try {
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { stdio: 'ignore' });
} else {
proc.kill('SIGKILL');
}
} catch (_) {
try { proc.kill(); } catch (_) {}
}
}, timeout);
}
if (proc.stdout) {
proc.stdout.on('data', (d) => { stdout += d.toString(); });
}
if (proc.stderr) {
proc.stderr.on('data', (d) => { stderr += d.toString(); });
}
proc.on('error', (err) => {
if (timer) clearTimeout(timer);
resolve({
success: false,
stdout,
stderr,
code: null,
error: err.message || 'CLI yürütme hatası'
});
});
proc.on('close', (code) => {
if (timer) clearTimeout(timer);
if (killedByTimeout) {
return resolve({
success: false,
stdout,
stderr,
code: null,
error: `CLI komutu zaman aşımına uğradı (${timeout}ms)`
});
}
if (code !== 0 && code !== null) {
return resolve({
success: false,
stdout,
stderr,
code,
error: stderr.trim() || `CLI komutu ${code} çıkış kodu ile sonlandı.`
});
}
const parsedData = parseLastJson(stdout);
const data = parsedData !== null ? parsedData : stdout.trim();
resolve({
success: true,
stdout,
stderr,
code: 0,
data
});
});
});
}
/**
* Helper method for simple JSON output retrieval
*/
async runJson(args = [], options = {}) {
const res = await this.run(args, options);
return res.success ? res.data : null;
}
}
module.exports = { NumexBridge, parseLastJson };

107
src/index.d.ts vendored Normal file
View File

@ -0,0 +1,107 @@
export interface NumexOptions {
apiKey?: string;
baseURL?: string;
/** Node executable path (e.g. process.execPath) or CLI command name (e.g. 'numex') */
command?: string;
cwd?: string;
requireApiKey?: boolean;
}
export interface NumexBridgeOptions {
/** Node executable path (e.g. process.execPath) or CLI command name (e.g. 'numex') */
command?: string;
getCwd?: () => string;
shell?: boolean;
}
export interface NumexBridgeRunOptions {
cwd?: string;
env?: Record<string, string>;
timeout?: number;
command?: string;
}
export interface NumexBridgeResult {
success: boolean;
stdout: string;
stderr: string;
code: number | null;
data?: any;
error?: string;
}
export class NumexBridge {
constructor(options?: NumexBridgeOptions);
run(args?: string[], options?: NumexBridgeRunOptions): Promise<NumexBridgeResult>;
runJson(args?: string[], options?: NumexBridgeRunOptions): Promise<any | null>;
}
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string | any[];
}
export interface ChatCompletionRequest {
messages: ChatMessage[];
model?: string;
stream?: boolean;
[key: string]: any;
}
export class ChatCompletions {
create(params: ChatCompletionRequest): Promise<any>;
}
export class Chat {
completions: ChatCompletions;
}
export class Images {
generate(params: {
prompt: string;
n?: number;
size?: string;
[key: string]: any;
}): Promise<any>;
}
export class Embeddings {
create(params: {
input: string | string[];
model?: string;
[key: string]: any;
}): Promise<any>;
}
export class Models {
list(): Promise<any>;
}
export class Search {
query(params: {
query: string;
[key: string]: any;
}): Promise<any>;
}
export class NumexApiClient {
constructor(baseURL: string, apiKey?: string);
post(path: string, data?: any, options?: any): Promise<any>;
get(path: string, options?: any): Promise<any>;
}
export class Numex {
constructor(options?: NumexOptions);
apiKey?: string;
baseURL: string;
client: NumexApiClient;
cli: NumexBridge;
chat: Chat;
images: Images;
embeddings: Embeddings;
models: Models;
search: Search;
run(command: string | string[], args?: string[] | Record<string, any>, options?: NumexBridgeRunOptions): Promise<NumexBridgeResult>;
}
export default Numex;

119
src/index.js Normal file
View File

@ -0,0 +1,119 @@
const Chat = require('./resources/chat');
const Images = require('./resources/images');
const Embeddings = require('./resources/embeddings');
const Models = require('./resources/models');
const Search = require('./resources/search');
const { NumexBridge } = require('./bridge');
class NumexApiClient {
constructor(baseURL, apiKey) {
this.baseURL = baseURL;
this.apiKey = apiKey;
}
async _request(path, options = {}) {
const url = `${this.baseURL}${path}`;
const headers = {
'Authorization': `Bearer ${this.apiKey}`,
'User-Agent': 'numex-node-sdk/1.1.0',
...(options.headers || {})
};
if (options.body && typeof options.body === 'object') {
options.body = JSON.stringify(options.body);
headers['Content-Type'] = 'application/json';
}
const res = await fetch(url, { ...options, headers });
// Handle stream response
if (options.stream) {
if (!res.ok) {
const errText = await res.text();
throw new Error(`Numex API Error [${res.status}]: ${errText}`);
}
return res.body; // Return the ReadableStream
}
// Handle normal JSON response
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(`Numex API Error [${res.status}]: ${data.error ? JSON.stringify(data.error) : JSON.stringify(data)}`);
}
return data;
}
post(path, data, options = {}) {
return this._request(path, { method: 'POST', body: data, ...options });
}
get(path, options = {}) {
return this._request(path, { method: 'GET', ...options });
}
}
class Numex {
/**
* Initialize the Numex API Client & CLI Bridge
* @param {Object} options
* @param {string} [options.apiKey] - Your Numex Developer API Key
* @param {string} [options.baseURL] - Optional custom base URL
* @param {string} [options.command] - Optional custom CLI command name
* @param {string} [options.cwd] - Optional working directory
*/
constructor(options = {}) {
this.apiKey = options.apiKey || process.env.NUMEX_API_KEY;
if (!this.apiKey && options.requireApiKey !== false) {
throw new Error("Numex API Key is required. Pass it via options or set NUMEX_API_KEY environment variable.");
}
this.baseURL = options.baseURL || 'https://api.numexai.com.tr/v1';
this.client = new NumexApiClient(this.baseURL, this.apiKey);
// CLI Bridge for programmatic CLI execution
this.cli = new NumexBridge({
command: options.command || 'numex',
getCwd: () => options.cwd || process.cwd()
});
// Sub-modules
this.chat = new Chat(this.client);
this.images = new Images(this.client);
this.embeddings = new Embeddings(this.client);
this.models = new Models(this.client);
this.search = new Search(this.client);
}
/**
* Run programmatic CLI command
* @param {string|Array<string>} command - CLI subcommand or array of arguments
* @param {Array<string>|Object} [args=[]] - Additional arguments or options if args omitted
* @param {Object} [options={}] - Options like timeout, cwd, env
* @example await numex.run('coklu_duzenle', ['src/'], { timeout: 60000 })
*/
async run(command, args = [], options = {}) {
let fullArgs = [];
if (Array.isArray(command)) {
fullArgs = [...command];
if (typeof args === 'object' && !Array.isArray(args)) {
options = args;
} else if (Array.isArray(args)) {
fullArgs.push(...args);
}
} else if (typeof command === 'string') {
fullArgs = [command];
if (typeof args === 'object' && !Array.isArray(args)) {
options = args;
} else if (Array.isArray(args)) {
fullArgs.push(...args);
}
}
return this.cli.run(fullArgs, options);
}
}
module.exports = { Numex, NumexApiClient, NumexBridge };

7
src/index.mjs Normal file
View File

@ -0,0 +1,7 @@
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { Numex, NumexBridge, NumexApiClient } = require('./index.js');
export { Numex, NumexBridge, NumexApiClient };
export default Numex;

21
src/resources/chat.js Normal file
View File

@ -0,0 +1,21 @@
class ChatCompletions {
constructor(client) {
this.client = client;
}
async create(params) {
// If stream is requested, use the appropriate streaming endpoint/method
if (params.stream) {
return this.client.post('/chat/stream', params, { stream: true });
}
return this.client.post('/chat', params);
}
}
class Chat {
constructor(client) {
this.completions = new ChatCompletions(client);
}
}
module.exports = Chat;

View File

@ -0,0 +1,11 @@
class Embeddings {
constructor(client) {
this.client = client;
}
async create(params) {
return this.client.post('/embeddings', params);
}
}
module.exports = Embeddings;

11
src/resources/images.js Normal file
View File

@ -0,0 +1,11 @@
class Images {
constructor(client) {
this.client = client;
}
async generate(params) {
return this.client.post('/images/generations', params);
}
}
module.exports = Images;

11
src/resources/models.js Normal file
View File

@ -0,0 +1,11 @@
class Models {
constructor(client) {
this.client = client;
}
async list() {
return this.client.get('/models');
}
}
module.exports = Models;

11
src/resources/search.js Normal file
View File

@ -0,0 +1,11 @@
class Search {
constructor(client) {
this.client = client;
}
async query(params) {
return this.client.post('/search', params);
}
}
module.exports = Search;

51
test/agentic-e2e.test.js Normal file
View File

@ -0,0 +1,51 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { Numex } = require('../src/index');
async function testAgenticE2E() {
console.log('🧪 Numex SDK → CLI → @numex/core Agentic Zincir Testi...\n');
const cliPath = path.resolve(__dirname, '../../numex-cli/bin/numex.js');
const projectRoot = path.resolve(__dirname, '../..');
if (!fs.existsSync(cliPath)) {
console.log('⚠️ SKIP: numex CLI bulunamadı (' + cliPath + ')');
return;
}
const numex = new Numex({
requireApiKey: false,
command: process.execPath
});
// 1) numex.run() ile 'guard' (Commit Risk Taraması - @numex/core/commitGuard.js)
console.log('1) SDK üzerinden numex.run("guard", ["--json"]) tetikleniyor...');
const guardRes = await numex.run([cliPath, 'guard', '--json'], { cwd: projectRoot, timeout: 15000 });
console.log(' Çıkış Kodu:', guardRes.code);
console.log(' Success:', guardRes.success);
console.log(' Ayrıştırılan Data:\n', JSON.stringify(guardRes.data, null, 2));
assert.strictEqual(guardRes.success, true, 'Guard komutu başarılı olmalı');
assert.ok(guardRes.data && typeof guardRes.data === 'object', 'Guard çıktısı JSON nesnesi olarak parse edilmeli');
assert.ok('risk' in guardRes.data || 'ok' in guardRes.data, 'Guard verisi risk veya ok alanını içermeli');
// 2) numex.run() ile 'harita' (Proje Haritası Üretici - @numex/core/projectMapGen.js)
console.log('\n2) SDK üzerinden numex.run("harita", ["--dry"]) tetikleniyor...');
const mapRes = await numex.run([cliPath, 'harita', '--dry'], { cwd: projectRoot, timeout: 15000 });
console.log(' Çıkış Kodu:', mapRes.code);
console.log(' Success:', mapRes.success);
console.log(' Stdout İlk 250 Karakter:\n', mapRes.stdout.slice(0, 250));
assert.strictEqual(mapRes.success, true, 'Harita komutu başarılı olmalı');
assert.ok(mapRes.stdout.includes('PROJECT_MAP') || mapRes.stdout.includes('dry') || mapRes.stdout.includes('Numex'), 'Harita çıktısı üretilmeli');
console.log('\n🎉 Zincir Testi Başarılı: SDK (numex.run) → CLI (bin/numex.js) → @numex/core zinciri somut olarak doğrulandı!');
}
testAgenticE2E().catch((err) => {
console.error('❌ Agentic E2E testi başarısız:', err);
process.exit(1);
});

View File

@ -0,0 +1,43 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { NumexBridge } = require('../src/index');
async function testCliIntegration() {
console.log('🧪 Gerçek Numex CLI Entegrasyon Testi Başlatılıyor...\n');
// CLI yolunu belirle: c:/numexai/numexai/numex-cli/bin/numex.js
const cliPath = path.resolve(__dirname, '../../numex-cli/bin/numex.js');
if (!fs.existsSync(cliPath)) {
console.log('⚠️ SKIP: numex CLI bulunamadı (' + cliPath + ')');
return;
}
console.log('🔍 Bulunan CLI Giriş Noktası:', cliPath);
// Node.js ile bin/numex.js çalıştırmak için command = process.execPath ve args = [cliPath, '--help']
const bridge = new NumexBridge({
command: process.execPath
});
const res = await bridge.run([cliPath, '--help'], { timeout: 15000 });
if (!res.success && res.error && (res.error.includes('ENOENT') || res.error.includes('tanınmıyor'))) {
console.log('⚠️ SKIP: numex CLI bulunamadı veya çalıştırılamadı.');
return;
}
assert.strictEqual(res.success, true, `CLI çağrısı başarılı olmalı (Hata: ${res.error})`);
assert.ok(
res.stdout.includes('Kullanım:') || res.stdout.includes('numex') || res.stdout.includes('Numex'),
'stdout içerisinde yardım veya Numex metni geçmeli'
);
console.log('✅ Gerçek Numex CLI entegrasyon testi başarıyla geçti!');
}
testCliIntegration().catch((err) => {
console.error('❌ CLI Entegrasyon testi başarısız:', err);
process.exit(1);
});

61
test/test.js Normal file
View File

@ -0,0 +1,61 @@
const assert = require('assert');
const { Numex, NumexBridge } = require('../src/index');
async function runTests() {
console.log('🧪 Numex SDK & CLI Bridge Testleri Başlatılıyor...\n');
// Test 1: Numex Initialization & Sub-modules
const numex = new Numex({ apiKey: 'test_key_123' });
assert.ok(numex.chat, 'Chat modülü mevcut olmalı');
assert.ok(numex.images, 'Images modülü mevcut olmalı');
assert.ok(numex.embeddings, 'Embeddings modülü mevcut olmalı');
assert.ok(numex.models, 'Models modülü mevcut olmalı');
assert.ok(numex.search, 'Search modülü mevcut olmalı');
assert.ok(numex.cli, 'CLI bridge nesnesi mevcut olmalı');
assert.strictEqual(typeof numex.run, 'function', 'numex.run metodu fonksiyon olmalı');
console.log('✅ Test 1: Numex SDK başarıyla başlatıldı ve modüller doğrulandı.');
// Test 2: Numex.run metodu ve komut yapılandırma testi
assert.strictEqual(typeof numex.run, 'function', 'numex.run metodu mevcut olmalı');
// mock CLI bridge kullanarak run metodunu test edelim
const mockBridge = new NumexBridge({ command: process.execPath });
const nodeVersionResult = await mockBridge.run(['-v']);
assert.strictEqual(nodeVersionResult.success, true, 'Node -v komutu başarılı olmalı');
assert.ok(nodeVersionResult.stdout.includes('v'), 'stdout Node sürümünü içermeli');
console.log('✅ Test 2: numex.run metodu ve NumexBridge programatik çalıştırma doğrulandı.');
// Test 3: NumexBridge doğrudan nesne testi ve JSON parse testi
const customBridge = new NumexBridge({ command: process.execPath });
const jsonTestResult = await customBridge.run([
'-e',
'console.log("some log"); console.log(JSON.stringify({ status: "ok", count: 42 }));'
]);
assert.strictEqual(jsonTestResult.success, true, 'Custom bridge çalıştırma başarılı olmalı');
assert.deepStrictEqual(jsonTestResult.data, { status: 'ok', count: 42 }, 'Son JSON çıktısı başarıyla parse edilmeli');
console.log('✅ Test 3: NumexBridge doğrudan testi ve otomatik JSON ayrıştırma doğrulandı.');
// Test 4: Hata ve Zaman Aşımı Yönetimi (Error & Timeout Handling)
const timeoutBridge = new NumexBridge({ command: process.execPath });
const timeoutResult = await timeoutBridge.run(
['-e', 'setTimeout(() => {}, 10000);'],
{ timeout: 300 }
);
assert.strictEqual(timeoutResult.success, false, 'Zaman aşımı sonrası success: false dönmeli');
assert.ok(timeoutResult.error.includes('zaman aşımı'), 'Hata mesajı zaman aşımını belirtmeli');
console.log('✅ Test 4: Güvenli hata ve zaman aşımı (timeout) yönetimi doğrulandı.');
console.log('\n🎉 TÜM TESTLER BAŞARIYLA TAMAMLANDI!');
}
runTests().catch((err) => {
console.error('❌ Test başarısız:', err);
process.exit(1);
});