diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..15df5c2 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2524647 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env* +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dff85d3 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/assets/numex-banner.png b/assets/numex-banner.png new file mode 100644 index 0000000..7fa817b Binary files /dev/null and b/assets/numex-banner.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..c71b747 --- /dev/null +++ b/package.json @@ -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 ", + "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" + } +} diff --git a/src/bridge.js b/src/bridge.js new file mode 100644 index 0000000..2489519 --- /dev/null +++ b/src/bridge.js @@ -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} [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 }; diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..2104397 --- /dev/null +++ b/src/index.d.ts @@ -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; + 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; + runJson(args?: string[], options?: NumexBridgeRunOptions): Promise; +} + +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; +} + +export class Chat { + completions: ChatCompletions; +} + +export class Images { + generate(params: { + prompt: string; + n?: number; + size?: string; + [key: string]: any; + }): Promise; +} + +export class Embeddings { + create(params: { + input: string | string[]; + model?: string; + [key: string]: any; + }): Promise; +} + +export class Models { + list(): Promise; +} + +export class Search { + query(params: { + query: string; + [key: string]: any; + }): Promise; +} + +export class NumexApiClient { + constructor(baseURL: string, apiKey?: string); + post(path: string, data?: any, options?: any): Promise; + get(path: string, options?: any): Promise; +} + +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, options?: NumexBridgeRunOptions): Promise; +} + +export default Numex; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..9f01ab0 --- /dev/null +++ b/src/index.js @@ -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} command - CLI subcommand or array of arguments + * @param {Array|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 }; diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 0000000..5a1346f --- /dev/null +++ b/src/index.mjs @@ -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; diff --git a/src/resources/chat.js b/src/resources/chat.js new file mode 100644 index 0000000..87d3c9b --- /dev/null +++ b/src/resources/chat.js @@ -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; diff --git a/src/resources/embeddings.js b/src/resources/embeddings.js new file mode 100644 index 0000000..80d64d4 --- /dev/null +++ b/src/resources/embeddings.js @@ -0,0 +1,11 @@ +class Embeddings { + constructor(client) { + this.client = client; + } + + async create(params) { + return this.client.post('/embeddings', params); + } +} + +module.exports = Embeddings; diff --git a/src/resources/images.js b/src/resources/images.js new file mode 100644 index 0000000..17b7ef6 --- /dev/null +++ b/src/resources/images.js @@ -0,0 +1,11 @@ +class Images { + constructor(client) { + this.client = client; + } + + async generate(params) { + return this.client.post('/images/generations', params); + } +} + +module.exports = Images; diff --git a/src/resources/models.js b/src/resources/models.js new file mode 100644 index 0000000..22d220a --- /dev/null +++ b/src/resources/models.js @@ -0,0 +1,11 @@ +class Models { + constructor(client) { + this.client = client; + } + + async list() { + return this.client.get('/models'); + } +} + +module.exports = Models; diff --git a/src/resources/search.js b/src/resources/search.js new file mode 100644 index 0000000..35222fb --- /dev/null +++ b/src/resources/search.js @@ -0,0 +1,11 @@ +class Search { + constructor(client) { + this.client = client; + } + + async query(params) { + return this.client.post('/search', params); + } +} + +module.exports = Search; diff --git a/test/agentic-e2e.test.js b/test/agentic-e2e.test.js new file mode 100644 index 0000000..c722dbf --- /dev/null +++ b/test/agentic-e2e.test.js @@ -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); +}); diff --git a/test/cli-integration.test.js b/test/cli-integration.test.js new file mode 100644 index 0000000..0ed8b9d --- /dev/null +++ b/test/cli-integration.test.js @@ -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); +}); diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..ee2b37b --- /dev/null +++ b/test/test.js @@ -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); +});