natureco-cli 4.7.4 → 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.4",
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.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() {
@@ -517,9 +659,33 @@ async function startRepl(args) {
517
659
  process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
518
660
  try {
519
661
  const apiMessages = messages.filter(m => !m._internal);
520
- const reply = await sendStreaming(providerUrl, providerApiKey, apiMessages, model, (chunk) => {
521
- process.stdout.write(chunk);
522
- });
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
+ );
523
689
  process.stdout.write('\n');
524
690
  messages.push({ role: 'assistant', content: reply });
525
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
+ };