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 +1 -1
- package/src/commands/repl.js +213 -47
- package/src/utils/tools.js +96 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "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"
|
package/src/commands/repl.js
CHANGED
|
@@ -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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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(
|
|
521
|
-
|
|
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
|
+
};
|