natureco-cli 4.7.4 → 4.8.1

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.4",
3
+ "version": "4.8.1",
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.3, 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() {
@@ -320,6 +462,8 @@ async function startRepl(args) {
320
462
  // v4.7.4: Daha agresif dil zorlaması — model önceki versiyonlarda İngilizce cevap veriyordu
321
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ı.',
322
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ş.',
465
+ // v4.8.0: Kullanıcının dosya yollarını bil — tool'lar doğru path kullansın
466
+ cfg.userHome ? `Kullanıcının home dizini: ${cfg.userHome}. Downloads: ${cfg.userHome}/Downloads, Desktop: ${cfg.userHome}/Desktop. Dosya işlemlerinde BU path'leri kullan.` : '',
323
467
  memory.nickname && memory.name
324
468
  ? `Kullanıcının adı: ${memory.name}. Sana "${memory.nickname}" diye hitap etmesinden hoşlanıyor.`
325
469
  : memory.name ? `Kullanıcının adı: ${memory.name}.` : '',
@@ -517,9 +661,33 @@ async function startRepl(args) {
517
661
  process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
518
662
  try {
519
663
  const apiMessages = messages.filter(m => !m._internal);
520
- const reply = await sendStreaming(providerUrl, providerApiKey, apiMessages, model, (chunk) => {
521
- process.stdout.write(chunk);
522
- });
664
+ const reply = await sendStreaming(
665
+ providerUrl,
666
+ providerApiKey,
667
+ apiMessages,
668
+ model,
669
+ // Text chunk callback
670
+ (chunk) => process.stdout.write(chunk),
671
+ // Tool call callback — kullanıcıya göster
672
+ (toolEvent) => {
673
+ if (toolEvent.status === 'running') {
674
+ process.stdout.write('\n');
675
+ console.log(tui.styled(' 🔧 Tool: ' + toolEvent.name, { color: tui.PALETTE.accent, bold: true }));
676
+ const argsStr = JSON.stringify(toolEvent.args).slice(0, 120);
677
+ console.log(tui.styled(' Args: ' + argsStr, { color: tui.PALETTE.muted }));
678
+ } else if (toolEvent.status === 'done') {
679
+ if (toolEvent.result.error) {
680
+ console.log(tui.styled(' ✗ Hata: ' + toolEvent.result.error.slice(0, 100), { color: tui.PALETTE.danger }));
681
+ } else {
682
+ const resultStr = typeof toolEvent.result.result === 'string'
683
+ ? toolEvent.result.result.slice(0, 200)
684
+ : JSON.stringify(toolEvent.result.result).slice(0, 200);
685
+ console.log(tui.styled(' ✓ Sonuç: ' + resultStr, { color: tui.PALETTE.success }));
686
+ }
687
+ process.stdout.write(tui.styled(' AI ', { color: tui.PALETTE.secondary, bold: true }));
688
+ }
689
+ }
690
+ );
523
691
  process.stdout.write('\n');
524
692
  messages.push({ role: 'assistant', content: reply });
525
693
  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
+ };