natureco-cli 5.23.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 CHANGED
@@ -2,6 +2,43 @@
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
+
16
+ ## [5.25.0] - 2026-07-04 — "PROVIDER-AGNOSTİK + STREAMING ALTYAPISI"
17
+
18
+ ### ✨ İyileştirme
19
+ - **Provider-agnostik**: agentic düzeltmeler MiniMax'e özel DEĞİL — `workflow.js`'in paylaşılan non-tool-calling dalında çalışır (MiniMax, Groq, Ollama, yerel modeller). Tool-calling sağlayıcıları (OpenAI, Anthropic, Gemini) kendi native `tool_calls` yolunu kullanır (dokunulmadı). `agentic-runner` hem native `tool_calls` hem native XML parse eder → sağlayıcıdan bağımsız çalışır.
20
+ - **Streaming altyapısı**: canlı akışta tool-call/skill protokol jetonlarını (`<minimax:tool_call>`, `<invoke>`, `<skill>`) gizleyip düz metni gösteren, chunk sınırında bölünen tag'leri doğru işleyen akış filtresi (`makeStreamFilter`, 4 test). Tam devreye alma (streaming-güvenli model-adı temizleme ile birlikte) sonraki adım.
21
+
22
+ > Not: v5.24.0'daki büyük düzeltmeler (MiniMax dosya yazma, komut çalıştırma, memory recall) bu sürümde de yer alır.
23
+
24
+ ## [5.24.0] - 2026-07-04 — "AGENTIC SERTLEŞTİRME" (MiniMax native tool-call + komut çalıştırma)
25
+
26
+ Kök neden: **MiniMax M2.5 agentic bir model** — tool call'ları OpenAI `tool_calls` JSON'u yerine metin içinde native XML olarak üretir (`<minimax:tool_call><invoke name="write_file">...`) ve skill'i `<skill>ad</skill>` ile yükler. 5.23.0'ın tek-atış JSON planı bunu yakalayamıyordu; `JSON.parse` patlayıp boş `catch{}` yutunca dosya sessizce yazılmıyordu ("masaüstünde yarış oyunu yapamadı").
27
+
28
+ ### ✨ Yeni
29
+ - **`src/tools/agentic-runner.js`**: MiniMax'in native XML/skill protokolünü parse edip gerçek araçları çalıştıran bounded agentic döngü (parse→execute→sonucu geri besle→dur, max 15 iterasyon). `workflow.js`'in non-tool-calling dalı buna bağlandı. 13 yeni birim testi.
30
+ - **Komut çalıştırma (onaylı)**: ajan artık `bash` ile npm/git/node/test çalıştırıp çıktıya göre devam edebilir — gerçek **yaz→çalıştır→test→düzelt** döngüsü.
31
+
32
+ ### 🐛 Düzeltme
33
+ - **MiniMax dosya yazma**: native `<invoke>`/`<skill>` parse edilip gerçek araçlara yönlendiriliyor; büyük içerikteki JSON-kaçış sorunu ortadan kalktı.
34
+ - **Memory recall split-brain**: hafıza `default.json`'da tutulurken okuyucular `<userName>.json` arıyordu → chat/code kullanıcıyı hiç hatırlamıyordu ("adım ne?"→"bilmiyorum"). `loadUserMemory` + `repl.loadMemory` artık `<user>.json` + legacy `default.json`'ı birleştirir (isim-eşleşme guard'ı).
35
+ - **Bot personası**: jenerik "Asistan" placeholder'ı gerçek persona (örn. "Hinata") ile eziliyor.
36
+
37
+ ### 🔒 Güvenlik
38
+ - **Ajan modunda yıkıcı komut guard'ı**: varsayılan 'full' politika `rm -rf /`'i bile geçiriyordu (insan için bilinçli olabilir, model için değil). Agentic-runner artık `isDangerousCommand`'ı politikadan bağımsız uygular — yıkıcı komutlar ajan tarafından **çalıştırılmaz**. Diğer ~85 araç allowlist dışı (yalnızca write_file/read_file/edit_file/skill_view/bash).
39
+
40
+ Doğrulama: gerçek MiniMax API ile uçtan uca (yarış oyunu + "node script yaz&çalıştır"→42); **474 test yeşil**.
41
+
5
42
  ## [5.23.0] - 2026-07-02 — "NON-TOOL-CALLING FIX" (MiniMax dosya yazma)
6
43
 
7
44
  ### 🐛 Düzeltme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.23.0",
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"
@@ -145,11 +145,36 @@ function getConfig() {
145
145
  const { isMiniMax, isGemini, buildChatEndpoint } = require('../utils/provider-detect');
146
146
 
147
147
  function loadMemory(username) {
148
- const file = path.join(MEMORY_DIR, `${(username || 'default').toLowerCase()}.json`);
149
- try {
150
- if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8'));
151
- } catch {}
152
- return { name: username || 'Kullanıcı', nickname: null, botName: 'Asistan', facts: [], preferences: [], history: [] };
148
+ const uname = (username || 'default').toLowerCase();
149
+ const base = { name: username || 'Kullanıcı', nickname: null, botName: null, facts: [], preferences: [], history: [] };
150
+ const readJson = (f) => { try { return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null; } catch { return null; } };
151
+
152
+ const userMem = readJson(path.join(MEMORY_DIR, `${uname}.json`));
153
+ const merged = userMem
154
+ ? { ...base, ...userMem, facts: [...(userMem.facts || [])], preferences: [...(userMem.preferences || [])], history: [...(userMem.history || [])] }
155
+ : { ...base };
156
+
157
+ // Legacy default.json'i birlestir (isim eslesiyorsa ya da isimsizse): eski kurulumlarda
158
+ // hafiza + bot personasi default.json'da kalmis olabiliyordu; <user>.json ile split-brain
159
+ // olusuyordu. Birlestirince recall calisir ve ilk kayitta konsolide olur.
160
+ if (uname !== 'default') {
161
+ const def = readJson(path.join(MEMORY_DIR, 'default.json'));
162
+ if (def && (!def.name || String(def.name).toLowerCase() === uname)) {
163
+ // Jenerik "Asistan" placeholder'ini gercek persona (orn. Hinata) ile ez
164
+ const isGeneric = (b) => !b || /^asistan$/i.test(String(b));
165
+ if (isGeneric(merged.botName) && def.botName && !isGeneric(def.botName)) merged.botName = def.botName;
166
+ if ((!merged.name || merged.name === 'Kullanıcı') && def.name) merged.name = def.name;
167
+ const factVal = (f) => ((f && (f.value != null ? f.value : f)) || '').toString().trim();
168
+ const seen = new Set(merged.facts.map(f => factVal(f).toLowerCase()));
169
+ for (const f of (def.facts || [])) {
170
+ const v = factVal(f);
171
+ if (v && !seen.has(v.toLowerCase())) { seen.add(v.toLowerCase()); merged.facts.push(f); }
172
+ }
173
+ }
174
+ }
175
+
176
+ if (!merged.botName) merged.botName = 'Asistan';
177
+ return merged;
153
178
  }
154
179
 
155
180
  function saveMemory(username, memory) {
@@ -1468,16 +1493,20 @@ async function startRepl(args) {
1468
1493
  messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1469
1494
 
1470
1495
  // v5.13.0: Run workflow FIRST for every request
1471
- 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 }));
1472
1499
  const wfToolDefs = getToolDefs();
1473
1500
  const recentHistory = messages.length > 1 ? messages.slice(-10) : [];
1474
- 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);
1475
1502
  const wf = wfResult?.result || {};
1476
- if (wf.success !== false) {
1477
- const loaded = wf.skillsLoaded && wf.skillsLoaded.length > 0 ? ` [skill: ${wf.skillsLoaded.join(', ')}]` : '';
1478
- process.stdout.write(tui.styled(` ✓ workflow${loaded}\n`, { color: tui.PALETTE.success }));
1479
- } else {
1480
- 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
+ }
1481
1510
  }
1482
1511
 
1483
1512
  if (wf.passthrough && wf.reply !== undefined && wf.reply !== null) {
@@ -1497,7 +1526,9 @@ async function startRepl(args) {
1497
1526
  fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1498
1527
  fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
1499
1528
  fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
1500
- 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');
1501
1532
  messages.push({ role: 'assistant', content: fixedReply });
1502
1533
  totalInputTokens += Math.ceil(line.length / 4);
1503
1534
  totalOutputTokens += Math.ceil(fullReply.length / 4);
@@ -0,0 +1,322 @@
1
+ /**
2
+ * agentic-runner — "agentic-text" modeller (MiniMax M2.x gibi) icin bounded tool döngüsü.
3
+ *
4
+ * Neden: MiniMax M2.5 tool call'larini OpenAI tarzi `message.tool_calls` JSON'u yerine
5
+ * dogrudan metin icinde native XML olarak uretir:
6
+ * <minimax:tool_call><invoke name="write_file"><parameter name="path">...</parameter>
7
+ * <parameter name="content">...</parameter></invoke></minimax:tool_call>
8
+ * Ayrica skill'leri <skill>ad</skill> ile yuklemek ister. Eski passthrough bunlari
9
+ * hic islemedigi icin (JSON.parse patliyor, bos catch yutuyor) dosya asla yazilmiyordu.
10
+ *
11
+ * Bu modul o XML'i parse eder, gercek araclari (write_file/read_file/edit_file/skill_view)
12
+ * calistirir, sonuclari modele geri besler ve is bitene kadar (ya da MAX adima kadar) doner.
13
+ *
14
+ * Guvenlik: sadece allowlist'teki araclar calisir. bash/shell gibi keyfi komut calistirma
15
+ * bu modda KAPALIDIR (onay katmanini atlamamak icin).
16
+ */
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ // Agentic dongude izin verilen araclar. bash BURADA ama guvenli: bash.js kendi
21
+ // icinde approvals politikasini uyguluyor (isSafeCommand → direkt; tehlikeli → red;
22
+ // digerleri → allowlist/full moda gore). Yani keyfi/yikici komut calismaz.
23
+ // Diger ~85 arac (discord, telegram, cron, browser...) bilerek DISARIDA.
24
+ const DEFAULT_ALLOWED = ['write_file', 'read_file', 'edit_file', 'skill_view', 'bash', 'file_search', 'list_dir'];
25
+
26
+ const TOOL_ALIASES = {
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',
28
+ read_file: 'read_file', readfile: 'read_file', view_file: 'read_file', open_file: 'read_file', cat: 'read_file',
29
+ edit_file: 'edit_file', editfile: 'edit_file', str_replace: 'edit_file', str_replace_editor: 'edit_file', replace_in_file: 'edit_file',
30
+ skill_view: 'skill_view', skillview: 'skill_view', load_skill: 'skill_view', view_skill: 'skill_view', skill: 'skill_view',
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',
34
+ };
35
+
36
+ function expandHome(p) {
37
+ if (!p || typeof p !== 'string') return p;
38
+ if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
39
+ return p;
40
+ }
41
+
42
+ // Ajanin urettigi komutlar icin ekstra koruma. bash.js kendi politikasini uygular
43
+ // ama varsayilan 'full' mod yikici komutlari (rm -rf gibi) bile gecirir — bu insan
44
+ // icin bilincli olabilir, ama MODELIN urettigi komut icin degil. Bu yuzden agentic
45
+ // yolda yikici komutlari politikadan BAGIMSIZ olarak engelliyoruz.
46
+ function defaultIsDangerous(cmd) {
47
+ try { return require('../utils/approvals').isDangerousCommand(cmd); } catch { return false; }
48
+ }
49
+
50
+ /**
51
+ * Model metninden agentic tool cagrilarini cikar.
52
+ * Destekler: <invoke name><parameter name> (opsiyonel <minimax:tool_call> sarmali),
53
+ * <skill>ad</skill> kisayolu, ve yan yana gelen native OpenAI tool_calls.
54
+ */
55
+ function parseAgenticCalls(content, nativeToolCalls) {
56
+ const calls = [];
57
+ content = content || '';
58
+
59
+ for (const tc of nativeToolCalls || []) {
60
+ const fn = tc.function || {};
61
+ if (!fn.name) continue;
62
+ let args = {};
63
+ try { args = typeof fn.arguments === 'string' ? JSON.parse(fn.arguments || '{}') : (fn.arguments || {}); } catch {}
64
+ calls.push({ tool: fn.name, args });
65
+ }
66
+
67
+ const invokeRe = /<invoke\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/invoke>/g;
68
+ let m;
69
+ while ((m = invokeRe.exec(content)) !== null) {
70
+ const tool = m[1].trim();
71
+ const body = m[2];
72
+ const args = {};
73
+ const paramRe = /<parameter\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/parameter>/g;
74
+ let pm;
75
+ while ((pm = paramRe.exec(body)) !== null) {
76
+ args[pm[1].trim()] = pm[2];
77
+ }
78
+ calls.push({ tool, args });
79
+ }
80
+
81
+ const skillRe = /<skill>\s*([^<>\n]+?)\s*<\/skill>/g;
82
+ while ((m = skillRe.exec(content)) !== null) {
83
+ const name = m[1].trim();
84
+ if (!calls.some(c => /skill/i.test(c.tool || '') && c.args && c.args.name === name)) {
85
+ calls.push({ tool: 'skill_view', args: { name } });
86
+ }
87
+ }
88
+
89
+ return calls;
90
+ }
91
+
92
+ /** Final yanittan protokol jetonlarini temizle (kullaniciya gosterim icin). */
93
+ function stripProtocolTokens(s) {
94
+ return (s || '')
95
+ .replace(/<minimax:tool_call>[\s\S]*?<\/minimax:tool_call>/g, '')
96
+ .replace(/<invoke[\s\S]*?<\/invoke>/g, '')
97
+ .replace(/<skill>[\s\S]*?<\/skill>/g, '')
98
+ .replace(/<available_skills>[\s\S]*?<\/available_skills>/g, '')
99
+ .replace(/<\/?(minimax:tool_call|available_skills|available_skins)>/g, '')
100
+ .trim();
101
+ }
102
+
103
+ /**
104
+ * Streaming ekran filtresi: model canli akarken duz metni goster, ama tool-call/skill
105
+ * protokol bloklarini (<minimax:tool_call>, <invoke>, <skill>, <available_skills>)
106
+ * kullaniciya HAM gosterme. Bir protokol jetonu gorunce o tur icin gostermeyi keser
107
+ * (icerik yine tam biriktirilir; parse sonra yapilir). Kismi tag chunk sinirinda
108
+ * bolunebilir — sonda olasi kismi-tag kuyrugunu tutar.
109
+ *
110
+ * const f = makeStreamFilter(t => process.stdout.write(t), () => process.stdout.write(' 🔧'));
111
+ * f.push(deltaChunk); ... ; f.end();
112
+ */
113
+ function makeStreamFilter(onText, onTool) {
114
+ const MARKERS = ['<minimax:tool_call', '<invoke', '<skill>', '<available_skills'];
115
+ let buf = '';
116
+ let suppressed = false;
117
+ return {
118
+ push(chunk) {
119
+ if (suppressed) return;
120
+ buf += (chunk || '');
121
+ let idx = -1;
122
+ for (const mk of MARKERS) { const i = buf.indexOf(mk); if (i !== -1 && (idx === -1 || i < idx)) idx = i; }
123
+ if (idx !== -1) {
124
+ if (idx > 0 && onText) onText(buf.slice(0, idx));
125
+ buf = '';
126
+ suppressed = true;
127
+ if (onTool) onTool();
128
+ return;
129
+ }
130
+ const lastLt = buf.lastIndexOf('<');
131
+ if (lastLt === -1) { if (buf && onText) onText(buf); buf = ''; return; }
132
+ if (lastLt > 0 && onText) onText(buf.slice(0, lastLt));
133
+ buf = buf.slice(lastLt);
134
+ // buf artik '<' ile basliyor; bir marker'a buyuyebilecekse tut, degilse yaz (orn. literal <div>)
135
+ if (!MARKERS.some(mk => mk.startsWith(buf))) { if (onText) onText(buf); buf = ''; }
136
+ },
137
+ end() { if (!suppressed && buf && onText) onText(buf); buf = ''; return suppressed; },
138
+ };
139
+ }
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
+
170
+ function sanitizeArgs(args) {
171
+ const a = { ...args };
172
+ if (typeof a.content === 'string' && a.content.length > 160) a.content = `[${a.content.length} chars]`;
173
+ if (typeof a.new_string === 'string' && a.new_string.length > 160) a.new_string = `[${a.new_string.length} chars]`;
174
+ return a;
175
+ }
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
+
195
+ /**
196
+ * Tek bir parse edilmis cagriyi calistir.
197
+ * Donus: { records: [...], feedback: '...' }
198
+ */
199
+ async function executeCall(call, opts = {}) {
200
+ const toolsDir = opts.toolsDir || __dirname;
201
+ const loadTool = opts.loadTool || ((n) => require(path.join(toolsDir, n + '.js')));
202
+ const allowed = opts.allowed || new Set(DEFAULT_ALLOWED);
203
+ const rawTool = (call.tool || '').trim();
204
+ const norm = TOOL_ALIASES[rawTool.toLowerCase()] || rawTool;
205
+ const records = [];
206
+ const feedbacks = [];
207
+
208
+ // "files" JSON dizisi (bulk-file-operations veya files parametresi) → coklu write_file
209
+ let files = call.args && call.args.files;
210
+ if (typeof files === 'string') { try { files = JSON.parse(files); } catch { files = null; } }
211
+ if (Array.isArray(files) && (/bulk/i.test(rawTool) || /file/i.test(rawTool) || norm === 'write_file')) {
212
+ let wf;
213
+ try { wf = loadTool('write_file'); } catch { wf = null; }
214
+ for (const f of files) {
215
+ if (!wf || !f || !f.path) { records.push({ tool: 'write_file', status: 'error', error: 'gecersiz dosya girisi' }); feedbacks.push('write_file HATA: gecersiz giris'); continue; }
216
+ try {
217
+ const res = await wf.execute({ path: expandHome(f.path), content: f.content != null ? String(f.content) : '' });
218
+ const ok = res && res.success !== false;
219
+ records.push({ tool: 'write_file', status: ok ? 'done' : 'error', args: { path: f.path }, result: res, error: ok ? undefined : (res && res.error) });
220
+ feedbacks.push(ok ? `write_file OK: ${res.path || f.path} (${res.size != null ? res.size : '?'} bytes)` : `write_file HATA: ${res && res.error}`);
221
+ } catch (e) {
222
+ records.push({ tool: 'write_file', status: 'error', args: { path: f.path }, error: e.message });
223
+ feedbacks.push(`write_file HATA: ${e.message}`);
224
+ }
225
+ }
226
+ return { records, feedback: feedbacks.join('\n') };
227
+ }
228
+
229
+ if (!allowed.has(norm)) {
230
+ records.push({ tool: rawTool, status: 'error', error: 'Bu modda kullanilamayan arac: ' + rawTool });
231
+ return { records, feedback: `${rawTool}: bu arac bu modda kullanilamaz (izin verilenler: ${[...allowed].join(', ')})` };
232
+ }
233
+
234
+ const args = { ...(call.args || {}) };
235
+ for (const k of ['path', 'name', 'command', 'old_string', 'new_string']) {
236
+ if (typeof args[k] === 'string') args[k] = args[k].trim();
237
+ }
238
+ if (args.path) args.path = expandHome(args.path);
239
+
240
+ // Ajan modu guvenlik guard'i: yikici/tehlikeli kabuk komutlarini calistirmadan engelle
241
+ if (norm === 'bash') {
242
+ const cmd = (args.command || args.cmd || '').toString();
243
+ if (!cmd.trim()) {
244
+ records.push({ tool: 'bash', status: 'error', error: 'Bos komut' });
245
+ return { records, feedback: 'bash HATA: bos komut' };
246
+ }
247
+ const isDangerous = opts.isDangerous || defaultIsDangerous;
248
+ if (isDangerous(cmd)) {
249
+ records.push({ tool: 'bash', status: 'error', args: { command: cmd }, error: 'Yikici/tehlikeli komut ajan modunda engellendi' });
250
+ return { records, feedback: `bash: "${cmd.slice(0, 80)}" yikici/tehlikeli goruldugu icin ajan tarafindan CALISTIRILMADI. Gerekirse kullanici komutu kendisi calistirabilir.` };
251
+ }
252
+ }
253
+
254
+ let mod;
255
+ try { mod = loadTool(norm); } catch {
256
+ records.push({ tool: rawTool, status: 'error', error: 'Bilinmeyen arac: ' + rawTool });
257
+ return { records, feedback: `${rawTool} HATA: bilinmeyen arac` };
258
+ }
259
+ const fn = mod.execute || (mod.default && mod.default.execute);
260
+ if (typeof fn !== 'function') {
261
+ records.push({ tool: norm, status: 'error', error: 'execute yok' });
262
+ return { records, feedback: `${norm} HATA: execute fonksiyonu yok` };
263
+ }
264
+ try {
265
+ const res = await fn(args);
266
+ const ok = typeof res === 'string' ? true : (res && res.success !== false);
267
+ const status = ok ? 'done' : 'error';
268
+ const feedback = buildFeedback(norm, res);
269
+ records.push({ tool: norm, status, args: sanitizeArgs(args), result: res });
270
+ return { records, feedback };
271
+ } catch (e) {
272
+ records.push({ tool: norm, status: 'error', args: sanitizeArgs(args), error: e.message });
273
+ return { records, feedback: `${norm} HATA: ${e.message}` };
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Ana agentic dongu.
279
+ * callModel(messages) => Promise<{ content, toolCalls }>
280
+ * Donus: { records, reply, iterations }
281
+ */
282
+ async function runAgentic({ callModel, systemPrompt, historyMessages, task, toolsDir, loadTool, allowed, maxIterations = 15 }) {
283
+ const messages = [{ role: 'system', content: systemPrompt }];
284
+ for (const mm of historyMessages || []) messages.push({ role: mm.role, content: mm.content || '' });
285
+ messages.push({ role: 'user', content: task });
286
+
287
+ const allowedSet = allowed ? new Set(allowed) : new Set(DEFAULT_ALLOWED);
288
+ const allRecords = [];
289
+ let finalReply = '';
290
+ let iterations = 0;
291
+
292
+ for (let i = 0; i < maxIterations; i++) {
293
+ iterations++;
294
+ const { content, toolCalls } = await callModel(messages);
295
+ const calls = parseAgenticCalls(content, toolCalls);
296
+
297
+ if (calls.length === 0) {
298
+ finalReply = stripProtocolTokens(content);
299
+ break;
300
+ }
301
+
302
+ messages.push({ role: 'assistant', content: content || '' });
303
+ const feedbacks = [];
304
+ for (const call of calls) {
305
+ const { records, feedback } = await executeCall(call, { toolsDir, loadTool, allowed: allowedSet });
306
+ allRecords.push(...records);
307
+ feedbacks.push(feedback);
308
+ }
309
+ messages.push({
310
+ role: 'user',
311
+ content: '<tool_results>\n' + feedbacks.join('\n') + '\n</tool_results>\nGorev tamamlandiysa ARAC CAGIRMADAN tek cumlelik ozet yaz. Devam gerekiyorsa sonraki araci cagir.',
312
+ });
313
+
314
+ if (i === maxIterations - 1) {
315
+ finalReply = stripProtocolTokens(content) || 'Islem maksimum adima ulasti.';
316
+ }
317
+ }
318
+
319
+ return { records: allRecords, reply: finalReply, iterations };
320
+ }
321
+
322
+ module.exports = { parseAgenticCalls, stripProtocolTokens, executeCall, runAgentic, expandHome, makeStreamFilter, makeSanitizeStream, TOOL_ALIASES, DEFAULT_ALLOWED };
@@ -24,23 +24,58 @@ function allToolNames() {
24
24
 
25
25
  function loadUserMemory(username) {
26
26
  try {
27
- const file = path.join(os.homedir(), '.natureco', 'memory', `${(username || 'default').toLowerCase()}.json`);
28
- if (fs.existsSync(file)) {
29
- const mem = JSON.parse(fs.readFileSync(file, 'utf8'));
30
- const facts = (mem.facts || []).map(f => f.value || f).filter(Boolean);
31
- let name = mem.name || '';
32
- // Extract name from facts if not saved as memory.name
33
- if (!name) {
34
- for (const f of facts) {
35
- const match = f.toLowerCase().match(/(?:kullanici\s*adi?|kullanıcı\s*adı?|isim|name)\s*:?\s*(.+)/);
36
- if (match && match[1].trim().length > 2) { name = match[1].trim(); break; }
37
- }
27
+ const dir = path.join(os.homedir(), '.natureco', 'memory');
28
+ const uname = (username || 'default').toLowerCase();
29
+ // Kullanici-ozel dosya (`<user>.json`) + legacy `default.json`'i birlestir.
30
+ // Eski kurulumlarda hafiza default.json'a yazilmis olabilir; okuyucu sadece
31
+ // <user>.json'a bakinca eski kayitlar (ve bot personasi) hic yuklenmiyordu.
32
+ // default.json'i yalnizca ismi aktif kullaniciyla eslesiyorsa (ya da isimsizse)
33
+ // katariz — coklu kullanicida baska birinin hafizasini sizdirmamak icin.
34
+ const files = [];
35
+ const userFile = path.join(dir, `${uname}.json`);
36
+ if (fs.existsSync(userFile)) files.push(userFile);
37
+ if (uname !== 'default') {
38
+ const defFile = path.join(dir, 'default.json');
39
+ if (fs.existsSync(defFile)) {
40
+ try {
41
+ const dm = JSON.parse(fs.readFileSync(defFile, 'utf8'));
42
+ const dn = (dm.name || '').toLowerCase();
43
+ if (!dn || dn === uname) files.push(defFile);
44
+ } catch {}
38
45
  }
39
- const parts = [];
40
- if (name) parts.push(`Kullanici adi: ${name}`);
41
- if (facts.length > 0) parts.push(`Bilinenler: ${facts.slice(0, 10).join('; ')}`);
42
- return parts.join('\n');
43
46
  }
47
+ if (files.length === 0) return '';
48
+
49
+ const seen = new Set();
50
+ const facts = [];
51
+ let name = '', botName = '';
52
+ for (const file of files) {
53
+ let mem;
54
+ try { mem = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { continue; }
55
+ if (!name && mem.name) name = mem.name;
56
+ // Jenerik "Asistan" placeholder yerine gercek persona adini tercih et
57
+ if ((!botName || /^asistan$/i.test(botName)) && mem.botName && !/^asistan$/i.test(mem.botName)) botName = mem.botName;
58
+ for (const f of (mem.facts || [])) {
59
+ const v = (f && (f.value != null ? f.value : f));
60
+ if (!v || typeof v !== 'string') continue;
61
+ const key = v.trim().toLowerCase();
62
+ if (!key || seen.has(key)) continue;
63
+ seen.add(key);
64
+ facts.push(v.trim());
65
+ }
66
+ }
67
+ // isim memory.name'de yoksa fact'lerden cikar
68
+ if (!name) {
69
+ for (const f of facts) {
70
+ const match = f.toLowerCase().match(/(?:kullanici\s*adi?|kullanıcı\s*adı?|isim|name)\s*:?\s*(.+)/);
71
+ if (match && match[1].trim().length > 2) { name = match[1].trim(); break; }
72
+ }
73
+ }
74
+ const parts = [];
75
+ if (name) parts.push(`Kullanici adi: ${name}`);
76
+ if (botName) parts.push(`Bot adi: ${botName}`);
77
+ if (facts.length > 0) parts.push(`Bilinenler: ${facts.slice(0, 15).join('; ')}`);
78
+ return parts.join('\n');
44
79
  } catch {}
45
80
  return '';
46
81
  }
@@ -82,6 +117,58 @@ function apiCall(providerUrl, apiKey, body) {
82
117
  });
83
118
  }
84
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
+
85
172
  async function workflow(params) {
86
173
  const { action, task, steps, name, workflowId, regenerateStep, conversationHistory } = params;
87
174
  const cfg = loadConfig();
@@ -126,89 +213,90 @@ async function workflow(params) {
126
213
  if (action === 'run') {
127
214
  if (!task) return { success: false, error: 'task gerekli' };
128
215
 
129
- // Non-tool-calling modellerde once simple/complex kontrolu yap
216
+ // Non-tool-calling / agentic-text modeller (MiniMax M2.x): model tool call'larini
217
+ // native XML olarak (<invoke> / <minimax:tool_call>) ve skill yuklemeyi <skill> ile
218
+ // uretir. Bounded agentic dongu ile parse edip gercek araclari calistir.
130
219
  if (!supportsToolCalls()) {
220
+ const { runAgentic } = require('./agentic-runner');
131
221
  const memCtx = memoryContext();
132
- // Phase 0: Check if simple chat or complex task
133
- const simpleCheckPrompt = {
134
- role: 'system',
135
- content: 'Gorevin basit bir selamlasma/sohbet mi yoksa arac gerektiren bir islem mi oldugunu belirle. Sadece "simple" veya "complex" yaz, kesinlikle baska bir sey yazma.\n\nSimple: selamlasma, nasilsin, bugun ne yaptin, havadan sudan, genel bilgi sorusu, ben kimim, adim ne, kullanici bilgisi sorgulama, hatirlatma talebi\nComplex: dosya islemleri, kod yazma, arastirma, karsilastirma, duzenleme, otomasyon'
136
- };
137
- let isComplex = true;
138
- try {
139
- const checkBody = { model, stream: false, messages: [simpleCheckPrompt, { role: 'user', content: task }], temperature: 0, max_tokens: 20 };
140
- const checkResult = await apiCall(providerUrl, providerApiKey, checkBody);
141
- const raw = (checkResult.choices?.[0]?.message?.content || '').trim().toLowerCase().replace(/[^a-z]/g, '');
142
- isComplex = raw !== 'simple';
143
- } catch {}
144
-
145
- if (!isComplex) {
146
- // Simple chat — passthrough
147
- const sysMsg = 'Sen yardimci bir asistansin. Kisa ve oz yanit ver. Konusma gecmisi varsa onceki mesajlari dikkate al.' + (memCtx ? '\n\nKullanici bilgisi:\n' + memCtx : '') + '\n\n' + skillsIndexBlock;
148
- const chatBody = { model, stream: false, messages: chatMessages(sysMsg, task), temperature: 0.7, max_tokens: 1000 };
149
- try {
150
- const chatResult = await apiCall(providerUrl, providerApiKey, chatBody);
151
- const reply = chatResult.choices?.[0]?.message?.content || '';
152
- return { success: true, workflowId: 'passthrough', name: 'Direct Chat', status: 'completed', totalSteps: 0, completedSteps: 0, results: [], passthrough: true, reply };
153
- } catch (e) {
154
- return { success: false, error: 'Sohbet yaniti alinamadi: ' + e.message };
155
- }
222
+ const desktop = path.join(os.homedir(), 'Desktop');
223
+ const sysMsg = [
224
+ 'Sen NatureCo adli, arac kullanabilen bir yapay zeka ajanisin. Kullanicinin istegini SADECE anlatarak degil, ARACLARI cagirarak fiilen gerceklestir.',
225
+ memCtx ? '\nKullanici bilgisi:\n' + memCtx : '',
226
+ '\n\nOrtam:\n- Isletim sistemi: ' + process.platform + '\n- Kullanici home: ' + os.homedir() + '\n- Masaustu: ' + desktop + '\n- Calisma dizini: ' + process.cwd(),
227
+ '\n\nArac cagirmak icin TAM olarak su formati kullan:\n<minimax:tool_call>\n<invoke name="ARAC_ADI">\n<parameter name="PARAM">DEGER</parameter>\n</invoke>\n</minimax:tool_call>',
228
+ '\n\nKullanabilecegin araclar:',
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.',
230
+ '- read_file: dosya oku. parametre: path',
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.',
235
+ '- skill_view: gorevle ilgili bir skill yukle. parametre: name',
236
+ '\nKurallar:',
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.',
240
+ '- Birden fazla dosya gerekiyorsa her biri icin AYRI write_file cagir.',
241
+ '- Kullanici "masaustu"/"desktop" dediyse ve tam yol vermediyse dosyayi buraya yaz: ' + desktop,
242
+ '- Goreceli yol yerine TAM yol kullan.',
243
+ '- Arac sonuclari <tool_results> icinde geri gelir; gorev bitince ARAC CAGIRMADAN tek cumlelik ozet yaz.',
244
+ '- Basit sohbet/selamlasma ise arac cagirma, dogrudan kisa yanit ver.',
245
+ skillsIndexBlock ? '\n\n' + skillsIndexBlock : '',
246
+ ].filter(Boolean).join('\n');
247
+
248
+ const historyMessages = [];
249
+ if (conversationHistory && Array.isArray(conversationHistory)) {
250
+ for (const hm of conversationHistory) { if (hm._internal) continue; historyMessages.push({ role: hm.role, content: hm.content || '' }); }
156
251
  }
157
252
 
158
- // Complex task for non-tool-calling model: ask LLM for structured file ops
159
- const execSysMsg = 'Gorevi tamamlamak icin hangi dosyalarin olusturulacagini JSON olarak belirt. SADECE JSON yaz.\n\n' +
160
- 'Kullanici bilgisi:\n' + (memCtx || '') +
161
- '\n\nMasaustu yolu: ' + require('path').join(require('os').homedir(), 'Desktop') +
162
- '\n\nKullanici home: ' + require('os').homedir() +
163
- '\n\nJSON format:\n{"files": [{"path": "/tam/dosya/yolu/dosya.html", "content": "dosya icerigi buraya"}]}\n\nHer dosya icin path ve content zorunlu. path TAM yol olmali (~ veya goreceli degil). Birden fazla dosya olusturulabilir.' +
164
- '\n\n' + skillsIndexBlock;
165
- const execBody = { model, stream: false, messages: chatMessages(execSysMsg, task), temperature: 0.3, max_tokens: 8000 };
166
- try {
167
- const execResult = await apiCall(providerUrl, providerApiKey, execBody);
168
- const reply = execResult.choices?.[0]?.message?.content || '';
169
-
170
- // Try to extract JSON with file operations
171
- const jsonStart = reply.indexOf('{');
172
- const jsonEnd = reply.lastIndexOf('}');
173
- let stepResults = [];
174
- let fileWriteSuccess = false;
175
-
176
- if (jsonStart !== -1 && jsonEnd > jsonStart) {
177
- try {
178
- const plan = JSON.parse(reply.slice(jsonStart, jsonEnd + 1));
179
- if (plan.files && Array.isArray(plan.files)) {
180
- // Execute file writes locally
181
- for (const f of plan.files) {
182
- try {
183
- const wfPath = f.path ? require('path').resolve(f.path.replace(/^~/, require('os').homedir())) : '';
184
- if (!wfPath) continue;
185
- require('fs').mkdirSync(require('path').dirname(wfPath), { recursive: true });
186
- require('fs').writeFileSync(wfPath, f.content || '', 'utf-8');
187
- const stats = require('fs').statSync(wfPath);
188
- stepResults.push({ step: 1, tool: 'write_file', status: 'done', args: { path: wfPath }, result: { success: true, path: wfPath, size: stats.size } });
189
- fileWriteSuccess = true;
190
- } catch (wfErr) {
191
- stepResults.push({ step: 1, tool: 'write_file', status: 'error', error: wfErr.message });
192
- }
193
- }
194
- }
195
- } catch {}
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
+
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 || [] };
196
267
  }
268
+ const body = { model, stream: false, messages: msgs, temperature: 0.3, max_tokens: 16000 };
269
+ const r = await apiCall(providerUrl, providerApiKey, body);
270
+ const msg = r.choices?.[0]?.message || {};
271
+ return { content: msg.content || '', toolCalls: msg.tool_calls || [] };
272
+ }
197
273
 
198
- if (fileWriteSuccess) {
199
- const lines = stepResults.map(r => { const p = r.result?.path || ''; const s = r.result?.size || 0; const name = require('path').basename(p); return ` ✓ ${name} oluşturuldu (${s} bytes)`; }).join('\n');
200
- const reply = `Dosya(lar) başarıyla oluşturuldu:\n${lines}`;
201
- return {
202
- success: true, workflowId: 'file_write_' + Date.now().toString(36),
203
- name: 'File Operations', status: 'completed',
204
- totalSteps: stepResults.length, completedSteps: stepResults.length,
205
- results: stepResults,
206
- passthrough: true, reply,
207
- };
274
+ try {
275
+ const { records, reply } = await runAgentic({
276
+ callModel, systemPrompt: sysMsg, historyMessages, task,
277
+ toolsDir: __dirname, maxIterations: 15,
278
+ });
279
+ const fileWrites = records.filter(r => r.tool === 'write_file' && r.status === 'done');
280
+ let finalReply = reply || '';
281
+ if (fileWrites.length > 0) {
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');
283
+ const fileSummary = 'Dosya(lar):\n' + lines;
284
+ finalReply = (finalReply ? finalReply + '\n\n' : '') + fileSummary;
285
+ if (streamOn) process.stdout.write('\n\n' + fileSummary + '\n');
208
286
  }
209
-
210
- // If no structured file ops detected, return LLM reply as passthrough
211
- return { success: true, workflowId: 'passthrough', name: 'Direct Generation', status: 'completed', totalSteps: 0, completedSteps: 0, results: [{ step: 0, tool: 'chat', status: 'done', result: { reply } }], passthrough: true, reply };
287
+ const done = records.filter(r => r.status === 'done').length;
288
+ return {
289
+ success: true,
290
+ workflowId: (records.length ? 'agentic_' : 'passthrough_') + Date.now().toString(36),
291
+ name: records.length ? 'Agentic Run' : 'Direct Chat',
292
+ status: 'completed',
293
+ totalSteps: records.length,
294
+ completedSteps: done,
295
+ results: records.map((r, i) => ({ step: i + 1, ...r })),
296
+ passthrough: true,
297
+ streamed: streamOn,
298
+ reply: finalReply || 'Tamamlandi.',
299
+ };
212
300
  } catch (e) {
213
301
  return { success: false, error: 'Yanit alinamadi: ' + e.message };
214
302
  }