natureco-cli 5.25.0 → 5.27.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 CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to NatureCo CLI will be documented in this file.
4
4
 
5
+ ## [5.27.0] - 2026-07-04 — "GEMINI DÜZELTMESİ + DÖNGÜ BİRLEŞTİRME (2 sağlayıcı E2E)"
6
+
7
+ ### 🐛 Düzeltme / ✨ İyileştirme
8
+ - **Gemini artik calisiyor**: `supportsToolCalls()` Gemini'yi robust agentic-runner yoluna aldi. Neden: `gemini-2.5-flash` bir "thinking" modeli — plan-bazli yoldaki dusuk `max_tokens` (orn. 20) cagrilarinda tum butceyi ic dusunmede harcayip BOS donuyordu. Agentic yol (yuksek max_tokens + native tool_calls/XML parse) bunu cozer. (Kod yorumu zaten "Gemini desteklemez" diyordu ama uygulanmamisti — tutarsizlik giderildi.)
9
+ - **Dongu birlestirme (dogrulanmis)**: `agentic-runner` artik 2 farkli saglayici ailesinde E2E dogrulandi — **MiniMax** (native XML tool-call) + **Gemini** (OpenAI-compat, gercek key ile dosya olusturma + memory recall "Gencay"). Groq/Ollama/localhost da bu yolu kullanir. OpenAI/Anthropic native `tool_calls` yolunda kalir (standart, dokunulmadi).
10
+
11
+ Doğrulama: Gemini gercek key ile E2E (dosya olusturma + hafiza); **484 test yeşil**, ESLint temiz.
12
+
13
+ ## [5.26.0] - 2026-07-04 — "CANLI STREAMING + KEŞFET→DÜZENLE→DOĞRULA"
14
+
15
+ ### ✨ Yeni
16
+ - **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).
17
+ - **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).
18
+
19
+ ### 🐛 Düzeltme
20
+ - **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.
21
+
22
+ Doğrulama: MiniMax ile E2E (oku→edit→calistir; streaming XML/model-adi sizintisiz); **484 test yeşil**, ESLint temiz.
23
+
5
24
  ## [5.25.0] - 2026-07-04 — "PROVIDER-AGNOSTİK + STREAMING ALTYAPISI"
6
25
 
7
26
  ### ✨ İyileştirme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.25.0",
3
+ "version": "5.27.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"
@@ -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
- process.stdout.write(tui.styled('\r 🔧 workflow... ', { color: tui.PALETTE.muted }));
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.success !== false) {
1502
- const loaded = wf.skillsLoaded && wf.skillsLoaded.length > 0 ? ` [skill: ${wf.skillsLoaded.join(', ')}]` : '';
1503
- process.stdout.write(tui.styled(` ✓ workflow${loaded}\n`, { color: tui.PALETTE.success }));
1504
- } else {
1505
- process.stdout.write(tui.styled(' ✗ workflow\n', { color: tui.PALETTE.danger }));
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
- process.stdout.write('\n' + fixedReply + '\n');
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
- let feedback, status;
218
- if (typeof res === 'string') {
219
- status = 'done';
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 };
@@ -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();
@@ -149,8 +201,10 @@ async function workflow(params) {
149
201
  // Non-tool-calling model tespiti
150
202
  function supportsToolCalls() {
151
203
  const url = (providerUrl || '').toLowerCase();
152
- // MiniMax, Gemini (direct), Ollama, Mistral (direct) tool calling'i desteklemez
204
+ // Bu saglayicilar OpenAI-tarzi tool_calls JSON'unu guvenilir uretmiyor (ya native
205
+ // XML uretiyor ya da dusuk max_tokens'ta bos donuyor) → robust agentic-runner yolu.
153
206
  if (url.includes('minimax')) return false;
207
+ if (url.includes('generativelanguage') || url.includes('gemini')) return false; // Gemini 2.5 "thinking" — plan yolundaki dusuk max_tokens'ta bos doner
154
208
  if (url.includes('ollama')) return false;
155
209
  if (url.includes('localhost')) return false;
156
210
  if (url.includes('groq')) return false;
@@ -176,11 +230,15 @@ async function workflow(params) {
176
230
  '\n\nKullanabilecegin araclar:',
177
231
  '- 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
232
  '- read_file: dosya oku. parametre: path',
179
- '- edit_file: dosyada metin degistir. parametreler: path, old_string, new_string',
180
- '- bash: kabuk komutu calistir (npm, git, node, python, test, ls, mkdir...). parametre: command. Guvenli komutlar dogrudan calisir; yikici/tehlikeli komutlar guvenlik politikasiyla engellenir.',
233
+ '- 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).',
234
+ '- file_search: glob ile dosya bul. parametre: pattern (orn. "**/*.js", "src/**/*.json").',
235
+ '- list_dir: dizin icerigini listele. parametre: path',
236
+ '- 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
237
  '- skill_view: gorevle ilgili bir skill yukle. parametre: name',
182
238
  '\nKurallar:',
183
- '- Kod yazdiktan sonra gerektiginde bash ile calistir/test et (orn. "node dosya.js", "npm test"); hata cikarsa duzelt.',
239
+ '- MEVCUT bir dosyayi degistirmeden ONCE read_file ile oku; sonra edit_file ile hedefli degisiklik yap (tum dosyayi write_file ile ezme).',
240
+ '- Bir seyi nerede oldugunu bilmiyorsan once file_search/list_dir/bash(grep) ile kesfet.',
241
+ '- Kod yazdiktan/degistirdikten sonra gerektiginde bash ile calistir/test et (orn. "node dosya.js", "npm test"); hata cikarsa duzelt.',
184
242
  '- Birden fazla dosya gerekiyorsa her biri icin AYRI write_file cagir.',
185
243
  '- Kullanici "masaustu"/"desktop" dediyse ve tam yol vermediyse dosyayi buraya yaz: ' + desktop,
186
244
  '- Goreceli yol yerine TAM yol kullan.',
@@ -194,7 +252,21 @@ async function workflow(params) {
194
252
  for (const hm of conversationHistory) { if (hm._internal) continue; historyMessages.push({ role: hm.role, content: hm.content || '' }); }
195
253
  }
196
254
 
255
+ // Streaming: yalnizca TTY'de ve caller (repl) stream:true gecince. SSE token'lari
256
+ // XML-gizleyen filtre + model-adi sanitizer zincirinden gecip stdout'a canli akar.
257
+ const streamOn = params.stream === true && !!(process.stdout && process.stdout.isTTY);
258
+ const botName = cfg.botName || 'Asistan';
259
+ const { makeStreamFilter, makeSanitizeStream } = require('./agentic-runner');
260
+
197
261
  async function callModel(msgs) {
262
+ if (streamOn) {
263
+ const sani = makeSanitizeStream(botName, t => process.stdout.write(t));
264
+ const filter = makeStreamFilter(t => sani.push(t), null);
265
+ const body = { model, stream: true, messages: msgs, temperature: 0.3, max_tokens: 16000 };
266
+ const out = await apiCallStream(providerUrl, providerApiKey, body, d => filter.push(d));
267
+ filter.end(); sani.end();
268
+ return { content: out.content || '', toolCalls: out.toolCalls || [] };
269
+ }
198
270
  const body = { model, stream: false, messages: msgs, temperature: 0.3, max_tokens: 16000 };
199
271
  const r = await apiCall(providerUrl, providerApiKey, body);
200
272
  const msg = r.choices?.[0]?.message || {};
@@ -210,7 +282,9 @@ async function workflow(params) {
210
282
  let finalReply = reply || '';
211
283
  if (fileWrites.length > 0) {
212
284
  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
- finalReply = (finalReply ? finalReply + '\n\n' : '') + 'Dosya(lar):\n' + lines;
285
+ const fileSummary = 'Dosya(lar):\n' + lines;
286
+ finalReply = (finalReply ? finalReply + '\n\n' : '') + fileSummary;
287
+ if (streamOn) process.stdout.write('\n\n' + fileSummary + '\n');
214
288
  }
215
289
  const done = records.filter(r => r.status === 'done').length;
216
290
  return {
@@ -222,6 +296,7 @@ async function workflow(params) {
222
296
  completedSteps: done,
223
297
  results: records.map((r, i) => ({ step: i + 1, ...r })),
224
298
  passthrough: true,
299
+ streamed: streamOn,
225
300
  reply: finalReply || 'Tamamlandi.',
226
301
  };
227
302
  } catch (e) {