natureco-cli 5.25.0 → 5.26.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/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/src/commands/repl.js +14 -8
- package/src/tools/agentic-runner.js +54 -13
- package/src/tools/workflow.js +77 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NatureCo CLI will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [5.26.0] - 2026-07-04 — "CANLI STREAMING + KEŞFET→DÜZENLE→DOĞRULA"
|
|
6
|
+
|
|
7
|
+
### ✨ Yeni
|
|
8
|
+
- **Canlı streaming (TTY)**: yanit artik token token akarak gelir (Hermes/Claude Code hissi). SSE → tool-call/skill XML'ini gizleyen filtre → model-adi sanitizer ("Ben MiniMax"→"Ben {bot}") zinciri; kelime chunk sinirinda bolunse bile sizinti yok. Non-TTY (pipe/CI) bloklu yola duser. `makeStreamFilter` + `makeSanitizeStream` (birim testli).
|
|
9
|
+
- **Keşfet→düzenle→doğrula**: ajana `file_search` (glob), `list_dir` ve MEVCUT dosyayi hedefli degistiren `edit_file` eklendi; sistem prompt'u "once oku, sonra edit_file ile duzenle, sonra calistirip dogrula" akisini ogretir. E2E dogrulandi (dosyayi oku→edit→node ile calistir→sonucu raporla).
|
|
10
|
+
|
|
11
|
+
### 🐛 Düzeltme
|
|
12
|
+
- **Okuma araclari icerigi modele donmuyordu**: `read_file`/`list_dir`/`file_search`/`bash` sonucu ajana sadece "OK" olarak donuyordu → model icerigi goremeyip "okudum ama bos" diye takiliyordu. Feedback artik gercek ciktiyi (content/output/results) modele verir.
|
|
13
|
+
|
|
14
|
+
Doğrulama: MiniMax ile E2E (oku→edit→calistir; streaming XML/model-adi sizintisiz); **484 test yeşil**, ESLint temiz.
|
|
15
|
+
|
|
5
16
|
## [5.25.0] - 2026-07-04 — "PROVIDER-AGNOSTİK + STREAMING ALTYAPISI"
|
|
6
17
|
|
|
7
18
|
### ✨ İyileştirme
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.26.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
|
@@ -1493,16 +1493,20 @@ async function startRepl(args) {
|
|
|
1493
1493
|
messages[0] = { role: 'system', content: systemPrompt, _internal: true };
|
|
1494
1494
|
|
|
1495
1495
|
// v5.13.0: Run workflow FIRST for every request
|
|
1496
|
-
|
|
1496
|
+
// v5.26: TTY'de canli streaming — spinner yerine yanit akarak gelir (workflow stdout'a akitir)
|
|
1497
|
+
const wantStream = !!(process.stdout && process.stdout.isTTY);
|
|
1498
|
+
if (!wantStream) process.stdout.write(tui.styled('\r 🔧 workflow... ', { color: tui.PALETTE.muted }));
|
|
1497
1499
|
const wfToolDefs = getToolDefs();
|
|
1498
1500
|
const recentHistory = messages.length > 1 ? messages.slice(-10) : [];
|
|
1499
|
-
const wfResult = await executeTool('workflow', { action: 'run', task: line, conversationHistory: recentHistory }, wfToolDefs);
|
|
1501
|
+
const wfResult = await executeTool('workflow', { action: 'run', task: line, conversationHistory: recentHistory, stream: wantStream }, wfToolDefs);
|
|
1500
1502
|
const wf = wfResult?.result || {};
|
|
1501
|
-
if (wf.
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1503
|
+
if (!wf.streamed) {
|
|
1504
|
+
if (wf.success !== false) {
|
|
1505
|
+
const loaded = wf.skillsLoaded && wf.skillsLoaded.length > 0 ? ` [skill: ${wf.skillsLoaded.join(', ')}]` : '';
|
|
1506
|
+
process.stdout.write(tui.styled(` ✓ workflow${loaded}\n`, { color: tui.PALETTE.success }));
|
|
1507
|
+
} else {
|
|
1508
|
+
process.stdout.write(tui.styled(' ✗ workflow\n', { color: tui.PALETTE.danger }));
|
|
1509
|
+
}
|
|
1506
1510
|
}
|
|
1507
1511
|
|
|
1508
1512
|
if (wf.passthrough && wf.reply !== undefined && wf.reply !== null) {
|
|
@@ -1522,7 +1526,9 @@ async function startRepl(args) {
|
|
|
1522
1526
|
fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
|
|
1523
1527
|
fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
|
|
1524
1528
|
fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
|
|
1525
|
-
|
|
1529
|
+
// streamed ise yanit zaten canli basildi (sanitize edilerek) — tekrar basma
|
|
1530
|
+
if (wf.streamed) process.stdout.write('\n');
|
|
1531
|
+
else process.stdout.write('\n' + fixedReply + '\n');
|
|
1526
1532
|
messages.push({ role: 'assistant', content: fixedReply });
|
|
1527
1533
|
totalInputTokens += Math.ceil(line.length / 4);
|
|
1528
1534
|
totalOutputTokens += Math.ceil(fullReply.length / 4);
|
|
@@ -21,7 +21,7 @@ const os = require('os');
|
|
|
21
21
|
// icinde approvals politikasini uyguluyor (isSafeCommand → direkt; tehlikeli → red;
|
|
22
22
|
// digerleri → allowlist/full moda gore). Yani keyfi/yikici komut calismaz.
|
|
23
23
|
// Diger ~85 arac (discord, telegram, cron, browser...) bilerek DISARIDA.
|
|
24
|
-
const DEFAULT_ALLOWED = ['write_file', 'read_file', 'edit_file', 'skill_view', 'bash'];
|
|
24
|
+
const DEFAULT_ALLOWED = ['write_file', 'read_file', 'edit_file', 'skill_view', 'bash', 'file_search', 'list_dir'];
|
|
25
25
|
|
|
26
26
|
const TOOL_ALIASES = {
|
|
27
27
|
write_file: 'write_file', create_file: 'write_file', writefile: 'write_file', write: 'write_file', create: 'write_file', save_file: 'write_file', new_file: 'write_file',
|
|
@@ -29,6 +29,8 @@ const TOOL_ALIASES = {
|
|
|
29
29
|
edit_file: 'edit_file', editfile: 'edit_file', str_replace: 'edit_file', str_replace_editor: 'edit_file', replace_in_file: 'edit_file',
|
|
30
30
|
skill_view: 'skill_view', skillview: 'skill_view', load_skill: 'skill_view', view_skill: 'skill_view', skill: 'skill_view',
|
|
31
31
|
bash: 'bash', run_command: 'bash', shell: 'bash', shell_command: 'bash', exec: 'bash', run_terminal: 'bash', terminal: 'bash', run: 'bash', command: 'bash',
|
|
32
|
+
file_search: 'file_search', glob: 'file_search', find_files: 'file_search', search_files: 'file_search', find: 'file_search',
|
|
33
|
+
list_dir: 'list_dir', ls: 'list_dir', dir: 'list_dir', list_directory: 'list_dir', filesystem: 'list_dir',
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
function expandHome(p) {
|
|
@@ -136,6 +138,35 @@ function makeStreamFilter(onText, onTool) {
|
|
|
136
138
|
};
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Streaming model-adi temizleyici: canli akarken model kendi adini soylerse
|
|
143
|
+
* (MiniMax/Claude/GPT...) persona adiyla degistir. Kelime sinirinda (bosluk)
|
|
144
|
+
* emit eder — boylece bir chunk'ta "Mini", digerinde "Max" gelse bile kelime
|
|
145
|
+
* tam olusmadan gonderilmez, sizinti olmaz. onText'e temizlenmis metin gider.
|
|
146
|
+
*/
|
|
147
|
+
function makeSanitizeStream(botName, onText) {
|
|
148
|
+
const name = botName || 'Asistan';
|
|
149
|
+
let buf = '';
|
|
150
|
+
const clean = (s) => s
|
|
151
|
+
.replace(/\bMiniMaxi?\b/gi, name)
|
|
152
|
+
.replace(/\bChatGPT\b/gi, name)
|
|
153
|
+
.replace(/\bGPT-?[0-9.]*\b/gi, name)
|
|
154
|
+
.replace(/\bClaude(?:\s*[0-9.]+)?\b/gi, name)
|
|
155
|
+
.replace(/\bM2\.5\b/gi, name);
|
|
156
|
+
return {
|
|
157
|
+
push(chunk) {
|
|
158
|
+
buf += (chunk || '');
|
|
159
|
+
const lastWs = Math.max(buf.lastIndexOf(' '), buf.lastIndexOf('\n'), buf.lastIndexOf('\t'));
|
|
160
|
+
if (lastWs >= 0) {
|
|
161
|
+
const safe = buf.slice(0, lastWs + 1);
|
|
162
|
+
buf = buf.slice(lastWs + 1);
|
|
163
|
+
if (onText) onText(clean(safe));
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
end() { if (buf && onText) onText(clean(buf)); buf = ''; },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
139
170
|
function sanitizeArgs(args) {
|
|
140
171
|
const a = { ...args };
|
|
141
172
|
if (typeof a.content === 'string' && a.content.length > 160) a.content = `[${a.content.length} chars]`;
|
|
@@ -143,6 +174,24 @@ function sanitizeArgs(args) {
|
|
|
143
174
|
return a;
|
|
144
175
|
}
|
|
145
176
|
|
|
177
|
+
// Modele geri beslenecek metin. KRITIK: read_file/list_dir/file_search/bash gibi
|
|
178
|
+
// araclarda modelin GORMESI gereken asil cikti (content/output/results/items) geri
|
|
179
|
+
// verilmeli — yoksa model "okudum ama icerik yok" diye takilir.
|
|
180
|
+
function buildFeedback(norm, res) {
|
|
181
|
+
if (typeof res === 'string') return `${norm} sonucu:\n` + res.slice(0, 2500);
|
|
182
|
+
const ok = res && res.success !== false;
|
|
183
|
+
if (!ok) return `${norm} HATA: ${(res && res.error) || 'bilinmeyen hata'}`;
|
|
184
|
+
if (norm === 'write_file') return `write_file OK: ${res.path || ''} (${res.size != null ? res.size : '?'} bytes)`;
|
|
185
|
+
let body = '';
|
|
186
|
+
if (res.content != null) body = String(res.content).slice(0, 2500);
|
|
187
|
+
else if (res.output != null) body = String(res.output).slice(0, 2000);
|
|
188
|
+
else if (res.stdout != null) body = String(res.stdout).slice(0, 2000);
|
|
189
|
+
else if (res.results != null) body = JSON.stringify(res.results).slice(0, 2000);
|
|
190
|
+
else if (res.items != null) body = JSON.stringify(res.items).slice(0, 2000);
|
|
191
|
+
const head = `${norm} OK` + (res.path ? ` (${res.path})` : '');
|
|
192
|
+
return body ? `${head}:\n${body}` : head;
|
|
193
|
+
}
|
|
194
|
+
|
|
146
195
|
/**
|
|
147
196
|
* Tek bir parse edilmis cagriyi calistir.
|
|
148
197
|
* Donus: { records: [...], feedback: '...' }
|
|
@@ -214,17 +263,9 @@ async function executeCall(call, opts = {}) {
|
|
|
214
263
|
}
|
|
215
264
|
try {
|
|
216
265
|
const res = await fn(args);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
feedback = `${norm} sonucu:\n` + res.slice(0, 1500);
|
|
221
|
-
} else {
|
|
222
|
-
const ok = res && res.success !== false;
|
|
223
|
-
status = ok ? 'done' : 'error';
|
|
224
|
-
feedback = ok
|
|
225
|
-
? `${norm} OK` + (res.path ? `: ${res.path} (${res.size != null ? res.size : '?'} bytes)` : (res.output ? ': ' + String(res.output).slice(0, 300) : ''))
|
|
226
|
-
: `${norm} HATA: ${res && res.error}`;
|
|
227
|
-
}
|
|
266
|
+
const ok = typeof res === 'string' ? true : (res && res.success !== false);
|
|
267
|
+
const status = ok ? 'done' : 'error';
|
|
268
|
+
const feedback = buildFeedback(norm, res);
|
|
228
269
|
records.push({ tool: norm, status, args: sanitizeArgs(args), result: res });
|
|
229
270
|
return { records, feedback };
|
|
230
271
|
} catch (e) {
|
|
@@ -278,4 +319,4 @@ async function runAgentic({ callModel, systemPrompt, historyMessages, task, tool
|
|
|
278
319
|
return { records: allRecords, reply: finalReply, iterations };
|
|
279
320
|
}
|
|
280
321
|
|
|
281
|
-
module.exports = { parseAgenticCalls, stripProtocolTokens, executeCall, runAgentic, expandHome, makeStreamFilter, TOOL_ALIASES, DEFAULT_ALLOWED };
|
|
322
|
+
module.exports = { parseAgenticCalls, stripProtocolTokens, executeCall, runAgentic, expandHome, makeStreamFilter, makeSanitizeStream, TOOL_ALIASES, DEFAULT_ALLOWED };
|
package/src/tools/workflow.js
CHANGED
|
@@ -117,6 +117,58 @@ function apiCall(providerUrl, apiKey, body) {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Streaming (SSE) varyanti — onDelta(token) canli cagirilir; sonunda {content, toolCalls} doner.
|
|
121
|
+
// OpenAI-uyumlu delta formati (MiniMax chatcompletion_v2, Gemini /openai, OpenAI, ...).
|
|
122
|
+
function apiCallStream(providerUrl, apiKey, body, onDelta) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const base = providerUrl.replace(/\/+$/, '');
|
|
125
|
+
const endpoint = isMiniMax(base)
|
|
126
|
+
? base + '/v1/text/chatcompletion_v2'
|
|
127
|
+
: isGemini(base)
|
|
128
|
+
? base + '/openai/chat/completions'
|
|
129
|
+
: base + '/chat/completions';
|
|
130
|
+
const req = https.request(endpoint, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
|
133
|
+
timeout: 120000,
|
|
134
|
+
}, (res) => {
|
|
135
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
136
|
+
let data = '';
|
|
137
|
+
res.on('data', c => data += c);
|
|
138
|
+
res.on('end', () => reject(new Error('HTTP ' + res.statusCode + ': ' + data.slice(0, 300))));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
res.setEncoding('utf8');
|
|
142
|
+
let sseBuf = '';
|
|
143
|
+
let content = '';
|
|
144
|
+
const toolCalls = [];
|
|
145
|
+
res.on('data', (chunk) => {
|
|
146
|
+
sseBuf += chunk;
|
|
147
|
+
const parts = sseBuf.split('\n');
|
|
148
|
+
sseBuf = parts.pop(); // eksik son satiri buffer'da tut
|
|
149
|
+
for (const line of parts) {
|
|
150
|
+
const l = line.trim();
|
|
151
|
+
if (!l.startsWith('data:')) continue;
|
|
152
|
+
const payload = l.slice(5).trim();
|
|
153
|
+
if (!payload || payload === '[DONE]') continue;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(payload);
|
|
156
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
157
|
+
if (!delta) continue;
|
|
158
|
+
if (delta.content) { content += delta.content; if (onDelta) onDelta(delta.content); }
|
|
159
|
+
if (Array.isArray(delta.tool_calls)) { for (const tc of delta.tool_calls) toolCalls.push(tc); }
|
|
160
|
+
} catch { /* eksik/parcali JSON — atla */ }
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
res.on('end', () => resolve({ content, toolCalls }));
|
|
164
|
+
});
|
|
165
|
+
req.on('error', reject);
|
|
166
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
167
|
+
req.write(JSON.stringify(body));
|
|
168
|
+
req.end();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
120
172
|
async function workflow(params) {
|
|
121
173
|
const { action, task, steps, name, workflowId, regenerateStep, conversationHistory } = params;
|
|
122
174
|
const cfg = loadConfig();
|
|
@@ -176,11 +228,15 @@ async function workflow(params) {
|
|
|
176
228
|
'\n\nKullanabilecegin araclar:',
|
|
177
229
|
'- write_file: dosya olustur/uzerine yaz. parametreler: path (TAM yol), content (dosyanin TAM icerigi). Kod/oyun/site isteniyorsa TUM icerigi content icine yaz, kisaltma.',
|
|
178
230
|
'- read_file: dosya oku. parametre: path',
|
|
179
|
-
'- edit_file: dosyada metin degistir. parametreler: path, old_string, new_string',
|
|
180
|
-
'-
|
|
231
|
+
'- edit_file: MEVCUT dosyada metin degistir. parametreler: path, old_string (birebir mevcut metin), new_string. Bir dosyanin bir kismini degistirirken write_file yerine bunu kullan (tum dosyayi yeniden yazma).',
|
|
232
|
+
'- file_search: glob ile dosya bul. parametre: pattern (orn. "**/*.js", "src/**/*.json").',
|
|
233
|
+
'- list_dir: dizin icerigini listele. parametre: path',
|
|
234
|
+
'- bash: kabuk komutu calistir (npm, git, node, python, test, ls, grep/findstr, mkdir...). parametre: command. Guvenli komutlar dogrudan calisir; yikici/tehlikeli komutlar guvenlik politikasiyla engellenir. Icerik aramasi icin grep/findstr kullan.',
|
|
181
235
|
'- skill_view: gorevle ilgili bir skill yukle. parametre: name',
|
|
182
236
|
'\nKurallar:',
|
|
183
|
-
'-
|
|
237
|
+
'- MEVCUT bir dosyayi degistirmeden ONCE read_file ile oku; sonra edit_file ile hedefli degisiklik yap (tum dosyayi write_file ile ezme).',
|
|
238
|
+
'- Bir seyi nerede oldugunu bilmiyorsan once file_search/list_dir/bash(grep) ile kesfet.',
|
|
239
|
+
'- Kod yazdiktan/degistirdikten sonra gerektiginde bash ile calistir/test et (orn. "node dosya.js", "npm test"); hata cikarsa duzelt.',
|
|
184
240
|
'- Birden fazla dosya gerekiyorsa her biri icin AYRI write_file cagir.',
|
|
185
241
|
'- Kullanici "masaustu"/"desktop" dediyse ve tam yol vermediyse dosyayi buraya yaz: ' + desktop,
|
|
186
242
|
'- Goreceli yol yerine TAM yol kullan.',
|
|
@@ -194,7 +250,21 @@ async function workflow(params) {
|
|
|
194
250
|
for (const hm of conversationHistory) { if (hm._internal) continue; historyMessages.push({ role: hm.role, content: hm.content || '' }); }
|
|
195
251
|
}
|
|
196
252
|
|
|
253
|
+
// Streaming: yalnizca TTY'de ve caller (repl) stream:true gecince. SSE token'lari
|
|
254
|
+
// XML-gizleyen filtre + model-adi sanitizer zincirinden gecip stdout'a canli akar.
|
|
255
|
+
const streamOn = params.stream === true && !!(process.stdout && process.stdout.isTTY);
|
|
256
|
+
const botName = cfg.botName || 'Asistan';
|
|
257
|
+
const { makeStreamFilter, makeSanitizeStream } = require('./agentic-runner');
|
|
258
|
+
|
|
197
259
|
async function callModel(msgs) {
|
|
260
|
+
if (streamOn) {
|
|
261
|
+
const sani = makeSanitizeStream(botName, t => process.stdout.write(t));
|
|
262
|
+
const filter = makeStreamFilter(t => sani.push(t), null);
|
|
263
|
+
const body = { model, stream: true, messages: msgs, temperature: 0.3, max_tokens: 16000 };
|
|
264
|
+
const out = await apiCallStream(providerUrl, providerApiKey, body, d => filter.push(d));
|
|
265
|
+
filter.end(); sani.end();
|
|
266
|
+
return { content: out.content || '', toolCalls: out.toolCalls || [] };
|
|
267
|
+
}
|
|
198
268
|
const body = { model, stream: false, messages: msgs, temperature: 0.3, max_tokens: 16000 };
|
|
199
269
|
const r = await apiCall(providerUrl, providerApiKey, body);
|
|
200
270
|
const msg = r.choices?.[0]?.message || {};
|
|
@@ -210,7 +280,9 @@ async function workflow(params) {
|
|
|
210
280
|
let finalReply = reply || '';
|
|
211
281
|
if (fileWrites.length > 0) {
|
|
212
282
|
const lines = fileWrites.map(r => ` ✓ ${path.basename((r.result && r.result.path) || (r.args && r.args.path) || '')} olusturuldu (${(r.result && r.result.size != null) ? r.result.size : '?'} bytes)`).join('\n');
|
|
213
|
-
|
|
283
|
+
const fileSummary = 'Dosya(lar):\n' + lines;
|
|
284
|
+
finalReply = (finalReply ? finalReply + '\n\n' : '') + fileSummary;
|
|
285
|
+
if (streamOn) process.stdout.write('\n\n' + fileSummary + '\n');
|
|
214
286
|
}
|
|
215
287
|
const done = records.filter(r => r.status === 'done').length;
|
|
216
288
|
return {
|
|
@@ -222,6 +294,7 @@ async function workflow(params) {
|
|
|
222
294
|
completedSteps: done,
|
|
223
295
|
results: records.map((r, i) => ({ step: i + 1, ...r })),
|
|
224
296
|
passthrough: true,
|
|
297
|
+
streamed: streamOn,
|
|
225
298
|
reply: finalReply || 'Tamamlandi.',
|
|
226
299
|
};
|
|
227
300
|
} catch (e) {
|