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 +1 -1
- package/src/commands/repl.js +215 -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.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"
|
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() {
|
|
@@ -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(
|
|
521
|
-
|
|
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
|
+
};
|