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 +26 -0
- package/package.json +1 -1
- package/src/commands/repl.js +30 -5
- package/src/tools/agentic-runner.js +281 -0
- package/src/tools/workflow.js +107 -92
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.
|
|
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"
|
package/src/commands/repl.js
CHANGED
|
@@ -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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 };
|
package/src/tools/workflow.js
CHANGED
|
@@ -24,23 +24,58 @@ function allToolNames() {
|
|
|
24
24
|
|
|
25
25
|
function loadUserMemory(username) {
|
|
26
26
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
}
|