natureco-cli 5.23.0 → 5.25.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,32 @@
2
2
 
3
3
  All notable changes to NatureCo CLI will be documented in this file.
4
4
 
5
+ ## [5.25.0] - 2026-07-04 — "PROVIDER-AGNOSTİK + STREAMING ALTYAPISI"
6
+
7
+ ### ✨ İyileştirme
8
+ - **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.
9
+ - **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.
10
+
11
+ > 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.
12
+
13
+ ## [5.24.0] - 2026-07-04 — "AGENTIC SERTLEŞTİRME" (MiniMax native tool-call + komut çalıştırma)
14
+
15
+ 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ı").
16
+
17
+ ### ✨ Yeni
18
+ - **`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.
19
+ - **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ü.
20
+
21
+ ### 🐛 Düzeltme
22
+ - **MiniMax dosya yazma**: native `<invoke>`/`<skill>` parse edilip gerçek araçlara yönlendiriliyor; büyük içerikteki JSON-kaçış sorunu ortadan kalktı.
23
+ - **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'ı).
24
+ - **Bot personası**: jenerik "Asistan" placeholder'ı gerçek persona (örn. "Hinata") ile eziliyor.
25
+
26
+ ### 🔒 Güvenlik
27
+ - **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).
28
+
29
+ Doğrulama: gerçek MiniMax API ile uçtan uca (yarış oyunu + "node script yaz&çalıştır"→42); **474 test yeşil**.
30
+
5
31
  ## [5.23.0] - 2026-07-02 — "NON-TOOL-CALLING FIX" (MiniMax dosya yazma)
6
32
 
7
33
  ### 🐛 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.25.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) {
@@ -0,0 +1,281 @@
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'];
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
+ };
33
+
34
+ function expandHome(p) {
35
+ if (!p || typeof p !== 'string') return p;
36
+ if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
37
+ return p;
38
+ }
39
+
40
+ // Ajanin urettigi komutlar icin ekstra koruma. bash.js kendi politikasini uygular
41
+ // ama varsayilan 'full' mod yikici komutlari (rm -rf gibi) bile gecirir — bu insan
42
+ // icin bilincli olabilir, ama MODELIN urettigi komut icin degil. Bu yuzden agentic
43
+ // yolda yikici komutlari politikadan BAGIMSIZ olarak engelliyoruz.
44
+ function defaultIsDangerous(cmd) {
45
+ try { return require('../utils/approvals').isDangerousCommand(cmd); } catch { return false; }
46
+ }
47
+
48
+ /**
49
+ * Model metninden agentic tool cagrilarini cikar.
50
+ * Destekler: <invoke name><parameter name> (opsiyonel <minimax:tool_call> sarmali),
51
+ * <skill>ad</skill> kisayolu, ve yan yana gelen native OpenAI tool_calls.
52
+ */
53
+ function parseAgenticCalls(content, nativeToolCalls) {
54
+ const calls = [];
55
+ content = content || '';
56
+
57
+ for (const tc of nativeToolCalls || []) {
58
+ const fn = tc.function || {};
59
+ if (!fn.name) continue;
60
+ let args = {};
61
+ try { args = typeof fn.arguments === 'string' ? JSON.parse(fn.arguments || '{}') : (fn.arguments || {}); } catch {}
62
+ calls.push({ tool: fn.name, args });
63
+ }
64
+
65
+ const invokeRe = /<invoke\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/invoke>/g;
66
+ let m;
67
+ while ((m = invokeRe.exec(content)) !== null) {
68
+ const tool = m[1].trim();
69
+ const body = m[2];
70
+ const args = {};
71
+ const paramRe = /<parameter\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/parameter>/g;
72
+ let pm;
73
+ while ((pm = paramRe.exec(body)) !== null) {
74
+ args[pm[1].trim()] = pm[2];
75
+ }
76
+ calls.push({ tool, args });
77
+ }
78
+
79
+ const skillRe = /<skill>\s*([^<>\n]+?)\s*<\/skill>/g;
80
+ while ((m = skillRe.exec(content)) !== null) {
81
+ const name = m[1].trim();
82
+ if (!calls.some(c => /skill/i.test(c.tool || '') && c.args && c.args.name === name)) {
83
+ calls.push({ tool: 'skill_view', args: { name } });
84
+ }
85
+ }
86
+
87
+ return calls;
88
+ }
89
+
90
+ /** Final yanittan protokol jetonlarini temizle (kullaniciya gosterim icin). */
91
+ function stripProtocolTokens(s) {
92
+ return (s || '')
93
+ .replace(/<minimax:tool_call>[\s\S]*?<\/minimax:tool_call>/g, '')
94
+ .replace(/<invoke[\s\S]*?<\/invoke>/g, '')
95
+ .replace(/<skill>[\s\S]*?<\/skill>/g, '')
96
+ .replace(/<available_skills>[\s\S]*?<\/available_skills>/g, '')
97
+ .replace(/<\/?(minimax:tool_call|available_skills|available_skins)>/g, '')
98
+ .trim();
99
+ }
100
+
101
+ /**
102
+ * Streaming ekran filtresi: model canli akarken duz metni goster, ama tool-call/skill
103
+ * protokol bloklarini (<minimax:tool_call>, <invoke>, <skill>, <available_skills>)
104
+ * kullaniciya HAM gosterme. Bir protokol jetonu gorunce o tur icin gostermeyi keser
105
+ * (icerik yine tam biriktirilir; parse sonra yapilir). Kismi tag chunk sinirinda
106
+ * bolunebilir — sonda olasi kismi-tag kuyrugunu tutar.
107
+ *
108
+ * const f = makeStreamFilter(t => process.stdout.write(t), () => process.stdout.write(' 🔧'));
109
+ * f.push(deltaChunk); ... ; f.end();
110
+ */
111
+ function makeStreamFilter(onText, onTool) {
112
+ const MARKERS = ['<minimax:tool_call', '<invoke', '<skill>', '<available_skills'];
113
+ let buf = '';
114
+ let suppressed = false;
115
+ return {
116
+ push(chunk) {
117
+ if (suppressed) return;
118
+ buf += (chunk || '');
119
+ let idx = -1;
120
+ for (const mk of MARKERS) { const i = buf.indexOf(mk); if (i !== -1 && (idx === -1 || i < idx)) idx = i; }
121
+ if (idx !== -1) {
122
+ if (idx > 0 && onText) onText(buf.slice(0, idx));
123
+ buf = '';
124
+ suppressed = true;
125
+ if (onTool) onTool();
126
+ return;
127
+ }
128
+ const lastLt = buf.lastIndexOf('<');
129
+ if (lastLt === -1) { if (buf && onText) onText(buf); buf = ''; return; }
130
+ if (lastLt > 0 && onText) onText(buf.slice(0, lastLt));
131
+ buf = buf.slice(lastLt);
132
+ // buf artik '<' ile basliyor; bir marker'a buyuyebilecekse tut, degilse yaz (orn. literal <div>)
133
+ if (!MARKERS.some(mk => mk.startsWith(buf))) { if (onText) onText(buf); buf = ''; }
134
+ },
135
+ end() { if (!suppressed && buf && onText) onText(buf); buf = ''; return suppressed; },
136
+ };
137
+ }
138
+
139
+ function sanitizeArgs(args) {
140
+ const a = { ...args };
141
+ if (typeof a.content === 'string' && a.content.length > 160) a.content = `[${a.content.length} chars]`;
142
+ if (typeof a.new_string === 'string' && a.new_string.length > 160) a.new_string = `[${a.new_string.length} chars]`;
143
+ return a;
144
+ }
145
+
146
+ /**
147
+ * Tek bir parse edilmis cagriyi calistir.
148
+ * Donus: { records: [...], feedback: '...' }
149
+ */
150
+ async function executeCall(call, opts = {}) {
151
+ const toolsDir = opts.toolsDir || __dirname;
152
+ const loadTool = opts.loadTool || ((n) => require(path.join(toolsDir, n + '.js')));
153
+ const allowed = opts.allowed || new Set(DEFAULT_ALLOWED);
154
+ const rawTool = (call.tool || '').trim();
155
+ const norm = TOOL_ALIASES[rawTool.toLowerCase()] || rawTool;
156
+ const records = [];
157
+ const feedbacks = [];
158
+
159
+ // "files" JSON dizisi (bulk-file-operations veya files parametresi) → coklu write_file
160
+ let files = call.args && call.args.files;
161
+ if (typeof files === 'string') { try { files = JSON.parse(files); } catch { files = null; } }
162
+ if (Array.isArray(files) && (/bulk/i.test(rawTool) || /file/i.test(rawTool) || norm === 'write_file')) {
163
+ let wf;
164
+ try { wf = loadTool('write_file'); } catch { wf = null; }
165
+ for (const f of files) {
166
+ if (!wf || !f || !f.path) { records.push({ tool: 'write_file', status: 'error', error: 'gecersiz dosya girisi' }); feedbacks.push('write_file HATA: gecersiz giris'); continue; }
167
+ try {
168
+ const res = await wf.execute({ path: expandHome(f.path), content: f.content != null ? String(f.content) : '' });
169
+ const ok = res && res.success !== false;
170
+ records.push({ tool: 'write_file', status: ok ? 'done' : 'error', args: { path: f.path }, result: res, error: ok ? undefined : (res && res.error) });
171
+ feedbacks.push(ok ? `write_file OK: ${res.path || f.path} (${res.size != null ? res.size : '?'} bytes)` : `write_file HATA: ${res && res.error}`);
172
+ } catch (e) {
173
+ records.push({ tool: 'write_file', status: 'error', args: { path: f.path }, error: e.message });
174
+ feedbacks.push(`write_file HATA: ${e.message}`);
175
+ }
176
+ }
177
+ return { records, feedback: feedbacks.join('\n') };
178
+ }
179
+
180
+ if (!allowed.has(norm)) {
181
+ records.push({ tool: rawTool, status: 'error', error: 'Bu modda kullanilamayan arac: ' + rawTool });
182
+ return { records, feedback: `${rawTool}: bu arac bu modda kullanilamaz (izin verilenler: ${[...allowed].join(', ')})` };
183
+ }
184
+
185
+ const args = { ...(call.args || {}) };
186
+ for (const k of ['path', 'name', 'command', 'old_string', 'new_string']) {
187
+ if (typeof args[k] === 'string') args[k] = args[k].trim();
188
+ }
189
+ if (args.path) args.path = expandHome(args.path);
190
+
191
+ // Ajan modu guvenlik guard'i: yikici/tehlikeli kabuk komutlarini calistirmadan engelle
192
+ if (norm === 'bash') {
193
+ const cmd = (args.command || args.cmd || '').toString();
194
+ if (!cmd.trim()) {
195
+ records.push({ tool: 'bash', status: 'error', error: 'Bos komut' });
196
+ return { records, feedback: 'bash HATA: bos komut' };
197
+ }
198
+ const isDangerous = opts.isDangerous || defaultIsDangerous;
199
+ if (isDangerous(cmd)) {
200
+ records.push({ tool: 'bash', status: 'error', args: { command: cmd }, error: 'Yikici/tehlikeli komut ajan modunda engellendi' });
201
+ return { records, feedback: `bash: "${cmd.slice(0, 80)}" yikici/tehlikeli goruldugu icin ajan tarafindan CALISTIRILMADI. Gerekirse kullanici komutu kendisi calistirabilir.` };
202
+ }
203
+ }
204
+
205
+ let mod;
206
+ try { mod = loadTool(norm); } catch {
207
+ records.push({ tool: rawTool, status: 'error', error: 'Bilinmeyen arac: ' + rawTool });
208
+ return { records, feedback: `${rawTool} HATA: bilinmeyen arac` };
209
+ }
210
+ const fn = mod.execute || (mod.default && mod.default.execute);
211
+ if (typeof fn !== 'function') {
212
+ records.push({ tool: norm, status: 'error', error: 'execute yok' });
213
+ return { records, feedback: `${norm} HATA: execute fonksiyonu yok` };
214
+ }
215
+ try {
216
+ 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
+ }
228
+ records.push({ tool: norm, status, args: sanitizeArgs(args), result: res });
229
+ return { records, feedback };
230
+ } catch (e) {
231
+ records.push({ tool: norm, status: 'error', args: sanitizeArgs(args), error: e.message });
232
+ return { records, feedback: `${norm} HATA: ${e.message}` };
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Ana agentic dongu.
238
+ * callModel(messages) => Promise<{ content, toolCalls }>
239
+ * Donus: { records, reply, iterations }
240
+ */
241
+ async function runAgentic({ callModel, systemPrompt, historyMessages, task, toolsDir, loadTool, allowed, maxIterations = 15 }) {
242
+ const messages = [{ role: 'system', content: systemPrompt }];
243
+ for (const mm of historyMessages || []) messages.push({ role: mm.role, content: mm.content || '' });
244
+ messages.push({ role: 'user', content: task });
245
+
246
+ const allowedSet = allowed ? new Set(allowed) : new Set(DEFAULT_ALLOWED);
247
+ const allRecords = [];
248
+ let finalReply = '';
249
+ let iterations = 0;
250
+
251
+ for (let i = 0; i < maxIterations; i++) {
252
+ iterations++;
253
+ const { content, toolCalls } = await callModel(messages);
254
+ const calls = parseAgenticCalls(content, toolCalls);
255
+
256
+ if (calls.length === 0) {
257
+ finalReply = stripProtocolTokens(content);
258
+ break;
259
+ }
260
+
261
+ messages.push({ role: 'assistant', content: content || '' });
262
+ const feedbacks = [];
263
+ for (const call of calls) {
264
+ const { records, feedback } = await executeCall(call, { toolsDir, loadTool, allowed: allowedSet });
265
+ allRecords.push(...records);
266
+ feedbacks.push(feedback);
267
+ }
268
+ messages.push({
269
+ role: 'user',
270
+ content: '<tool_results>\n' + feedbacks.join('\n') + '\n</tool_results>\nGorev tamamlandiysa ARAC CAGIRMADAN tek cumlelik ozet yaz. Devam gerekiyorsa sonraki araci cagir.',
271
+ });
272
+
273
+ if (i === maxIterations - 1) {
274
+ finalReply = stripProtocolTokens(content) || 'Islem maksimum adima ulasti.';
275
+ }
276
+ }
277
+
278
+ return { records: allRecords, reply: finalReply, iterations };
279
+ }
280
+
281
+ module.exports = { parseAgenticCalls, stripProtocolTokens, executeCall, runAgentic, expandHome, makeStreamFilter, 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 {}
45
+ }
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());
38
65
  }
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
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
  }
@@ -126,89 +161,69 @@ async function workflow(params) {
126
161
  if (action === 'run') {
127
162
  if (!task) return { success: false, error: 'task gerekli' };
128
163
 
129
- // Non-tool-calling modellerde once simple/complex kontrolu yap
164
+ // Non-tool-calling / agentic-text modeller (MiniMax M2.x): model tool call'larini
165
+ // native XML olarak (<invoke> / <minimax:tool_call>) ve skill yuklemeyi <skill> ile
166
+ // uretir. Bounded agentic dongu ile parse edip gercek araclari calistir.
130
167
  if (!supportsToolCalls()) {
168
+ const { runAgentic } = require('./agentic-runner');
131
169
  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
- }
170
+ const desktop = path.join(os.homedir(), 'Desktop');
171
+ const sysMsg = [
172
+ 'Sen NatureCo adli, arac kullanabilen bir yapay zeka ajanisin. Kullanicinin istegini SADECE anlatarak degil, ARACLARI cagirarak fiilen gerceklestir.',
173
+ memCtx ? '\nKullanici bilgisi:\n' + memCtx : '',
174
+ '\n\nOrtam:\n- Isletim sistemi: ' + process.platform + '\n- Kullanici home: ' + os.homedir() + '\n- Masaustu: ' + desktop + '\n- Calisma dizini: ' + process.cwd(),
175
+ '\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>',
176
+ '\n\nKullanabilecegin araclar:',
177
+ '- 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
+ '- 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.',
181
+ '- skill_view: gorevle ilgili bir skill yukle. parametre: name',
182
+ '\nKurallar:',
183
+ '- Kod yazdiktan sonra gerektiginde bash ile calistir/test et (orn. "node dosya.js", "npm test"); hata cikarsa duzelt.',
184
+ '- Birden fazla dosya gerekiyorsa her biri icin AYRI write_file cagir.',
185
+ '- Kullanici "masaustu"/"desktop" dediyse ve tam yol vermediyse dosyayi buraya yaz: ' + desktop,
186
+ '- Goreceli yol yerine TAM yol kullan.',
187
+ '- Arac sonuclari <tool_results> icinde geri gelir; gorev bitince ARAC CAGIRMADAN tek cumlelik ozet yaz.',
188
+ '- Basit sohbet/selamlasma ise arac cagirma, dogrudan kisa yanit ver.',
189
+ skillsIndexBlock ? '\n\n' + skillsIndexBlock : '',
190
+ ].filter(Boolean).join('\n');
191
+
192
+ const historyMessages = [];
193
+ if (conversationHistory && Array.isArray(conversationHistory)) {
194
+ for (const hm of conversationHistory) { if (hm._internal) continue; historyMessages.push({ role: hm.role, content: hm.content || '' }); }
156
195
  }
157
196
 
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 {}
196
- }
197
+ async function callModel(msgs) {
198
+ const body = { model, stream: false, messages: msgs, temperature: 0.3, max_tokens: 16000 };
199
+ const r = await apiCall(providerUrl, providerApiKey, body);
200
+ const msg = r.choices?.[0]?.message || {};
201
+ return { content: msg.content || '', toolCalls: msg.tool_calls || [] };
202
+ }
197
203
 
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
- };
204
+ try {
205
+ const { records, reply } = await runAgentic({
206
+ callModel, systemPrompt: sysMsg, historyMessages, task,
207
+ toolsDir: __dirname, maxIterations: 15,
208
+ });
209
+ const fileWrites = records.filter(r => r.tool === 'write_file' && r.status === 'done');
210
+ let finalReply = reply || '';
211
+ if (fileWrites.length > 0) {
212
+ 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;
208
214
  }
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 };
215
+ const done = records.filter(r => r.status === 'done').length;
216
+ return {
217
+ success: true,
218
+ workflowId: (records.length ? 'agentic_' : 'passthrough_') + Date.now().toString(36),
219
+ name: records.length ? 'Agentic Run' : 'Direct Chat',
220
+ status: 'completed',
221
+ totalSteps: records.length,
222
+ completedSteps: done,
223
+ results: records.map((r, i) => ({ step: i + 1, ...r })),
224
+ passthrough: true,
225
+ reply: finalReply || 'Tamamlandi.',
226
+ };
212
227
  } catch (e) {
213
228
  return { success: false, error: 'Yanit alinamadi: ' + e.message };
214
229
  }