natureco-cli 4.7.3 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "4.7.3",
3
+ "version": "4.8.0",
4
4
  "description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -22,6 +22,20 @@ const https = require('https');
22
22
  const { spawn } = require('child_process');
23
23
  const chalk = require('chalk');
24
24
  const tui = require('../utils/tui');
25
+ const { loadToolDefinitions, toOpenAIFormat, executeTool } = require('../utils/tools');
26
+
27
+ // v4.8.0: Tool definitions — başlangıçta bir kez yükle (performans)
28
+ let _toolDefs = null;
29
+ function getToolDefs() {
30
+ if (!_toolDefs) {
31
+ try {
32
+ _toolDefs = loadToolDefinitions();
33
+ } catch (e) {
34
+ _toolDefs = [];
35
+ }
36
+ }
37
+ return _toolDefs;
38
+ }
25
39
 
26
40
  // CLI komutları (REPL içinden çalıştırılabilir)
27
41
  const CLI_COMMANDS = {
@@ -194,55 +208,183 @@ function apiRequest(providerUrl, providerApiKey, body, stream = false) {
194
208
  });
195
209
  }
196
210
 
197
- async function sendStreaming(providerUrl, providerApiKey, messages, model, onChunk) {
211
+ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChunk, onToolCall) {
198
212
  const isMM = isMiniMax(providerUrl);
199
- const body = { model, messages, stream: !isMM, temperature: 0.7, max_tokens: 2048 };
200
- if (!body.stream) {
201
- const res = await apiRequest(providerUrl, providerApiKey, body, false);
202
- const content = res.choices?.[0]?.message?.content || '';
203
- for (const char of content) {
204
- onChunk(char);
205
- await new Promise(r => setTimeout(r, 8));
206
- }
207
- return content;
208
- }
209
- const endpoint = `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
210
- return new Promise((resolve, reject) => {
211
- const req = https.request(endpoint, {
212
- method: 'POST',
213
- headers: { 'Authorization': `Bearer ${providerApiKey}`, 'Content-Type': 'application/json' },
214
- timeout: 60000,
215
- }, (res) => {
216
- if (res.statusCode !== 200) {
217
- let data = '';
218
- res.on('data', c => data += c);
219
- res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
220
- return;
213
+ const toolDefs = getToolDefs();
214
+ const toolParam = isMM ? undefined : toOpenAIFormat(toolDefs);
215
+
216
+ // v4.8.0: Tool calling + streaming + multi-turn tool execution
217
+ let currentMessages = messages;
218
+ let fullText = '';
219
+ let iterations = 0;
220
+ const MAX_TOOL_ITERATIONS = 5; // Sonsuz döngüyü önle
221
+
222
+ while (iterations < MAX_TOOL_ITERATIONS) {
223
+ iterations++;
224
+ const body = {
225
+ model,
226
+ messages: currentMessages,
227
+ stream: !isMM,
228
+ temperature: 0.3,
229
+ max_tokens: 2048,
230
+ };
231
+ if (toolParam) body.tools = toolParam;
232
+
233
+ if (!body.stream) {
234
+ // MiniMax (non-stream) — tool_calls desteklemiyor varsayalım
235
+ const res = await apiRequest(providerUrl, providerApiKey, body, false);
236
+ const msg = res.choices?.[0]?.message || {};
237
+ const content = msg.content || '';
238
+ for (const char of content) {
239
+ onChunk(char);
240
+ await new Promise(r => setTimeout(r, 8));
221
241
  }
222
- let buffer = ''; let full = '';
223
- res.on('data', (chunk) => {
224
- buffer += chunk.toString('utf8');
225
- const lines = buffer.split('\n');
226
- buffer = lines.pop() || '';
227
- for (const line of lines) {
228
- const trimmed = line.trim();
229
- if (!trimmed || !trimmed.startsWith('data:')) continue;
230
- const data = trimmed.slice(5).trim();
231
- if (data === '[DONE]') { resolve(full); return; }
232
- try {
233
- const parsed = JSON.parse(data);
234
- const delta = parsed.choices?.[0]?.delta?.content || '';
235
- if (delta) { full += delta; onChunk(delta); }
236
- } catch {}
242
+ fullText = content;
243
+ // Non-stream tool call desteği
244
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
245
+ const toolResults = await processToolCalls(msg.tool_calls, onToolCall);
246
+ currentMessages.push(msg);
247
+ currentMessages.push(...toolResults);
248
+ continue; // Tekrar API çağır
249
+ }
250
+ break;
251
+ }
252
+
253
+ // OpenAI uyumlu streaming
254
+ const endpoint = `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
255
+ const result = await new Promise((resolve, reject) => {
256
+ const req = https.request(endpoint, {
257
+ method: 'POST',
258
+ headers: { 'Authorization': `Bearer ${providerApiKey}`, 'Content-Type': 'application/json' },
259
+ timeout: 60000,
260
+ }, (res) => {
261
+ if (res.statusCode !== 200) {
262
+ let data = '';
263
+ res.on('data', c => data += c);
264
+ res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
265
+ return;
237
266
  }
267
+ let buffer = '';
268
+ let streamText = '';
269
+ const toolCalls = []; // { index, id, name, args }
270
+ res.on('data', (chunk) => {
271
+ buffer += chunk.toString('utf8');
272
+ const lines = buffer.split('\n');
273
+ buffer = lines.pop() || '';
274
+ for (const line of lines) {
275
+ const trimmed = line.trim();
276
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
277
+ const data = trimmed.slice(5).trim();
278
+ if (data === '[DONE]') {
279
+ resolve({ streamText, toolCalls });
280
+ return;
281
+ }
282
+ try {
283
+ const parsed = JSON.parse(data);
284
+ const choice = parsed.choices?.[0];
285
+ if (!choice) continue;
286
+ const delta = choice.delta;
287
+
288
+ // Text content
289
+ if (delta.content) {
290
+ streamText += delta.content;
291
+ onChunk(delta.content);
292
+ }
293
+
294
+ // Tool calls (streaming delta)
295
+ if (delta.tool_calls) {
296
+ for (const tcDelta of delta.tool_calls) {
297
+ const idx = tcDelta.index;
298
+ if (!toolCalls[idx]) {
299
+ toolCalls[idx] = { index: idx, id: '', name: '', args: '' };
300
+ }
301
+ if (tcDelta.id) toolCalls[idx].id = tcDelta.id;
302
+ if (tcDelta.function?.name) toolCalls[idx].name += tcDelta.function.name;
303
+ if (tcDelta.function?.arguments) toolCalls[idx].args += tcDelta.function.arguments;
304
+ }
305
+ }
306
+ } catch {}
307
+ }
308
+ });
309
+ res.on('end', () => resolve({ streamText, toolCalls }));
238
310
  });
239
- res.on('end', () => resolve(full));
311
+ req.on('error', reject);
312
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
313
+ req.write(JSON.stringify(body));
314
+ req.end();
240
315
  });
241
- req.on('error', reject);
242
- req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
243
- req.write(JSON.stringify(body));
244
- req.end();
245
- });
316
+
317
+ fullText = result.streamText;
318
+
319
+ // Tool call var mı?
320
+ if (result.toolCalls && result.toolCalls.length > 0 && result.toolCalls[0].name) {
321
+ // Assistant mesajını ekle (tool_calls ile)
322
+ currentMessages.push({
323
+ role: 'assistant',
324
+ content: result.streamText || null,
325
+ tool_calls: result.toolCalls.map(tc => ({
326
+ id: tc.id || `call_${Date.now()}_${tc.index}`,
327
+ type: 'function',
328
+ function: { name: tc.name, arguments: tc.args },
329
+ })),
330
+ });
331
+ // Her tool call'ı çalıştır, sonuçları tool mesajı olarak ekle
332
+ const toolResults = await processToolCalls(result.toolCalls, onToolCall);
333
+ currentMessages.push(...toolResults);
334
+ // Devam — model sonuçları görsün, cevap versin
335
+ continue;
336
+ }
337
+
338
+ break; // Tool call yok, çık
339
+ }
340
+
341
+ return fullText;
342
+ }
343
+
344
+ /**
345
+ * Tool call'ları çalıştır, sonuçları OpenAI uyumlu tool mesajlarına dönüştür.
346
+ * @param toolCalls - API'den gelen tool_calls array
347
+ * @param onToolCall - UI callback (her tool call'ı kullanıcıya göster)
348
+ * @returns messages array — { role: 'tool', tool_call_id, content }
349
+ */
350
+ async function processToolCalls(toolCalls, onToolCall) {
351
+ const toolDefs = getToolDefs();
352
+ const messages = [];
353
+
354
+ for (const tc of toolCalls) {
355
+ const name = tc.function?.name || tc.name;
356
+ const argsStr = tc.function?.arguments || tc.args || '{}';
357
+ const id = tc.id || `call_${Date.now()}`;
358
+
359
+ let args = {};
360
+ try {
361
+ args = typeof argsStr === 'string' ? JSON.parse(argsStr) : argsStr;
362
+ } catch (e) {
363
+ args = { _parse_error: e.message, _raw: argsStr };
364
+ }
365
+
366
+ // UI callback — tool çalıştırılıyor bildir
367
+ if (onToolCall) {
368
+ onToolCall({ name, args, status: 'running' });
369
+ }
370
+
371
+ // Tool çalıştır
372
+ const result = await executeTool(name, args, toolDefs);
373
+
374
+ if (onToolCall) {
375
+ onToolCall({ name, args, status: 'done', result });
376
+ }
377
+
378
+ messages.push({
379
+ role: 'tool',
380
+ tool_call_id: id,
381
+ content: result.error
382
+ ? JSON.stringify({ error: result.error })
383
+ : (typeof result.result === 'string' ? result.result : JSON.stringify(result.result).slice(0, 8000)),
384
+ });
385
+ }
386
+
387
+ return messages;
246
388
  }
247
389
 
248
390
  function printHelp() {
@@ -315,12 +457,10 @@ async function startRepl(args) {
315
457
 
316
458
  // System prompt oluştur (memory + identity + persistent bağlam)
317
459
  const systemPrompt = [
318
- `Sen ${memory.botName || 'İchigo'} adında bir AI asistanısın. NatureCo platformunun Türkçe asistanısın.`,
319
- `Sen Claude, GPT veya başka bir marka değilsin — sen ${memory.botName || 'İchigo'}'sin.`,
320
- 'Kullanıcı Türkçe yazıyorsa Türkçe cevap ver, İngilizce yazıyorsa İngilizce.',
321
- 'Cevapların kısa, net ve faydalı olsun. Markdown kullanabilirsin ama abartma.',
322
- // v4.7.3: Tool call simülasyonunu kapat — model tool isteyemez,
323
- // sadece açıklayıcı text yazar. CLI tool sistemi kullanıcı isteğiyle çalışır.
460
+ `Sen ${memory.botName || 'İchigo'} adında bir Türk yapay zeka asistanısın. NatureCo platformunun resmi Türkçe asistanısın.`,
461
+ `Sen Claude, GPT, MiniMax veya başka bir marka değilsin — sen ${memory.botName || 'İchigo'}'sin.`,
462
+ // v4.7.4: Daha agresif dil zorlaması — model önceki versiyonlarda İngilizce cevap veriyordu
463
+ 'KRİTİK DİL KURALI: Kullanıcı Türkçe yazıyorsa MUTLAKA %100 Türkçe cevap ver. Asla İngilizce, Çince veya başka dil kullanma. Cevabının TAMAMI Türkçe olmalı.',
324
464
  'ÖNEMLİ: <tool_call>, <invoke>, function_call veya benzeri XML/JSON formatında tool çağrısı SİMÜLE ETME. Sadece düz metin cevap ver. Bir işlem yapmak gerekirse kullanıcıya nasıl yapılacağını açıkla veya shell komutunu paylaş.',
325
465
  memory.nickname && memory.name
326
466
  ? `Kullanıcının adı: ${memory.name}. Sana "${memory.nickname}" diye hitap etmesinden hoşlanıyor.`
@@ -519,9 +659,33 @@ async function startRepl(args) {
519
659
  process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
520
660
  try {
521
661
  const apiMessages = messages.filter(m => !m._internal);
522
- const reply = await sendStreaming(providerUrl, providerApiKey, apiMessages, model, (chunk) => {
523
- process.stdout.write(chunk);
524
- });
662
+ const reply = await sendStreaming(
663
+ providerUrl,
664
+ providerApiKey,
665
+ apiMessages,
666
+ model,
667
+ // Text chunk callback
668
+ (chunk) => process.stdout.write(chunk),
669
+ // Tool call callback — kullanıcıya göster
670
+ (toolEvent) => {
671
+ if (toolEvent.status === 'running') {
672
+ process.stdout.write('\n');
673
+ console.log(tui.styled(' 🔧 Tool: ' + toolEvent.name, { color: tui.PALETTE.accent, bold: true }));
674
+ const argsStr = JSON.stringify(toolEvent.args).slice(0, 120);
675
+ console.log(tui.styled(' Args: ' + argsStr, { color: tui.PALETTE.muted }));
676
+ } else if (toolEvent.status === 'done') {
677
+ if (toolEvent.result.error) {
678
+ console.log(tui.styled(' ✗ Hata: ' + toolEvent.result.error.slice(0, 100), { color: tui.PALETTE.danger }));
679
+ } else {
680
+ const resultStr = typeof toolEvent.result.result === 'string'
681
+ ? toolEvent.result.result.slice(0, 200)
682
+ : JSON.stringify(toolEvent.result.result).slice(0, 200);
683
+ console.log(tui.styled(' ✓ Sonuç: ' + resultStr, { color: tui.PALETTE.success }));
684
+ }
685
+ process.stdout.write(tui.styled(' AI ', { color: tui.PALETTE.secondary, bold: true }));
686
+ }
687
+ }
688
+ );
525
689
  process.stdout.write('\n');
526
690
  messages.push({ role: 'assistant', content: reply });
527
691
  totalInputTokens += apiMessages.reduce((s, m) => s + Math.ceil((m.content || '').length / 4), 0);
@@ -0,0 +1,96 @@
1
+ /**
2
+ * NatureCo CLI — Tool Definitions for OpenAI-compatible APIs
3
+ *
4
+ * src/tools/*.js dosyalarını OpenAI uyumlu function calling format'ına dönüştürür.
5
+ * Her tool'un:
6
+ * - name: tool adı
7
+ * - description: ne yaptığı
8
+ * - parameters: JSON schema
9
+ *
10
+ * REPL bu listeyi API'ye gönderir, model tool çağrısı yapar,
11
+ * biz tool'u çalıştırır, sonucu modele geri veririz.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const TOOLS_DIR = path.join(__dirname, '..', 'tools');
18
+
19
+ /**
20
+ * src/tools/*.js dosyalarını oku, her birinin export'ladığı
21
+ * tool metadata'sını topla. Eğer tool'un export'unda
22
+ * { name, description, parameters, execute } varsa kullan,
23
+ * yoksa dosya adından otomatik üret.
24
+ */
25
+ function loadToolDefinitions() {
26
+ const tools = [];
27
+ const files = fs.readdirSync(TOOLS_DIR).filter(f => f.endsWith('.js'));
28
+
29
+ for (const file of files) {
30
+ try {
31
+ const toolPath = path.join(TOOLS_DIR, file);
32
+ const mod = require(toolPath);
33
+
34
+ // Tool metadata çıkar
35
+ const meta = {
36
+ name: mod.name || path.basename(file, '.js'),
37
+ description: mod.description || `${path.basename(file, '.js')} tool`,
38
+ parameters: mod.parameters || mod.inputSchema || { type: 'object', properties: {} },
39
+ execute: mod.execute || (mod.default && mod.default.execute) || null,
40
+ };
41
+
42
+ // Eğer execute fonksiyonu varsa ekle (CLI'da çalıştırmak için)
43
+ if (meta.execute) {
44
+ tools.push(meta);
45
+ }
46
+ } catch (e) {
47
+ // Sessizce atla — bozuk tool dosyaları kritik değil
48
+ }
49
+ }
50
+
51
+ return tools;
52
+ }
53
+
54
+ /**
55
+ * OpenAI uyumlu API'ye gönderilecek format:
56
+ * [{ type: "function", function: { name, description, parameters } }]
57
+ */
58
+ function toOpenAIFormat(toolDefs) {
59
+ return toolDefs.map(t => ({
60
+ type: 'function',
61
+ function: {
62
+ name: t.name,
63
+ description: t.description,
64
+ parameters: t.parameters,
65
+ },
66
+ }));
67
+ }
68
+
69
+ /**
70
+ * Tool çağrısını çalıştır
71
+ * @param toolName - tool adı
72
+ * @param args - tool argümanları (object)
73
+ * @param toolDefs - loadToolDefinitions() sonucu
74
+ * @returns { result, error }
75
+ */
76
+ async function executeTool(toolName, args, toolDefs) {
77
+ const tool = toolDefs.find(t => t.name === toolName);
78
+ if (!tool) {
79
+ return { error: `Tool bulunamadı: ${toolName}` };
80
+ }
81
+ if (!tool.execute) {
82
+ return { error: `Tool execute fonksiyonu yok: ${toolName}` };
83
+ }
84
+ try {
85
+ const result = await tool.execute(args || {});
86
+ return { result };
87
+ } catch (e) {
88
+ return { error: e.message || String(e) };
89
+ }
90
+ }
91
+
92
+ module.exports = {
93
+ loadToolDefinitions,
94
+ toOpenAIFormat,
95
+ executeTool,
96
+ };