natureco-cli 5.20.2 → 5.20.4

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.
@@ -1,1580 +1,1604 @@
1
- /**
2
- * natureco repl — Persistent Interactive REPL
3
- *
4
- * Özellikler:
5
- * ✅ Cross-session hafıza (memory dosyası)
6
- * ✅ Otomatik fact extraction (LLM konuşmadan öğrenir)
7
- * ✅ Session resume (--resume ile kaldığın yerden devam)
8
- * ✅ Tüm CLI komutları REPL içinden (/doctor, /cost, /audit, /team...)
9
- * ✅ Slash komutlar (/help, /memory, /model, /system, /exit)
10
- * ✅ Ctrl+C temiz çıkış, konuşma otomatik kayıt
11
- * ✅ Persistent session list (~/.natureco/sessions.json)
12
- * ✅ Token tracking
13
- *
14
- * v4.6.0 — Persistent Memory Edition
15
- */
16
-
17
- const readline = require('readline');
18
- const fs = require('fs');
19
- const path = require('path');
20
- const os = require('os');
21
- const https = require('https');
22
- const { spawn } = require('child_process');
23
- const chalk = require('chalk');
24
- const tui = require('../utils/tui');
25
- const { loadToolDefinitions, toOpenAIFormat, executeTool } = require('../utils/tools');
26
- const { accumulateToolCallDeltas, finalizeToolCalls } = require('../utils/streaming-tools');
27
- const { createPasteSafeInput, createOutputFilter, enableBracketedPaste, disableBracketedPaste, restoreNewlines, clearPasteContext } = require('../utils/paste-safe-input');
28
- const { getMemoryStore } = require('../utils/memory-store');
29
- const { buildSkillIndex } = require('../utils/skill-index');
30
- const { buildTiers, assemble, discoverProjectRules } = require('../utils/system-prompt');
31
- const { ToolGuardrails } = require('../utils/tool-guardrails');
32
-
33
- // v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
34
- const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
35
- function fixModelNameLeak(text, botName) {
36
- if (!text) return text;
37
- let fixed = text;
38
- for (const modelName of MODEL_NAMES_TO_HIDE) {
39
- const regex = new RegExp(modelName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
40
- fixed = fixed.replace(regex, botName || 'asistan');
41
- }
42
- fixed = fixed.replace(/Ben\s+MiniMax[^.,!?\n]*/gi, 'Ben ' + (botName || 'asistan'));
43
- fixed = fixed.replace(/I'm\s+MiniMax[^.,!?\n]*/gi, "I'm " + (botName || 'asistan'));
44
- fixed = fixed.replace(/I am\s+Claude[^.,!?\n]*/gi, 'I am ' + (botName || 'asistan'));
45
- return fixed;
46
- }
47
- global.fixModelNameLeak = fixModelNameLeak;
48
-
49
- // v4.8.0: Tool definitions — başlangıçta bir kez yükle (performans)
50
- let _toolDefs = null;
51
- function getToolDefs() {
52
- if (!_toolDefs) {
53
- try {
54
- _toolDefs = loadToolDefinitions();
55
- } catch (e) {
56
- _toolDefs = [];
57
- }
58
- }
59
- return _toolDefs;
60
- }
61
-
62
- // ── System prompt tier cache (Hermes-style prefix cache warmth) ────────────
63
- // stable+context built once at session start, volatile rebuilt per turn.
64
- let _cachedStable = '';
65
- let _cachedContext = '';
66
- let _cachedTierOpts = null; // opts snapshot for volatile-only rebuilds
67
-
68
- function rebuildSystemPrompt(opts) {
69
- // If stable/context opts changed, rebuild them too
70
- const needsFullRebuild = !_cachedTierOpts ||
71
- _cachedTierOpts.botName !== opts.botName ||
72
- _cachedTierOpts.soulSummary !== opts.soulSummary ||
73
- _cachedTierOpts.skillsIndexBlock !== opts.skillsIndexBlock ||
74
- _cachedTierOpts.crossSessionContext !== opts.crossSessionContext ||
75
- _cachedTierOpts.projectRules !== opts.projectRules;
76
-
77
- if (needsFullRebuild || !_cachedStable) {
78
- const tiers = buildTiers(opts);
79
- _cachedStable = tiers.stable;
80
- _cachedContext = tiers.context;
81
- _cachedTierOpts = {
82
- botName: opts.botName,
83
- soulSummary: opts.soulSummary,
84
- skillsIndexBlock: opts.skillsIndexBlock,
85
- crossSessionContext: opts.crossSessionContext,
86
- projectRules: opts.projectRules,
87
- };
88
- }
89
- // Volatile always rebuilt fresh
90
- const volatileOnly = buildTiers({
91
- ...opts,
92
- // Pass empty strings for stable/context fields so buildTiers only builds volatile
93
- botName: '',
94
- soulSummary: '',
95
- skillsIndexBlock: '',
96
- crossSessionContext: '',
97
- projectRules: '',
98
- });
99
- return assemble(_cachedStable, _cachedContext, volatileOnly.volatile);
100
- }
101
-
102
- // ── Tool Guardrails instance (Hermes-style) ─────────────────────────────
103
- const guardrails = new ToolGuardrails();
104
-
105
- // CLI komutları (REPL içinden çalıştırılabilir)
106
- const CLI_COMMANDS = {
107
- '/doctor': { desc: 'Sistem sağlığı kontrolü', run: ['doctor'] },
108
- '/cost': { desc: 'Maliyet takibi (today|week|month|budget)', run: ['cost', 'today'] },
109
- '/audit': { desc: 'Audit log (today|stats|files)', run: ['audit', 'stats'] },
110
- '/team': { desc: 'Multi-agent (list|status)', run: ['team', 'list'] },
111
- '/xp': { desc: 'XP/Level durumu', run: ['xp'] },
112
- '/skills': { desc: 'Yüklü skill listesi', run: ['skills', 'list'] },
113
- '/status': { desc: 'Sistem durumu', run: ['status'] },
114
- '/mcp': { desc: 'MCP sunucuları', run: ['mcp', 'list'] },
115
- '/channels': { desc: 'Bağlı kanallar', run: ['channels'] },
116
- '/crons': { desc: 'Cron görevleri', run: ['cron', 'list'] },
117
- '/bots': { desc: 'Bot listesi', run: ['bots'] },
118
- '/models': { desc: 'Modeller', run: ['models', 'list'] },
119
- '/memory-ls': { desc: 'Memory dosyaları', run: ['memory', 'list'] },
120
- '/seo': { desc: 'SEO denetimi (URL gerek)', needsArg: true, run: ['seo', 'audit'] },
121
- '/naturehub': { desc: 'Bota mesaj gönder (text gerek)', needsArg: true, run: ['naturehub', 'post'] },
122
- '/dashboard': { desc: 'Web dashboard başlat (port 7421)', run: ['dashboard', 'start'] },
123
- };
124
-
125
- const MEMORY_DIR = path.join(os.homedir(), '.natureco', 'memory');
126
- const SESSION_DIR = path.join(os.homedir(), '.natureco', 'sessions');
127
- const SESSIONS_INDEX = path.join(os.homedir(), '.natureco', 'sessions.json');
128
- const REPL_STATE = path.join(os.homedir(), '.natureco', 'repl-state.json');
129
-
130
- function ensureDir(dir) {
131
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
132
- }
133
-
134
- function getConfig() {
135
- try {
136
- return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.natureco', 'config.json'), 'utf8'));
137
- } catch { return {}; }
138
- }
139
-
140
- function isMiniMax(url) {
141
- return url && (url.includes('minimax.io') || url.includes('minimaxi.com') || url.includes('minimax.cn'));
142
- }
143
- function isGemini(url) {
144
- return url && (url.includes('generativelanguage.googleapis.com') || url.includes('gemini'));
145
- }
146
-
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: [] };
153
- }
154
-
155
- function saveMemory(username, memory) {
156
- ensureDir(MEMORY_DIR);
157
- const file = path.join(MEMORY_DIR, `${(username || 'default').toLowerCase()}.json`);
158
- memory.lastUpdated = new Date().toISOString();
159
- fs.writeFileSync(file, JSON.stringify(memory, null, 2));
160
- return file;
161
- }
162
-
163
- function loadSessionsIndex() {
164
- try {
165
- if (fs.existsSync(SESSIONS_INDEX)) return JSON.parse(fs.readFileSync(SESSIONS_INDEX, 'utf8'));
166
- } catch {}
167
- return { sessions: [] };
168
- }
169
-
170
- function saveSessionsIndex(index) {
171
- fs.writeFileSync(SESSIONS_INDEX, JSON.stringify(index, null, 2));
172
- }
173
-
174
- function saveSession(messages, meta) {
175
- ensureDir(SESSION_DIR);
176
- const id = `sess-${Date.now().toString(36)}`;
177
- const file = path.join(SESSION_DIR, `${id}.json`);
178
- const data = {
179
- id,
180
- createdAt: new Date().toISOString(),
181
- updatedAt: new Date().toISOString(),
182
- messageCount: messages.length,
183
- ...meta,
184
- messages: messages.filter(m => !m._internal),
185
- };
186
- fs.writeFileSync(file, JSON.stringify(data, null, 2));
187
- // Index güncelle
188
- const idx = loadSessionsIndex();
189
- idx.sessions.unshift({
190
- id, file, createdAt: data.createdAt, messageCount: messages.length,
191
- firstUserMessage: messages.find(m => m.role === 'user')?.content?.slice(0, 60) || '(boş)',
192
- });
193
- // Son 50 session tut
194
- idx.sessions = idx.sessions.slice(0, 50);
195
- saveSessionsIndex(idx);
196
- return id;
197
- }
198
-
199
- function loadSession(id) {
200
- // ID veya index
201
- const idx = loadSessionsIndex();
202
- let meta;
203
- if (id === 'last' || id === 'latest') {
204
- meta = idx.sessions[0];
205
- } else {
206
- meta = idx.sessions.find(s => s.id === id || s.id.endsWith(id));
207
- }
208
- if (!meta) return null;
209
- try {
210
- return JSON.parse(fs.readFileSync(meta.file, 'utf8'));
211
- } catch { return null; }
212
- }
213
-
214
- function extractFacts(messages, currentFacts) {
215
- // Basit fact extraction: Türkçe/İngilizce pattern'lerle kullanıcı hakkında bilgi çıkar
216
- // Production'da LLM ile yapılabilir, şimdilik pattern matching
217
- const newFacts = [];
218
- const userMessages = messages.filter(m => m.role === 'user' && !m._internal);
219
- const existingValues = new Set((currentFacts || []).map(f => (f.value || f).toLowerCase()));
220
-
221
- for (const msg of userMessages) {
222
- const text = msg.content || '';
223
- const lower = text.toLowerCase();
224
-
225
- // İsim/tercih pattern'leri
226
- const patterns = [
227
- { re: /(?:benim adım|adım|I'm called|my name is)\s+([A-ZÇĞİÖŞÜa-zçğıöşü]+)/i, val: m => `Adı: ${m[1]}` },
228
- { re: /(?:yaşıyorum|yaşadığım|I live in)\s+([A-ZÇĞİÖŞÜa-zçğıöşü\s]+)/i, val: m => `Yaşadığı yer: ${m[1].trim()}` },
229
- { re: /(?:seviyorum|severim|sevdiğim|like|love)\s+(?:şu|bu)?\s*([a-zA-ZçğıöşüÇĞİÖŞÜ\s]{2,30})/i, val: m => `Sevdiği şey: ${m[1].trim()}` },
230
- { re: /(?:çalışıyorum|işim|mesleğim|I work as)\s+([A-ZÇĞİÖŞÜa-zçğıöşü\s]+)/i, val: m => `Meslek: ${m[1].trim()}` },
231
- ];
232
-
233
- for (const p of patterns) {
234
- const m = text.match(p.re);
235
- if (m && m[1]) {
236
- const val = p.val(m);
237
- if (val && !existingValues.has(val.toLowerCase())) {
238
- newFacts.push({ value: val, score: 5, learnedAt: new Date().toISOString() });
239
- existingValues.add(val.toLowerCase());
240
- }
241
- }
242
- }
243
- }
244
- return newFacts;
245
- }
246
-
247
- function apiRequest(providerUrl, providerApiKey, body, stream = false, retries = 3) {
248
- return new Promise((resolve, reject) => {
249
- const isMM = isMiniMax(providerUrl);
250
- const endpoint = isMM
251
- ? `${providerUrl.replace(/\/+$/, '')}/v1/text/chatcompletion_v2`
252
- : isGemini(providerUrl)
253
- ? `${providerUrl.replace(/\/+$/, '')}/openai/chat/completions`
254
- : `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
255
- const doRequest = (attempt) => {
256
- const req = https.request(endpoint, {
257
- method: 'POST',
258
- headers: {
259
- 'Authorization': `Bearer ${providerApiKey}`,
260
- 'Content-Type': 'application/json',
261
- },
262
- timeout: 60000,
263
- }, (res) => {
264
- if (stream) { resolve(res); return; }
265
- let data = '';
266
- res.on('data', c => data += c);
267
- res.on('end', () => {
268
- if (res.statusCode >= 200 && res.statusCode < 300) {
269
- try { resolve(JSON.parse(data)); } catch (e) { reject(new Error('Parse hatası')); }
270
- } else if (res.statusCode === 429 && attempt < retries) {
271
- const delay = Math.pow(2, attempt) * 1000;
272
- setTimeout(() => doRequest(attempt + 1), delay);
273
- } else {
274
- const msg = res.statusCode === 429
275
- ? 'HTTP 429: API rate limit aşıldı. Lütfen bekleyin veya planınızı yükseltin.'
276
- : `HTTP ${res.statusCode}: ${data.slice(0, 200)}`;
277
- reject(new Error(msg));
278
- }
279
- });
280
- });
281
- req.on('error', reject);
282
- req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
283
- req.write(JSON.stringify(body));
284
- req.end();
285
- };
286
- doRequest(0);
287
- });
288
- }
289
-
290
- async function sendStreaming(providerUrl, providerApiKey, messages, model, onChunk, onToolCall) {
291
- const isMM = isMiniMax(providerUrl);
292
- const isGM = isGemini(providerUrl);
293
- const planMode = getPlanMode();
294
-
295
- // Simple stdin prompt fallback (no rl dependency)
296
- function stdinPrompt(question, cb) {
297
- process.stdout.write(question);
298
- const stdin = process.stdin;
299
- const onData = (data) => {
300
- stdin.removeListener('data', onData);
301
- stdin.pause();
302
- cb(data.toString().trim());
303
- };
304
- stdin.resume();
305
- stdin.on('data', onData);
306
- }
307
-
308
- const toolDefs = getToolDefs();
309
- // Inject plan mode virtual tools
310
- const planToolDefs = [
311
- {
312
- name: 'EnterPlanMode',
313
- description: 'Switch to plan-only mode. Research, explore, and create a detailed plan without making changes. Use ExitPlanMode when ready.',
314
- parameters: { type: 'object', properties: {}, required: [] },
315
- execute: async () => {
316
- if (planMode.enter()) return { result: 'Plan mode activated. You can now research and plan. No changes will be made. Use ExitPlanMode when your plan is ready.' };
317
- return { result: 'Already in plan mode.' };
318
- },
319
- },
320
- {
321
- name: 'ExitPlanMode',
322
- description: 'Exit plan mode and present your plan for review. The plan must include steps, files to modify, and expected changes.',
323
- parameters: {
324
- type: 'object',
325
- properties: {
326
- plan: { type: 'string', description: 'Markdown plan with ## Plan title, ### Step N sections listing files and changes' },
327
- summary: { type: 'string', description: 'One-sentence summary of the plan' },
328
- },
329
- required: ['plan'],
330
- },
331
- execute: async (args) => {
332
- const ok = planMode.exit(args.plan);
333
- if (!ok) return { error: 'Not in plan mode.' };
334
- return {
335
- result: `Plan submitted for review.\n\n${args.plan}`,
336
- _plan_summary: args.summary || '',
337
- };
338
- },
339
- },
340
- ];
341
- toolDefs.push(...planToolDefs);
342
-
343
- // Inject worktree virtual tools
344
- const wt = getWorktree();
345
- const worktreeToolDefs = [
346
- {
347
- name: 'EnterWorktree',
348
- description: 'Create an isolated worktree for experimental changes without affecting the main project. Use ExitWorktree to merge changes back.',
349
- parameters: {
350
- type: 'object',
351
- properties: {
352
- branch: { type: 'string', description: 'Optional branch name for the worktree' },
353
- },
354
- },
355
- execute: async (args) => wt.enter(args),
356
- },
357
- {
358
- name: 'ExitWorktree',
359
- description: 'Exit the current worktree and merge changes back to the main branch.',
360
- parameters: {
361
- type: 'object',
362
- properties: {
363
- merge: { type: 'boolean', description: 'Merge changes back (default: true)' },
364
- },
365
- },
366
- execute: async (args) => wt.exit(args),
367
- },
368
- ];
369
- toolDefs.push(...worktreeToolDefs);
370
-
371
- // Inject tool-level virtual tools
372
- const { getTaskManager } = require('../utils/tasks');
373
- const taskMgr = getTaskManager();
374
- const toolVirtualTools = [
375
- {
376
- name: 'CreateTask',
377
- description: 'Run a command in the background. Returns immediately with a task ID. Use GetTaskResult or ListTasks to check status.',
378
- parameters: {
379
- type: 'object',
380
- properties: {
381
- command: { type: 'string', description: 'Shell command to run' },
382
- timeout: { type: 'number', description: 'Timeout in ms (default: 300000)' },
383
- },
384
- required: ['command'],
385
- },
386
- execute: async (args) => taskMgr.create(args.command, args),
387
- },
388
- {
389
- name: 'ListTasks',
390
- description: 'List all background tasks with their status.',
391
- parameters: { type: 'object', properties: {}, required: [] },
392
- execute: async () => ({ tasks: taskMgr.list() }),
393
- },
394
- {
395
- name: 'GetTaskResult',
396
- description: 'Get the full result (stdout/stderr) of a completed or running task.',
397
- parameters: {
398
- type: 'object',
399
- properties: { taskId: { type: 'string', description: 'Task ID' } },
400
- required: ['taskId'],
401
- },
402
- execute: async (args) => {
403
- const task = taskMgr.get(args.taskId);
404
- if (!task) return { error: 'Task not found' };
405
- return { result: task.stdout, error: task.stderr, status: task.status, exitCode: task.exitCode };
406
- },
407
- },
408
- {
409
- name: 'StopTask',
410
- description: 'Stop a running background task.',
411
- parameters: {
412
- type: 'object',
413
- properties: { taskId: { type: 'string' } },
414
- required: ['taskId'],
415
- },
416
- execute: async (args) => taskMgr.stop(args.taskId),
417
- },
418
- {
419
- name: 'SearchSessions',
420
- description: 'Search past conversation sessions for information. Useful for finding previously discussed topics or decisions.',
421
- parameters: {
422
- type: 'object',
423
- properties: { query: { type: 'string', description: 'Search query' } },
424
- required: ['query'],
425
- },
426
- execute: async (args) => {
427
- const { search } = require('../utils/session-search');
428
- return { results: search(args.query, 5) };
429
- },
430
- },
431
- {
432
- name: 'RestoreFile',
433
- description: 'Restore a file from its snapshot history. Use FileHistory first to list available snapshots.',
434
- parameters: {
435
- type: 'object',
436
- properties: {
437
- filePath: { type: 'string', description: 'File path to restore' },
438
- timestamp: { type: 'number', description: 'Snapshot timestamp (from FileHistory). If omitted, restores previous version.' },
439
- },
440
- required: ['filePath'],
441
- },
442
- execute: async (args) => {
443
- const fh = require('../utils/file-history');
444
- const snaps = fh.getHistory(args.filePath);
445
- if (snaps.length === 0) return { error: 'No history for this file' };
446
- const ts = args.timestamp || snaps[0].timestamp;
447
- return fh.restore(args.filePath, ts);
448
- },
449
- },
450
- {
451
- name: 'FileHistory',
452
- description: 'List snapshot history for a file, showing timestamps and sizes of previous versions.',
453
- parameters: {
454
- type: 'object',
455
- properties: { filePath: { type: 'string' } },
456
- required: ['filePath'],
457
- },
458
- execute: async (args) => {
459
- const fh = require('../utils/file-history');
460
- return { snapshots: fh.getHistory(args.filePath) };
461
- },
462
- },
463
- {
464
- name: 'UltraReview',
465
- description: 'Run a multi-focus code review (security, style, logic, performance) on specified files or git diff.',
466
- parameters: {
467
- type: 'object',
468
- properties: {
469
- files: { type: 'array', items: { type: 'string' }, description: 'File paths to review' },
470
- diff: { type: 'string', description: 'Git diff content to review instead of files' },
471
- },
472
- },
473
- execute: async (args) => {
474
- const ur = require('../utils/ultra-review');
475
- if (args.diff) return ur.reviewDiff(args.diff);
476
- if (args.files) {
477
- const fs = require('fs');
478
- return {
479
- reviews: args.files.map(f => {
480
- const content = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
481
- return ur.reviewFile(f, content);
482
- }),
483
- };
484
- }
485
- return { error: 'Specify files or diff' };
486
- },
487
- },
488
- {
489
- name: 'ScheduleTask',
490
- description: 'Schedule a recurring task using cron expressions. E.g., "*/5 * * * *" for every 5 min.',
491
- parameters: {
492
- type: 'object',
493
- properties: {
494
- schedule: { type: 'string', description: 'Cron expression: min hour dom mon dow' },
495
- command: { type: 'string', description: 'Command to run' },
496
- description: { type: 'string', description: 'Optional description' },
497
- },
498
- required: ['schedule', 'command'],
499
- },
500
- execute: async (args) => require('../utils/cron').addJob(args),
501
- },
502
- {
503
- name: 'ListScheduledTasks',
504
- description: 'List all scheduled cron tasks.',
505
- parameters: { type: 'object', properties: {} },
506
- execute: async () => ({ jobs: require('../utils/cron').loadJobs() }),
507
- },
508
- {
509
- name: 'RemoveScheduledTask',
510
- description: 'Remove a scheduled task by ID.',
511
- parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
512
- execute: async (args) => {
513
- require('../utils/cron').removeJob(args.id);
514
- return { removed: true };
515
- },
516
- },
517
- ];
518
- toolDefs.push(...toolVirtualTools);
519
-
520
- const toolParam = toOpenAIFormat(toolDefs);
521
- guardrails.reset();
522
-
523
- let currentMessages = messages;
524
- let fullText = '';
525
- let iterations = 0;
526
- const MAX_TOOL_ITERATIONS = 50;
527
- const MAX_CONTEXT_TOKENS = 32000; // safety limit before compression
528
-
529
- // v5.7.18: Preflight compression — if context too long, compress middle messages
530
- // (like Hermes' turn_context.py preflight)
531
- function preflightCompress(msgs) {
532
- const roughTokens = msgs.reduce((s, m) => s + Math.ceil(((m.content || '') + (m.role || '')).length / 4), 0);
533
- if (roughTokens <= MAX_CONTEXT_TOKENS || msgs.length < 6) return msgs;
534
-
535
- // Keep system prompt (first message) + last N turns (tail), compress middle
536
- const sysMsg = msgs[0] && msgs[0].role === 'system' ? msgs[0] : null;
537
- const tailCount = Math.min(6, Math.floor(msgs.length / 2));
538
- const startIdx = sysMsg ? 1 : 0;
539
- const tailStart = msgs.length - tailCount;
540
-
541
- const middle = msgs.slice(startIdx, tailStart);
542
- if (middle.length < 2) return msgs;
543
-
544
- // Summarize middle section
545
- const summary = '[' + middle.length + ' onceki mesaj ozetlendi]';
546
- const compressed = sysMsg ? [sysMsg] : [];
547
- compressed.push({ role: 'system', content: 'Gecmis konusma ozeti: ' + summary, _compressed: true });
548
- compressed.push(...msgs.slice(tailStart));
549
- return compressed;
550
- }
551
-
552
- // Apply preflight on entry
553
- currentMessages = preflightCompress(currentMessages);
554
-
555
- while (iterations < 50) {
556
- let effortLevel = 'medium', effortCfg;
557
- let cfg;
558
- try {
559
- cfg = require('../utils/config').getConfig();
560
- effortLevel = getEffortLevel(cfg);
561
- effortCfg = getEffortConfig(effortLevel);
562
- } catch { effortCfg = getEffortConfig(effortLevel); }
563
- const maxIter = effortCfg ? effortCfg.maxToolIterations : 50;
564
- iterations++;
565
- // v5.7.18: Preflight compress before each iteration to prevent context bloat
566
- currentMessages = preflightCompress(currentMessages);
567
- const shouldStream = !isMM && !isGM; // MiniMax + Gemini non-stream (tool_calls reliability)
568
- // Inject plan mode prompt into system message
569
- const planModePrompt = planMode.getSystemPrompt();
570
- if (planModePrompt) {
571
- const sysIdx = currentMessages.findIndex(m => m.role === 'system');
572
- if (sysIdx >= 0) {
573
- const base = currentMessages[sysIdx].content;
574
- if (!base.endsWith(planModePrompt)) {
575
- currentMessages[sysIdx] = { ...currentMessages[sysIdx], content: base + '\n\n' + planModePrompt };
576
- }
577
- }
578
- } else {
579
- // Clean up any stale plan mode prompt from previous iterations
580
- const sysIdx = currentMessages.findIndex(m => m.role === 'system');
581
- if (sysIdx >= 0) {
582
- const base = currentMessages[sysIdx].content;
583
- const marker = '\n\nYou are in PLAN MODE.';
584
- if (base.includes(marker)) {
585
- currentMessages[sysIdx] = { ...currentMessages[sysIdx], content: base.split('\n\nYou are in PLAN MODE.')[0] };
586
- }
587
- }
588
- }
589
-
590
- const fallbackChain = getFallbackChain();
591
-
592
- const body = {
593
- model: fallbackChain.current || model,
594
- messages: currentMessages,
595
- stream: shouldStream,
596
- temperature: effortCfg.temperature,
597
- max_tokens: effortCfg.maxTokens,
598
- };
599
- // Structured output support
600
- const respFmt = getResponseFormat(cfg);
601
- if (respFmt) body.response_format = respFmt;
602
- if (toolParam) body.tools = toolParam;
603
- if (isMM || isGM) body.tool_choice = 'auto'; // MiniMax + Gemini için explicit
604
-
605
- if (!shouldStream) {
606
- // MiniMax (non-stream) tool_calls desteklemiyor varsayalım
607
- const res = await apiRequest(providerUrl, providerApiKey, body, false);
608
- const msg = res.choices?.[0]?.message || {};
609
- const content = msg.content || '';
610
- for (const char of content) {
611
- onChunk(char);
612
- await new Promise(r => setTimeout(r, 8));
613
- }
614
- fullText = content;
615
- // Non-stream tool call desteği
616
- if (msg.tool_calls && msg.tool_calls.length > 0) {
617
- const toolResults = await processToolCalls(msg.tool_calls, onToolCall, stdinPrompt);
618
- currentMessages.push(msg);
619
- currentMessages.push(...toolResults);
620
- continue; // Tekrar API çağır
621
- }
622
- break;
623
- }
624
-
625
- // OpenAI uyumlu streaming (veya MiniMax /v1/text/chatcompletion_v2)
626
- // v5.9.5: Gemini /openai/chat/completions — provider-detect.js buildChatEndpoint
627
- const endpoint = isMM
628
- ? `${providerUrl.replace(/\/+$/, '')}/v1/text/chatcompletion_v2`
629
- : isGemini(providerUrl)
630
- ? `${providerUrl.replace(/\/+$/, '')}/openai/chat/completions`
631
- : `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
632
- let result;
633
- try {
634
- result = await new Promise((resolve, reject) => {
635
- const req = https.request(endpoint, {
636
- method: 'POST',
637
- headers: { 'Authorization': `Bearer ${providerApiKey}`, 'Content-Type': 'application/json' },
638
- timeout: 60000,
639
- }, (res) => {
640
- if (res.statusCode !== 200) {
641
- let data = '';
642
- res.on('data', c => data += c);
643
- res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
644
- return;
645
- }
646
- let buffer = '';
647
- let streamText = '';
648
- const toolCalls = []; // { index, id, name, args }
649
- res.on('data', (chunk) => {
650
- buffer += chunk.toString('utf8');
651
- const lines = buffer.split('\n');
652
- buffer = lines.pop() || '';
653
- for (const line of lines) {
654
- const trimmed = line.trim();
655
- if (!trimmed || !trimmed.startsWith('data:')) continue;
656
- const data = trimmed.slice(5).trim();
657
- if (data === '[DONE]') {
658
- resolve({ streamText, toolCalls });
659
- return;
660
- }
661
- try {
662
- const parsed = JSON.parse(data);
663
- const choice = parsed.choices?.[0];
664
- if (!choice) continue;
665
- const delta = choice.delta;
666
-
667
- // Text content
668
- if (delta.content) {
669
- streamText += delta.content;
670
- onChunk(delta.content);
671
- }
672
-
673
- // Tool calls (streaming delta) — shared accumulator,
674
- // see src/utils/streaming-tools.js for the per-index pattern.
675
- if (delta.tool_calls) {
676
- accumulateToolCallDeltas(toolCalls, delta.tool_calls);
677
- }
678
- } catch {}
679
- }
680
- });
681
- res.on('end', () => resolve({ streamText, toolCalls }));
682
- });
683
- req.on('error', reject);
684
- req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
685
- req.write(JSON.stringify(body));
686
- req.end();
687
- });
688
- } catch (err) {
689
- // Fallback chain: try next model on error
690
- const fb = fallbackChain.recordError(body.model, err);
691
- if (fb.fallback) {
692
- console.log(tui.C.yellow(`\n ⚠ ${body.model} başarısız → ${fb.nextModel} deneniyor...\n`));
693
- continue; // Retry with next model
694
- }
695
- throw err;
696
- }
697
-
698
- fullText = result.streamText;
699
-
700
- // Tool call var mı? finalizeToolCalls drops empty entries + synthesizes ids,
701
- // so we only need to check the resulting length.
702
- const finalized = finalizeToolCalls(result.toolCalls);
703
- if (finalized.length > 0) {
704
- // Assistant mesajını ekle (tool_calls ile)
705
- currentMessages.push({
706
- role: 'assistant',
707
- content: result.streamText || null,
708
- tool_calls: finalized,
709
- });
710
- // Her tool call'ı çalıştır, sonuçları tool mesajı olarak ekle
711
- const toolResults = await processToolCalls(finalized, onToolCall, stdinPrompt);
712
- currentMessages.push(...toolResults);
713
-
714
- // Plan mode review: plan submitted, wait for user approval
715
- if (planMode.inReview()) {
716
- const { plan } = planMode.planHistory[planMode.planHistory.length - 1] || {};
717
- console.log('\n' + tui.C.cyan(' 📋 Plan sunuldu onay bekleniyor...'));
718
- console.log(tui.C.muted(' ' + '─'.repeat(56)));
719
- console.log(plan ? `\n ${plan.replace(/\n/g, '\n ')}` : '');
720
- console.log('\n' + tui.C.muted(' ─'.repeat(28)));
721
- const approved = await new Promise(resolve => {
722
- stdinPrompt(tui.C.yellow(' Planı onaylıyor musun? [Y=exec, n=reddet, e=düzenle]: '), answer => {
723
- const key = answer.trim().toLowerCase();
724
- if (key === 'n' || key === 'no') { planMode.reject(); resolve(false); }
725
- else if (key === 'e' || key === 'edit') { planMode.reject(); resolve('edit'); }
726
- else { planMode.approve(); resolve(true); }
727
- });
728
- });
729
- if (approved === true) {
730
- console.log(tui.C.green(' ✓ Plan onaylandı. Plan uygulanıyor...\n'));
731
- // Devam model cevap versin
732
- } else if (approved === 'edit') {
733
- console.log(tui.C.yellow(' 📝 Planı düzenleyin ve /plan ile yeniden gönderin.\n'));
734
- // Plan modunda kal, mesaj ekle
735
- currentMessages.push({ role: 'user', content: 'Planı düzenle ve yeniden sun.' });
736
- continue;
737
- } else {
738
- console.log(tui.C.amber(' ⨯ Plan reddedildi. Yeniden plan yapılıyor...\n'));
739
- currentMessages.push({ role: 'user', content: 'Plan reddedildi. Lütfen farklı bir yaklaşım dene.' });
740
- continue;
741
- }
742
- }
743
-
744
- // Devam — model sonuçları görsün, cevap versin
745
- continue;
746
- }
747
-
748
- break; // Tool call yok, çık
749
- }
750
-
751
- return fullText;
752
- }
753
-
754
- /**
755
- * Tool call'ları çalıştır, sonuçları OpenAI uyumlu tool mesajlarına dönüştür.
756
- * v5.7.18: Concurrent execution for parallel-safe tools + untrusted result wrapping.
757
- */
758
- const UNTRUSTED_TOOLS = new Set(['browser', 'web_search', 'duckduckgo_search', 'searxng_search', 'exa_search', 'firecrawl', 'web_readability']);
759
- const PARALLEL_SAFE_TOOLS = new Set(['read_file', 'file_search', 'grep_search', 'web_search', 'web_readability', 'duckduckgo_search', 'exa_search', 'searxng_search', 'firecrawl', 'memory_search', 'memory']);
760
- const { checkPreHooks, runPostHooks, permissionSummary } = require('../utils/tool-hooks');
761
- const { checkPermission, isApproved, markApproved, formatPermissionPrompt } = require('../utils/permissions');
762
- const { getPlanMode } = require('../utils/plan-mode');
763
- const { getWorktree } = require('../utils/worktree');
764
- const { getLevel: getEffortLevel, getConfig: getEffortConfig } = require('../utils/effort-levels');
765
- const { getResponseFormat, hasStructuredOutput } = require('../utils/structured-output');
766
- const { getFallbackChain } = require('../utils/fallback-chain');
767
-
768
- /**
769
- * Prompt user for permission approval.
770
- * Returns: true (once), 'session', 'persistent', or false (denied).
771
- */
772
- async function askPermissionPrompt(question, hint, prompter) {
773
- const full = `${tui.C.yellow('⚠')} ${tui.C.bold('İzin gerekiyor')}: ${question}\n${tui.C.muted(hint)}`;
774
- return new Promise(resolve => {
775
- prompter(full, answer => {
776
- const key = answer.trim();
777
- if (key === 'y') resolve(true); // once
778
- else if (key === 'Y') resolve('session'); // session
779
- else if (key === 'p') resolve('persistent'); // disk
780
- else if (key === 'a') { _permSessionCache.set('__ALL__', true); resolve(true); } // all (legacy)
781
- else resolve(false); // no
782
- });
783
- });
784
- }
785
-
786
- /** Session permission cache (for ask hooks) */
787
- const _permSessionCache = new Map();
788
-
789
-
790
- async function processToolCalls(toolCalls, onToolCall, onAsk) {
791
- const toolDefs = getToolDefs();
792
- const results = [];
793
-
794
- // Parse all tool calls first
795
- const parsed = toolCalls.map(tc => {
796
- const name = tc.function?.name || tc.name;
797
- const argsStr = tc.function?.arguments || tc.args || '{}';
798
- const id = tc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
799
- let args = {};
800
- try {
801
- args = typeof argsStr === 'string' ? JSON.parse(argsStr) : argsStr;
802
- } catch (e) {
803
- args = { _parse_error: e.message, _raw: argsStr };
804
- }
805
- return { name, args, id };
806
- });
807
-
808
- // Filter out blocked tools via guardrails
809
- guardrails.startIteration();
810
- const blocked = parsed.filter(p => {
811
- const check = guardrails.check(p.name, p.args);
812
- if (check.blocked) {
813
- if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: check.reason } });
814
- results.push({ ...p, result: { error: check.reason }, _blocked: true });
815
- return false;
816
- }
817
- return true;
818
- });
819
- const allowed = parsed.filter(p => !results.some(r => r.id === p.id && r._blocked));
820
-
821
- // Separate parallel-safe and sequential tools (over allowed only)
822
- const parallelBatch = allowed.filter(p => PARALLEL_SAFE_TOOLS.has(p.name));
823
- const sequentialBatch = allowed.filter(p => !PARALLEL_SAFE_TOOLS.has(p.name));
824
-
825
- // Notify UI for all non-blocked
826
- for (const p of allowed) {
827
- if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'running' });
828
- }
829
-
830
- // Shared tool execution with: planCheck → permissions → hooks → execute → post-hooks
831
- async function executeOne(p) {
832
- // 0. Plan mode check
833
- const pm = getPlanMode();
834
- const planCheck = pm.checkTool(p.name, p.args);
835
- if (!planCheck.allowed) {
836
- const denied = { ...p, result: { error: planCheck.reason }, _plan_blocked: true };
837
- if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: planCheck.reason } });
838
- return denied;
839
- }
840
- pm.recordTool(p.name, p.args);
841
-
842
- // 1. Permission check (config-based granular rules)
843
- const perm = checkPermission(p.name, p.args);
844
- if (perm.action === 'deny') {
845
- const denied = { ...p, result: { error: perm.reason }, _perm_blocked: true };
846
- if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: perm.reason } });
847
- return denied;
848
- }
849
- if (perm.action === 'ask') {
850
- const permKey = `${perm.rule.raw}:${JSON.stringify(p.args)}`;
851
- if (!isApproved(permKey)) {
852
- const ok = await askPermissionPrompt(
853
- `${formatPermissionPrompt(p.name, p.args, perm.reason)}\n ${perm.reason}`,
854
- `Bu işleme izin ver? [y=once, Y=session, p=persistent, n=no] `,
855
- onAsk
856
- );
857
- if (ok === 'persistent') markApproved(permKey, true);
858
- else if (ok === 'session') markApproved(permKey, false);
859
- else if (!ok) {
860
- const denied = { ...p, result: { error: `İzin reddedildi: ${perm.rule.raw}` }, _perm_blocked: true };
861
- if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: `İzin reddedildi: ${perm.rule.raw}` } });
862
- return denied;
863
- }
864
- }
865
- }
866
-
867
- // 2. Pre-hook check
868
- const hook = checkPreHooks(p.name, p.args);
869
- if (hook.action === 'deny') {
870
- const denied = { ...p, result: { error: hook.reason }, _hook_blocked: true };
871
- if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: hook.reason } });
872
- return denied;
873
- }
874
- if (hook.action === 'ask') {
875
- const ok = await askPermissionPrompt(
876
- permissionSummary(hook.rule, p.name, p.args),
877
- `Hook onayı [y=once, Y=session, n=no]: `,
878
- onAsk
879
- );
880
- if (!ok) {
881
- const denied = { ...p, result: { error: `Hook reddetti: ${hook.rule.raw}` }, _hook_blocked: true };
882
- if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: `Hook reddetti: ${hook.rule.raw}` } });
883
- return denied;
884
- }
885
- }
886
-
887
- const result = await executeTool(p.name, p.args, toolDefs);
888
- const success = result?.success !== false;
889
- guardrails.record(p.name, p.args, success);
890
- return { ...p, result: runPostHooks(p.name, p.args, result) };
891
- }
892
-
893
- // Run parallel batch concurrently
894
- if (parallelBatch.length > 0) {
895
- const parallelResults = await Promise.all(parallelBatch.map(executeOne));
896
- results.push(...parallelResults);
897
- }
898
-
899
- // Run sequential batch one at a time
900
- for (const p of sequentialBatch) {
901
- const r = await executeOne(p);
902
- results.push(r);
903
- }
904
-
905
- // No-progress check: if all tools failed, inject warning
906
- if (guardrails.isNoProgress()) {
907
- if (onToolCall) onToolCall({ name: '_no_progress', args: null, status: 'done', result: { error: 'All tools failed this iteration' } });
908
- }
909
-
910
- // Same-tool loop detection: if same tool called >3x this iteration (regardless of success)
911
- const toolCallCounts = {};
912
- for (const { name } of results) {
913
- toolCallCounts[name] = (toolCallCounts[name] || 0) + 1;
914
- }
915
- for (const [name, count] of Object.entries(toolCallCounts)) {
916
- if (count > 3) {
917
- const loopResult = results.find(r => r.name === name);
918
- if (loopResult && !loopResult.result?.error) {
919
- const warnContent = JSON.stringify({
920
- _loop_warning: true,
921
- message: `${name} called ${count}x this turn. If you're not making progress, try a different approach or report the result.`,
922
- tool: name, call_count: count,
923
- });
924
- results.push({ name: '_loop_warning', id: `loop_${Date.now()}`, result: { result: warnContent } });
925
- }
926
- }
927
- }
928
-
929
- // Notify UI done + build messages
930
- // v5.9.7: Skip internal meta-tools (_loop_warning, _no_progress) from tool messages
931
- // Inject loop warning as user message instead (Gemini requires real tool names)
932
- // Gemini also requires 'name' field in tool response messages
933
- const messages = [];
934
- for (const { name, id, result } of results) {
935
- if (onToolCall) onToolCall({ name, args: null, status: 'done', result });
936
-
937
- if (name === '_loop_warning' || name === '_no_progress') {
938
- if (name === '_loop_warning') {
939
- const warnContent = typeof result?.result === 'string' ? result.result : '';
940
- if (warnContent) messages.push({ role: 'user', content: '[System: ' + warnContent + ']' });
941
- }
942
- continue;
943
- }
944
-
945
- let content;
946
- if (result.error) {
947
- content = JSON.stringify({ error: result.error });
948
- } else {
949
- let raw = typeof result.result === 'string' ? result.result : JSON.stringify(result.result).slice(0, 8000);
950
- // v5.7.18: Untrusted result wrapping (Hermes-style prompt injection defense)
951
- if (UNTRUSTED_TOOLS.has(name)) {
952
- content = `<untrusted_tool_result source="${name}">\n${raw}\n</untrusted_tool_result>`;
953
- } else {
954
- content = raw;
955
- }
956
- }
957
-
958
- messages.push({ role: 'tool', tool_call_id: id, name, content });
959
- }
960
-
961
- return messages;
962
- }
963
-
964
- function printHelp() {
965
- console.log(chalk.cyan('\n 📚 REPL Komutları:\n'));
966
- console.log(' ' + chalk.yellow('/help'.padEnd(22)) + chalk.gray('Bu yardım'));
967
- console.log(' ' + chalk.yellow('/clear'.padEnd(22)) + chalk.gray('Ekranı temizle'));
968
- console.log(' ' + chalk.yellow('/history'.padEnd(22)) + chalk.gray('Bu oturumun geçmişi'));
969
- console.log(' ' + chalk.yellow('/memory'.padEnd(22)) + chalk.gray('Memory\'i göster'));
970
- console.log(' ' + chalk.yellow('/plan [on|off|show]'.padEnd(22)) + chalk.gray('Plan modu'));
971
- console.log(' ' + chalk.yellow('/forget'.padEnd(22)) + chalk.gray('Memory\'i temizle'));
972
- console.log(' ' + chalk.yellow('/sessions'.padEnd(22)) + chalk.gray('Geçmiş oturumları listele'));
973
- console.log(' ' + chalk.yellow('/resume [id|last]'.padEnd(22)) + chalk.gray('Önceki oturuma dön'));
974
- console.log(' ' + chalk.yellow('/system <text>'.padEnd(22)) + chalk.gray('System prompt değiştir'));
975
- console.log(' ' + chalk.yellow('/model <name>'.padEnd(22)) + chalk.gray('Model değiştir'));
976
- console.log(' ' + chalk.yellow('/identity [ad]'.padEnd(22)) + chalk.gray('Bot adını değiştir'));
977
- console.log(' ' + chalk.yellow('/tokens'.padEnd(22)) + chalk.gray('Token kullanımı'));
978
- console.log(' ' + chalk.yellow('/save'.padEnd(22)) + chalk.gray('Oturumu manuel kaydet'));
979
- console.log(' ' + chalk.yellow('/exit veya /quit'.padEnd(22)) + chalk.gray('Çıkış (Ctrl+C de çalışır)'));
980
- console.log(chalk.cyan('\n 🛠️ Tüm CLI Komutları (REPL içinden):\n'));
981
- for (const [cmd, info] of Object.entries(CLI_COMMANDS)) {
982
- console.log(' ' + chalk.yellow(cmd.padEnd(22)) + chalk.gray(info.desc));
983
- }
984
- console.log('');
985
- }
986
-
987
- function runCliCommand(args) {
988
- return new Promise((resolve) => {
989
- const proc = spawn('node', [path.join(__dirname, '..', '..', 'bin', 'natureco.js'), ...args], {
990
- stdio: 'inherit',
991
- });
992
- proc.on('close', (code) => resolve(code));
993
- proc.on('error', (e) => { console.log(chalk.red(' Hata: ' + e.message)); resolve(1); });
994
- });
995
- }
996
-
997
- async function startRepl(args) {
998
- ensureDir(MEMORY_DIR); ensureDir(SESSION_DIR);
999
-
1000
- const cfg = getConfig();
1001
- let providerUrl = cfg.providerUrl;
1002
- let providerApiKey = cfg.providerApiKey;
1003
- let model = cfg.providerModel;
1004
-
1005
- // Arg parse
1006
- let resumeId = null;
1007
- for (let i = 0; i < args.length; i++) {
1008
- if (args[i] === '--model' && args[i + 1]) model = args[++i];
1009
- if (args[i] === '--resume') resumeId = args[i + 1] || 'last';
1010
- }
1011
-
1012
- if (!providerUrl || !providerApiKey) {
1013
- console.log(chalk.red('\n Provider ayarlı değil. Önce: natureco setup\n'));
1014
- process.exit(1);
1015
- }
1016
-
1017
- // Memory yükle
1018
- let memory = loadMemory(cfg.userName);
1019
-
1020
- // v5.6.19: Oncelik config.botName, sonra memory.botName
1021
- if (!memory.botName) {
1022
- memory.botName = cfg.botName || 'Asistan';
1023
- }
1024
- // BotName'i memory'ye persist et (her oturumda ayni kalsin)
1025
- try {
1026
- const fs = require('fs');
1027
- const memFile = path.join(os.homedir(), '.natureco', 'memory', ((cfg.userName || 'default').toLowerCase()) + '.json');
1028
- if (fs.existsSync(memFile)) {
1029
- const memData = JSON.parse(memFile, 'utf8');
1030
- if (!memData.botName || memData.botName !== memory.botName) {
1031
- memData.botName = memory.botName;
1032
- fs.writeFileSync(memFile, JSON.stringify(memData, null, 2), 'utf8');
1033
- }
1034
- }
1035
- } catch (e) {} // Sessizce devam et, kritik degil
1036
-
1037
- // v5.4.11: Sasuke Brain - Cross-session context otomatik yukle
1038
- // REPL acildiginda son oturumdan 1-2 context mesaji al
1039
- let crossSessionContext = '';
1040
- try {
1041
- const { listSessions, loadSession } = require('../utils/sessions-helper');
1042
- if (listSessions && loadSession) {
1043
- const sessions = listSessions(1);
1044
- if (sessions && sessions.length > 0) {
1045
- const lastSession = loadSession(sessions[0].id);
1046
- if (lastSession && lastSession.messages && lastSession.messages.length > 0) {
1047
- // Son 2 user message
1048
- const lastUserMsgs = lastSession.messages
1049
- .filter(m => m.role === 'user')
1050
- .slice(-2);
1051
- if (lastUserMsgs.length > 0) {
1052
- crossSessionContext = '\n[KONUSMA GECMISI: ' + sessions[0].id + ']\n' +
1053
- lastUserMsgs.map(m => 'User: ' + (m.content || '').slice(0, 100)).join('\n') + '\n[/GECMISI]\n';
1054
- }
1055
- }
1056
- }
1057
- }
1058
- } catch (e) {
1059
- // Cross-session yukleme basarisiz, devam et
1060
- }
1061
- // v5.4.12: 3 SOUL dosyasini birlestir (SOUL.md + IDENTITY.md + AGENTS.md)
1062
- const { buildSoulContext, summarizeSoul } = require("../tools/soul");
1063
- const soulSummary = buildSoulContext(); // 3 dosya birlesik ozet
1064
-
1065
- // v5.7.14: Hermes-style memory store (MEMORY.md / USER.md) + skill index
1066
- const memoryStore = getMemoryStore();
1067
- memoryStore.load();
1068
- const memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1069
- const skillsIndexBlock = buildSkillIndex();
1070
-
1071
- // Resume?
1072
- let messages = [];
1073
- if (resumeId) {
1074
- const session = loadSession(resumeId);
1075
- if (session) {
1076
- messages = session.messages || [];
1077
- console.log(chalk.green(`\n ✓ Oturum yüklendi: ${session.id} (${messages.length} mesaj)\n`));
1078
- } else {
1079
- console.log(chalk.yellow(`\n ⚠️ Oturum bulunamadı: ${resumeId}\n`));
1080
- }
1081
- }
1082
-
1083
- // System prompt oluştur (memory + identity + persistent bağlam)
1084
- // v5.6.5: Kucuk model tespiti (Groq, Mistral Small, Ollama) - SOUL injection skip
1085
- const botName = memory.botName || 'Asistan';
1086
- const userName = memory.name || memory.nickname || cfg.userName;
1087
- const isSmallModel = (cfg.providerUrl || '').includes('groq.com') ||
1088
- (cfg.providerUrl || '').includes('mistral.ai') ||
1089
- (cfg.providerUrl || '').includes('localhost') ||
1090
- (cfg.providerUrl || '').includes('ollama');
1091
- // Discover project rules (CLAUDE.md)
1092
- const projectRules = discoverProjectRules(process.cwd());
1093
- if (projectRules) {
1094
- console.log(chalk.cyan(` 📋 Proje kurallari bulundu (CLAUDE.md)\n`));
1095
- }
1096
-
1097
- // Build system prompt with tier caching (stable+context cached, volatile fresh)
1098
- const promptOpts = {
1099
- botName, userName, soulSummary, isSmallModel,
1100
- memorySnapshotBlock, skillsIndexBlock, projectRules,
1101
- crossSessionContext: crossSessionContext || '',
1102
- userHome: cfg.userHome || '',
1103
- hasHistory: messages.length > 0,
1104
- memoryFacts: memory.facts || [],
1105
- };
1106
- let systemPrompt = rebuildSystemPrompt(promptOpts);
1107
-
1108
- if (messages.length === 0) {
1109
- messages.push({ role: 'system', content: systemPrompt, _internal: true });
1110
- } else {
1111
- // Resume: system prompt'u güncelle (memory değişmiş olabilir)
1112
- const sysIdx = messages.findIndex(m => m._internal);
1113
- if (sysIdx >= 0) messages[sysIdx] = { role: 'system', content: systemPrompt, _internal: true };
1114
- else messages.unshift({ role: 'system', content: systemPrompt, _internal: true });
1115
- }
1116
-
1117
- // Header
1118
- console.log('');
1119
- console.log(tui.styled(' 🌿 NatureCo REPL · Persistent Sohbet', { color: tui.PALETTE.primary, bold: true }));
1120
- console.log(tui.styled(' ' + ''.repeat(56), { color: tui.PALETTE.border }));
1121
- console.log(tui.C.muted(' Provider: ') + tui.C.brand(providerUrl.replace(/https?:\/\//, '')));
1122
- console.log(tui.C.muted(' Model: ') + tui.C.brand(model));
1123
- console.log(tui.C.muted(' Kullanıcı: ') + tui.C.brand((memory.nickname || cfg.userName) + (memory.nickname ? ` (${cfg.userName})` : '')));
1124
- console.log(tui.C.muted(' Bot: ') + tui.C.brand(memory.botName || 'Asistan'));
1125
- if (messages.length > 1) {
1126
- console.log(tui.C.muted(' Oturum: ') + tui.C.amber(`${messages.filter(m => m.role === 'user' || m.role === 'assistant').length} mesaj (resume)`));
1127
- }
1128
- console.log(tui.C.muted(' Komutlar: ') + tui.C.yellow('/help') + tui.C.muted(' · ') + tui.C.yellow('/memory') + tui.C.muted(' · ') + tui.C.yellow('/sessions') + tui.C.muted(' · ') + tui.C.yellow('/exit'));
1129
- console.log('');
1130
- // v5.4.7: Hard-coded kimlik — v5.14.5: memory fact'lerinden kullanici adini tespit et
1131
- const displayBotName = memory.botName || 'Asistan';
1132
- const nameFromFact = (() => {
1133
- const facts = memory.facts || [];
1134
- for (const f of facts) {
1135
- const v = (f.value || f || '').trim();
1136
- const lv = v.toLowerCase();
1137
- const match = lv.match(/(?:kullanici\s*adi?|kullanıcı\s*adı?|isim|name)\s*:?\s*(.+)/);
1138
- if (match && match[1].trim().length > 2) return match[1].trim();
1139
- }
1140
- return null;
1141
- })();
1142
- const displayUserName = memory.name || nameFromFact || memory.nickname || cfg.userName;
1143
- console.log(tui.C.brand(' 👋 Ben ' + displayBotName + ', ' + displayUserName + '. Sen nasilsin?'));
1144
- console.log('');
1145
-
1146
- // v5.4.14: SOUL'dan onemli bilgileri de goster
1147
- if (soulSummary) {
1148
- const soulPreview = soulSummary.split('\n').slice(0, 3).join(' ').substring(0, 200);
1149
- if (soulPreview) {
1150
- console.log(tui.C.muted(' 📜 ' + soulPreview + '...'));
1151
- console.log('');
1152
- }
1153
- }
1154
-
1155
- let totalInputTokens = 0;
1156
- let totalOutputTokens = 0;
1157
-
1158
- enableBracketedPaste(process.stdout);
1159
-
1160
- const rl = readline.createInterface({
1161
- input: createPasteSafeInput(process.stdin),
1162
- output: createOutputFilter(process.stdout),
1163
- prompt: tui.styled('\n You ', { color: tui.PALETTE.primary, bold: true }),
1164
- terminal: true,
1165
- });
1166
- rl.prompt();
1167
-
1168
- const cleanup = async (exitCode = 0) => {
1169
- if (messages.length > 1) {
1170
- // v5.4.10: Once oturumdaki butun conversation'i memory'ye persist et
1171
- // Bu, Parton'un "oturum sonunda konusmalar kaydedilmiyor" sikayetini cözüyor
1172
- const persistResult = await persistSessionToMemory(messages, memory, cfg);
1173
- if (persistResult && persistResult.factsAdded > 0) {
1174
- console.log(chalk.gray(`\n 🧠 ${persistResult.factsAdded} yeni fact memory'ye kaydedildi`));
1175
- }
1176
- if (persistResult && persistResult.preferencesAdded > 0) {
1177
- console.log(chalk.gray(` 🎯 ${persistResult.preferencesAdded} yeni tercih kaydedildi`));
1178
- }
1179
-
1180
- const sessId = saveSession(messages, {
1181
- provider: providerUrl, model, user: cfg.userName,
1182
- bot: memory.botName, factCount: memory.facts?.length || 0,
1183
- });
1184
- console.log(chalk.gray(`\n 💾 Oturum kaydedildi: ${sessId}`));
1185
- }
1186
- // Global buffer temizle
1187
- if (global._fixBuffer) global._fixBuffer = '';
1188
- disableBracketedPaste(process.stdout);
1189
- console.log(chalk.gray('\n 👋 Görüşürüz!\n'));
1190
- process.exit(exitCode);
1191
- };
1192
-
1193
- /**
1194
- * v5.4.10: Oturum sonunda conversation'dan otomatik fact/preference extraction
1195
- * Bu, kullanıcının "her çıkışta konuşmalar kaydedilmiyor" sikayetini çözer
1196
- */
1197
- async function persistSessionToMemory(messages, memory, cfg) {
1198
- let factsAdded = 0;
1199
- let preferencesAdded = 0;
1200
-
1201
- try {
1202
- // Pattern-based extraction (zaten extractFacts var)
1203
- const newFacts = extractFacts(messages, memory.facts || []);
1204
-
1205
- // Bazi user message'lari da tara genel kalıplarla fact çıkar
1206
- const userMessages = messages.filter(m => m.role === 'user' && !m._internal);
1207
- for (const msg of userMessages) {
1208
- const text = (msg.content || '').toLowerCase();
1209
-
1210
- // BotName hatirlatmasi
1211
- if (text.includes('ad') && (text.includes('adin') || text.includes('ismin'))) {
1212
- // Bot adı sorgulanmış olabilir, mevcut adı koru
1213
- }
1214
-
1215
- // Kisilik tercihleri (genel pattern'ler)
1216
- const prefPatterns = [
1217
- { match: /(?:benim ad[ıi]m?|bana\s+.*de|ad[ıi]m?)\s+(\w+)/i, category: 'personal', key: 'ad' },
1218
- { match: /(?:seviyorum|hoşlan[ıi]yorum|beğeniyorum)\s+(\w+)/i, category: 'preference', key: 'sevilen' },
1219
- { match: /(?:yaşıyorum|oturuyorum|kalıyorum)\s+(\w+)/i, category: 'location', key: 'yer' },
1220
- ];
1221
- for (const p of prefPatterns) {
1222
- const m2 = msg.content.match(p.match);
1223
- if (m2) {
1224
- const val = m2[1].toLowerCase();
1225
- const fact = `Kullanici ${p.key}: ${val}`;
1226
- if (!(memory.facts || []).some(f => f.value === fact)) {
1227
- newFacts.push({ value: fact, score: 6, category: p.category, createdAt: new Date().toISOString() });
1228
- }
1229
- }
1230
- }
1231
- }
1232
-
1233
- // Deduplicate
1234
- const existingValues = new Set((memory.facts || []).map(f => (f.value || f).toLowerCase()));
1235
- const uniqueFacts = newFacts.filter(f => !existingValues.has((f.value || f).toLowerCase()));
1236
-
1237
- if (uniqueFacts.length > 0) {
1238
- memory.facts = [...(memory.facts || []), ...uniqueFacts];
1239
- // v5.4.10: Verification ile kaydet
1240
- const memFile = path.join(MEMORY_DIR, (cfg.userName || 'default').toLowerCase() + '.json');
1241
- memory.lastUpdated = new Date().toISOString();
1242
- fs.writeFileSync(memFile, JSON.stringify(memory, null, 2), 'utf8');
1243
- // Verification: geri oku
1244
- const verify = JSON.parse(fs.readFileSync(memFile, 'utf8'));
1245
- factsAdded = uniqueFacts.length;
1246
- }
1247
-
1248
- // Decay (eski fact'leri dusuk skora dusur)
1249
- if (memory.facts && memory.facts.length > 15) {
1250
- // Max 15 fact tut, en dusuk skorlu olanlari sil
1251
- memory.facts.sort((a, b) => (b.score || 5) - (a.score || 5));
1252
- memory.facts = memory.facts.slice(0, 15);
1253
- }
1254
- } catch (e) {
1255
- // Sessizce devam et, kritik degil
1256
- }
1257
-
1258
- return { factsAdded, preferencesAdded };
1259
- }
1260
-
1261
- rl.on('SIGINT', () => cleanup(0));
1262
- process.on('SIGINT', () => cleanup(0));
1263
- process.on('SIGTERM', () => cleanup(0));
1264
-
1265
- rl.on('line', async (input) => {
1266
- const line = restoreNewlines(input).trim();
1267
- if (!line) { rl.prompt(); return; }
1268
-
1269
- // Çok satırlı paste: output filter'a echo'yu durdurma sinyalini ver
1270
- clearPasteContext();
1271
-
1272
- // Slash komutlar
1273
- if (line.startsWith('/')) {
1274
- const parts = line.slice(1).split(/\s+/);
1275
- const cmd = parts[0].toLowerCase();
1276
- const arg = parts.slice(1).join(' ');
1277
-
1278
- switch (cmd) {
1279
- case 'help': printHelp(); break;
1280
- case 'clear': console.clear(); break;
1281
- case 'exit': case 'quit': case 'q': await cleanup(0); return;
1282
- case 'history':
1283
- console.log(chalk.cyan('\n 📜 Bu oturumun geçmişi:\n'));
1284
- for (const m of messages.filter(m => !m._internal)) {
1285
- const role = m.role === 'user' ? chalk.green('You') : chalk.blue('AI ');
1286
- const content = (m.content || '').slice(0, 120) + ((m.content || '').length > 120 ? '...' : '');
1287
- console.log(` ${role} ${content}`);
1288
- }
1289
- console.log('');
1290
- break;
1291
- case 'memory':
1292
- console.log(chalk.cyan('\n 🧠 Memory:\n'));
1293
- console.log(' Kullanıcı: ' + chalk.cyan(memory.name));
1294
- console.log(' Nickname: ' + chalk.cyan(memory.nickname || '(yok)'));
1295
- console.log(' Bot: ' + chalk.cyan(memory.botName || 'Asistan'));
1296
- if (memory.facts && memory.facts.length > 0) {
1297
- console.log(' Facts (' + memory.facts.length + '):');
1298
- for (const f of memory.facts) {
1299
- console.log(' • ' + chalk.gray((f.value || f) + (f.score ? ' [skor:' + f.score + ']' : '')));
1300
- }
1301
- } else {
1302
- console.log(chalk.gray(' (Henüz fact yok)'));
1303
- }
1304
- console.log('');
1305
- break;
1306
- case 'forget':
1307
- try {
1308
- if (fs.existsSync(path.join(MEMORY_DIR, `${(cfg.userName || 'default').toLowerCase()}.json`))) {
1309
- fs.unlinkSync(path.join(MEMORY_DIR, `${(cfg.userName || 'default').toLowerCase()}.json`));
1310
- }
1311
- memory = { name: cfg.userName, nickname: null, botName: 'Asistan', facts: [], preferences: [], history: [] };
1312
- // System prompt'u rebuild with cleared memory
1313
- promptOpts.memoryFacts = [];
1314
- memoryStore.clear();
1315
- promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1316
- systemPrompt = rebuildSystemPrompt(promptOpts);
1317
- messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1318
- console.log(chalk.green(' ✓ Memory temizlendi'));
1319
- } catch (e) {
1320
- console.log(chalk.red(' ❌ ' + e.message));
1321
- }
1322
- break;
1323
- case 'sessions':
1324
- const idx = loadSessionsIndex();
1325
- console.log(chalk.cyan('\n 📚 Geçmiş Oturumlar (' + idx.sessions.length + ')\n'));
1326
- for (let i = 0; i < Math.min(10, idx.sessions.length); i++) {
1327
- const s = idx.sessions[i];
1328
- console.log(` ${chalk.gray((i + 1).toString().padStart(2) + '.')} ${chalk.cyan(s.id)} ${chalk.muted('— ' + s.firstUserMessage)}`);
1329
- }
1330
- console.log(chalk.gray('\n Devam etmek için: /resume <id> veya /resume last\n'));
1331
- break;
1332
- case 'resume':
1333
- if (!arg) { console.log(chalk.yellow(' Kullanım: /resume <id> veya /resume last')); break; }
1334
- const session = loadSession(arg);
1335
- if (session) {
1336
- messages = session.messages || [];
1337
- const sysIdx = messages.findIndex(m => m._internal);
1338
- if (sysIdx >= 0) messages[sysIdx] = { role: 'system', content: systemPrompt, _internal: true };
1339
- else messages.unshift({ role: 'system', content: systemPrompt, _internal: true });
1340
- console.log(chalk.green(` ✓ Oturum yüklendi: ${session.id} (${messages.length} mesaj)`));
1341
- } else {
1342
- console.log(chalk.yellow(` ⚠️ Oturum bulunamadı: ${arg}`));
1343
- }
1344
- break;
1345
- case 'system':
1346
- if (!arg) { console.log(chalk.yellow(' Kullanım: /system <text>')); break; }
1347
- // Override stable tier directly (user's custom text)
1348
- _cachedStable = arg;
1349
- // Rebuild volatile only (context stays unchanged)
1350
- memoryStore.load();
1351
- promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1352
- promptOpts.memoryFacts = memory.facts || [];
1353
- const volOpts = { ...promptOpts, botName: '', soulSummary: '', skillsIndexBlock: '', crossSessionContext: '' };
1354
- const volTiers = buildTiers(volOpts);
1355
- systemPrompt = assemble(_cachedStable, _cachedContext, volTiers.volatile);
1356
- messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1357
- console.log(chalk.green(' ✓ System prompt güncellendi'));
1358
- break;
1359
- case 'model':
1360
- if (!arg) { console.log(chalk.yellow(' Kullanım: /model <name>')); break; }
1361
- model = arg;
1362
- console.log(chalk.green(' ✓ Model: ') + chalk.cyan(model));
1363
- break;
1364
- case 'identity':
1365
- if (!arg) { console.log(chalk.yellow(` Mevcut: ${memory.botName || 'Asistan'}`)); break; }
1366
- memory.botName = arg;
1367
- saveMemory(cfg.userName, memory);
1368
- // Rebuild stable tier with new botName
1369
- promptOpts.botName = arg;
1370
- _cachedTierOpts = null; // force full rebuild
1371
- memoryStore.load();
1372
- promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1373
- promptOpts.memoryFacts = memory.facts || [];
1374
- systemPrompt = rebuildSystemPrompt(promptOpts);
1375
- messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1376
- console.log(chalk.green(' ✓ Bot adı: ') + chalk.cyan(arg));
1377
- break;
1378
- case 'tokens':
1379
- console.log(chalk.gray(` Token: ~${totalInputTokens} in / ~${totalOutputTokens} out`));
1380
- break;
1381
- case 'plan':
1382
- if (arg === 'on' || arg === 'enter') {
1383
- if (getPlanMode().enter()) console.log(tui.C.cyan('\n 📋 Plan modu aktif. Plan yapın ve /plan off ile çıkın.\n'));
1384
- else console.log(tui.C.yellow(' Zaten plan modunda.'));
1385
- } else if (arg === 'off' || arg === 'exit') {
1386
- if (getPlanMode().isPlanning()) {
1387
- console.log(tui.C.yellow(' Plan modundan çıkılıyor. Plan yazılıp ExitPlanMode ile sunulmalı.'));
1388
- getPlanMode().approve();
1389
- } else {
1390
- console.log(tui.C.yellow(' Plan modunda değil.'));
1391
- }
1392
- } else if (arg === 'show') {
1393
- if (getPlanMode().planHistory.length > 0) {
1394
- const last = getPlanMode().planHistory[getPlanMode().planHistory.length - 1];
1395
- console.log(tui.C.cyan('\n 📋 Son Plan:\n'));
1396
- console.log(` ${last.plan.replace(/\n/g, '\n ')}`);
1397
- } else {
1398
- console.log(tui.C.yellow(' Henüz plan yok.'));
1399
- }
1400
- } else {
1401
- console.log(tui.C.yellow(' Kullanım: /plan on|off|show'));
1402
- }
1403
- break;
1404
- case 'save':
1405
- const sessId = saveSession(messages, {
1406
- provider: providerUrl, model, user: cfg.userName, bot: memory.botName,
1407
- });
1408
- console.log(chalk.green(' ✓ Kaydedildi: ') + chalk.cyan(sessId));
1409
- break;
1410
- default:
1411
- // CLI komutları (REPL içinden)
1412
- if (CLI_COMMANDS['/' + cmd]) {
1413
- const cliCmd = CLI_COMMANDS['/' + cmd];
1414
- if (cliCmd.needsArg && !arg) {
1415
- console.log(chalk.yellow(` ${cmd} bir argüman gerekli: ${cliCmd.desc}`));
1416
- } else {
1417
- console.log(chalk.gray(` → ${cmd} çalıştırılıyor...`));
1418
- const args2 = [...cliCmd.run];
1419
- if (arg && (cmd === 'seo' || cmd === 'naturehub')) args2.push(arg);
1420
- await runCliCommand(args2);
1421
- }
1422
- } else {
1423
- console.log(chalk.yellow(` Bilinmeyen komut: /${cmd}. /help yazın.`));
1424
- }
1425
- }
1426
- rl.prompt();
1427
- return;
1428
- }
1429
-
1430
- // User mesajı
1431
- messages.push({ role: 'user', content: line });
1432
-
1433
- // Çok satırlı (paste) mesajları gönderildikten sonra ekranda göster
1434
- if (line.indexOf('\n') !== -1) {
1435
- process.stdout.write(tui.styled(' You ', { color: tui.PALETTE.primary, bold: true }));
1436
- process.stdout.write(line + '\n');
1437
- }
1438
-
1439
- // v5.6.8: Hard-coded fallback - "sen kimsin?" sorulari icin dinamik botName
1440
- const trimmed = (line || '').toLowerCase();
1441
- const isIdentityQuestion = /(sen\s+kim|adin\s+ne|kendini\s+tan|kendin\s+tanit|kimsin|ne\s+adindasin)/.test(trimmed);
1442
- if (isIdentityQuestion) {
1443
- // v5.6.10: Hard-coded prefix minimal - model cevabini bozuyordu
1444
- // Once sadece isim yaz, modelin devamini getirsin
1445
- const displayName = memory.botName || 'Asistan';
1446
- process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
1447
- process.stdout.write('Merhaba! Ben ' + displayName + '. ');
1448
- }
1449
-
1450
- // AI cevabı — v5.13.0: workflow orchestrator ALWAYS first
1451
- process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
1452
- try {
1453
- // Per-turn: rebuild volatile tier with current memory snapshot
1454
- guardrails.reset();
1455
- memory = loadMemory(cfg.userName);
1456
- memoryStore.load();
1457
- promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1458
- promptOpts.memoryFacts = memory.facts || [];
1459
- systemPrompt = rebuildSystemPrompt(promptOpts);
1460
- messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1461
-
1462
- // v5.13.0: Run workflow FIRST for every request
1463
- process.stdout.write(tui.styled('\r 🔧 workflow... ', { color: tui.PALETTE.muted }));
1464
- const wfToolDefs = getToolDefs();
1465
- const wfResult = await executeTool('workflow', { action: 'run', task: line }, wfToolDefs);
1466
- const wf = wfResult?.result || {};
1467
- if (wf.success !== false) {
1468
- const loaded = wf.skillsLoaded && wf.skillsLoaded.length > 0 ? ` [skill: ${wf.skillsLoaded.join(', ')}]` : '';
1469
- process.stdout.write(tui.styled(` ✓ workflow${loaded}\n`, { color: tui.PALETTE.success }));
1470
- } else {
1471
- process.stdout.write(tui.styled(' ✗ workflow\n', { color: tui.PALETTE.danger }));
1472
- }
1473
-
1474
- if (wf.passthrough && wf.reply !== undefined && wf.reply !== null) {
1475
- // Simple chat workflow handled it directly
1476
- const fullReply = String(wf.reply);
1477
- const displayBotName = memory.botName || 'Asistan';
1478
- let fixedReply = String(fullReply);
1479
- fixedReply = fixedReply.replace(/\bMiniMax[-\s\w\.\d]*/gi, displayBotName);
1480
- fixedReply = fixedReply.replace(/\bM2\.5[-\s\w\.\d]*/gi, displayBotName);
1481
- fixedReply = fixedReply.replace(/\bM2[\s\-\.\w\d]*/gi, displayBotName);
1482
- fixedReply = fixedReply.replace(/\bClaude[-\s\w\.\d]*/gi, displayBotName);
1483
- fixedReply = fixedReply.replace(/\bGPT[-\s\w\.\d]*/gi, displayBotName);
1484
- fixedReply = fixedReply.replace(/\bChatGPT\b/g, displayBotName);
1485
- fixedReply = fixedReply.replace(/NatureCo\s+CLI(\s*'in|'nin)?/gi, displayBotName);
1486
- fixedReply = fixedReply.replace(/Ben\s+MiniMax[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1487
- fixedReply = fixedReply.replace(/Ben\s+Claude[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1488
- fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1489
- fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
1490
- fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
1491
- process.stdout.write('\n' + fixedReply + '\n');
1492
- messages.push({ role: 'assistant', content: fixedReply });
1493
- totalInputTokens += Math.ceil(line.length / 4);
1494
- totalOutputTokens += Math.ceil(fullReply.length / 4);
1495
- } else if (wf.status === 'completed' || (wf.results && wf.results.length > 0)) {
1496
- // Complex task — inject workflow report as context, then LLM crafts final reply
1497
- const workflowSteps = wf.results || [];
1498
- const report = workflowSteps.map(r => {
1499
- const t = r.tool || r.name || '?';
1500
- const s = r.status === 'done' ? '✓' : '✗';
1501
- let summary = '';
1502
- if (r.result) {
1503
- try { summary = typeof r.result === 'string' ? r.result.slice(0, 400) : JSON.stringify(r.result).slice(0, 400); } catch {}
1504
- }
1505
- return ` ${s} ${t}: ${summary}`;
1506
- }).join('\n');
1507
- const skillInfo = wf.skillsLoaded && wf.skillsLoaded.length > 0
1508
- ? `\n\nKullanilan skill'ler: ${wf.skillsLoaded.join(', ')}`
1509
- : '';
1510
- const preWfLen = messages.length;
1511
- messages.push({
1512
- role: 'system',
1513
- content: `=== WORKFLOW SONUCLARI ===\nSu araclar calisti:\n${report}${skillInfo}\n\nKullaniciya bu sonuclari anlamli bir sekilde ozetle.\n=== SONUC BITTI ===`,
1514
- });
1515
- const reply = await sendStreaming(
1516
- providerUrl,
1517
- providerApiKey,
1518
- messages,
1519
- model,
1520
- // v5.6.12: Callback bos - tam metin 'reply' olarak gelecek (non-stream mode)
1521
- () => {},
1522
- // Tool call callback — Hermes-style per-tool status line
1523
- ((toolEvent) => {
1524
- const name = toolEvent.name;
1525
- if (toolEvent.status === 'running') {
1526
- process.stdout.write(tui.styled('\r 🔧 ' + name + '... ', { color: tui.PALETTE.muted }));
1527
- } else if (toolEvent.status === 'done') {
1528
- if (toolEvent.result?.error) {
1529
- process.stdout.write(tui.styled(' ✗ ' + name + ': ' + String(toolEvent.result.error).slice(0, 80) + '\n', { color: tui.PALETTE.danger }));
1530
- } else {
1531
- process.stdout.write(tui.styled(' ✓ ' + name + '\n', { color: tui.PALETTE.success }));
1532
- }
1533
- }
1534
- })
1535
- );
1536
- // Remove workflow results message (already served its purpose)
1537
- messages.splice(preWfLen, 1);
1538
- // v5.6.12: Tam metin 'reply' olarak zaten geldi (non-stream mode)
1539
- const fullReply = String(reply || '');
1540
- // Bot adini al
1541
- const displayBotName = memory.botName || 'Asistan';
1542
- // v5.6.9: Tum model adlarini ve varyasyonlari temizle
1543
- let fixedReply = String(fullReply);
1544
- fixedReply = fixedReply.replace(/\bMiniMax[-\s\w\.\d]*/gi, displayBotName);
1545
- fixedReply = fixedReply.replace(/\bM2\.5[-\s\w\.\d]*/gi, displayBotName);
1546
- fixedReply = fixedReply.replace(/\bM2[\s\-\.\w\d]*/gi, displayBotName);
1547
- fixedReply = fixedReply.replace(/\bClaude[-\s\w\.\d]*/gi, displayBotName);
1548
- fixedReply = fixedReply.replace(/\bGPT[-\s\w\.\d]*/gi, displayBotName);
1549
- fixedReply = fixedReply.replace(/\bChatGPT\b/g, displayBotName);
1550
- fixedReply = fixedReply.replace(/NatureCo\s+CLI(\s*'in|'nin)?/gi, displayBotName);
1551
- fixedReply = fixedReply.replace(/Ben\s+MiniMax[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1552
- fixedReply = fixedReply.replace(/Ben\s+Claude[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1553
- fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1554
- fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
1555
- fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
1556
- process.stdout.write('\n' + fixedReply + '\n');
1557
- messages.push({ role: 'assistant', content: fixedReply });
1558
- totalInputTokens += Math.ceil(((fullReply || '') + report + skillInfo).length / 4);
1559
- totalOutputTokens += Math.ceil((fullReply || '').length / 4);
1560
- } else {
1561
- // Workflow failed or returned unexpected format
1562
- const fullReply = wf.error || JSON.stringify(wf).slice(0, 400) || 'Workflow islenemedi.';
1563
- const displayBotName = memory.botName || 'Asistan';
1564
- let fixedReply = fullReply.replace(/\bMiniMax[-\s\w\.\d]*/gi, displayBotName);
1565
- process.stdout.write('\n' + fixedReply + '\n');
1566
- messages.push({ role: 'assistant', content: fixedReply });
1567
- totalInputTokens += Math.ceil(line.length / 4);
1568
- totalOutputTokens += Math.ceil(fixedReply.length / 4);
1569
- }
1570
- } catch (err) {
1571
- process.stdout.write('\n');
1572
- console.log(chalk.red(' ❌ ' + err.message));
1573
- }
1574
- rl.prompt();
1575
- });
1576
-
1577
- rl.on('close', () => cleanup(0));
1578
- }
1579
-
1580
- module.exports = startRepl;
1
+ /**
2
+ * natureco repl — Persistent Interactive REPL
3
+ *
4
+ * Özellikler:
5
+ * ✅ Cross-session hafıza (memory dosyası)
6
+ * ✅ Otomatik fact extraction (LLM konuşmadan öğrenir)
7
+ * ✅ Session resume (--resume ile kaldığın yerden devam)
8
+ * ✅ Tüm CLI komutları REPL içinden (/doctor, /cost, /audit, /team...)
9
+ * ✅ Slash komutlar (/help, /memory, /model, /system, /exit)
10
+ * ✅ Ctrl+C temiz çıkış, konuşma otomatik kayıt
11
+ * ✅ Persistent session list (~/.natureco/sessions.json)
12
+ * ✅ Token tracking
13
+ *
14
+ * v4.6.0 — Persistent Memory Edition
15
+ */
16
+
17
+ const readline = require('readline');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+ const https = require('https');
22
+ const { spawn } = require('child_process');
23
+ const chalk = require('chalk');
24
+ const tui = require('../utils/tui');
25
+ const { loadToolDefinitions, toOpenAIFormat, executeTool } = require('../utils/tools');
26
+ const { accumulateToolCallDeltas, finalizeToolCalls } = require('../utils/streaming-tools');
27
+ const { createPasteSafeInput, createOutputFilter, enableBracketedPaste, disableBracketedPaste, restoreNewlines, clearPasteContext } = require('../utils/paste-safe-input');
28
+ const { getMemoryStore } = require('../utils/memory-store');
29
+ const { buildSkillIndex } = require('../utils/skill-index');
30
+ const { buildTiers, assemble, discoverProjectRules } = require('../utils/system-prompt');
31
+ const { ToolGuardrails } = require('../utils/tool-guardrails');
32
+
33
+ // v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
34
+ const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
35
+ function fixModelNameLeak(text, botName) {
36
+ if (!text) return text;
37
+ let fixed = text;
38
+ for (const modelName of MODEL_NAMES_TO_HIDE) {
39
+ const regex = new RegExp(modelName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
40
+ fixed = fixed.replace(regex, botName || 'asistan');
41
+ }
42
+ fixed = fixed.replace(/Ben\s+MiniMax[^.,!?\n]*/gi, 'Ben ' + (botName || 'asistan'));
43
+ fixed = fixed.replace(/I'm\s+MiniMax[^.,!?\n]*/gi, "I'm " + (botName || 'asistan'));
44
+ fixed = fixed.replace(/I am\s+Claude[^.,!?\n]*/gi, 'I am ' + (botName || 'asistan'));
45
+ return fixed;
46
+ }
47
+ global.fixModelNameLeak = fixModelNameLeak;
48
+
49
+ // v4.8.0: Tool definitions — başlangıçta bir kez yükle (performans)
50
+ let _toolDefs = null;
51
+ function getToolDefs() {
52
+ if (!_toolDefs) {
53
+ try {
54
+ _toolDefs = loadToolDefinitions();
55
+ } catch (e) {
56
+ _toolDefs = [];
57
+ }
58
+ }
59
+ return _toolDefs;
60
+ }
61
+
62
+ // ── System prompt tier cache (Hermes-style prefix cache warmth) ────────────
63
+ // stable+context built once at session start, volatile rebuilt per turn.
64
+ let _cachedStable = '';
65
+ let _cachedContext = '';
66
+ let _cachedTierOpts = null; // opts snapshot for volatile-only rebuilds
67
+
68
+ function rebuildSystemPrompt(opts) {
69
+ // If stable/context opts changed, rebuild them too
70
+ const needsFullRebuild = !_cachedTierOpts ||
71
+ _cachedTierOpts.botName !== opts.botName ||
72
+ _cachedTierOpts.soulSummary !== opts.soulSummary ||
73
+ _cachedTierOpts.skillsIndexBlock !== opts.skillsIndexBlock ||
74
+ _cachedTierOpts.crossSessionContext !== opts.crossSessionContext ||
75
+ _cachedTierOpts.projectRules !== opts.projectRules;
76
+
77
+ if (needsFullRebuild || !_cachedStable) {
78
+ const tiers = buildTiers(opts);
79
+ _cachedStable = tiers.stable;
80
+ _cachedContext = tiers.context;
81
+ _cachedTierOpts = {
82
+ botName: opts.botName,
83
+ soulSummary: opts.soulSummary,
84
+ skillsIndexBlock: opts.skillsIndexBlock,
85
+ crossSessionContext: opts.crossSessionContext,
86
+ projectRules: opts.projectRules,
87
+ };
88
+ }
89
+ // Volatile always rebuilt fresh
90
+ const volatileOnly = buildTiers({
91
+ ...opts,
92
+ // Pass empty strings for stable/context fields so buildTiers only builds volatile
93
+ botName: '',
94
+ soulSummary: '',
95
+ skillsIndexBlock: '',
96
+ crossSessionContext: '',
97
+ projectRules: '',
98
+ });
99
+ return assemble(_cachedStable, _cachedContext, volatileOnly.volatile);
100
+ }
101
+
102
+ // ── Tool Guardrails instance (Hermes-style) ─────────────────────────────
103
+ const guardrails = new ToolGuardrails();
104
+ const noopCallback = () => {};
105
+
106
+ // CLI komutları (REPL içinden çalıştırılabilir)
107
+ const CLI_COMMANDS = {
108
+ '/doctor': { desc: 'Sistem sağlığı kontrolü', run: ['doctor'] },
109
+ '/cost': { desc: 'Maliyet takibi (today|week|month|budget)', run: ['cost', 'today'] },
110
+ '/audit': { desc: 'Audit log (today|stats|files)', run: ['audit', 'stats'] },
111
+ '/team': { desc: 'Multi-agent (list|status)', run: ['team', 'list'] },
112
+ '/xp': { desc: 'XP/Level durumu', run: ['xp'] },
113
+ '/skills': { desc: 'Yüklü skill listesi', run: ['skills', 'list'] },
114
+ '/status': { desc: 'Sistem durumu', run: ['status'] },
115
+ '/mcp': { desc: 'MCP sunucuları', run: ['mcp', 'list'] },
116
+ '/channels': { desc: 'Bağlı kanallar', run: ['channels'] },
117
+ '/crons': { desc: 'Cron görevleri', run: ['cron', 'list'] },
118
+ '/bots': { desc: 'Bot listesi', run: ['bots'] },
119
+ '/models': { desc: 'Modeller', run: ['models', 'list'] },
120
+ '/memory-ls': { desc: 'Memory dosyaları', run: ['memory', 'list'] },
121
+ '/seo': { desc: 'SEO denetimi (URL gerek)', needsArg: true, run: ['seo', 'audit'] },
122
+ '/naturehub': { desc: 'NatureHub işlemleri (post|dm|inbox|forum|list)', needsArg: true, run: ['naturehub', 'post'] },
123
+ '/dashboard': { desc: 'Web dashboard başlat (port 7421)', run: ['dashboard', 'start'] },
124
+ };
125
+
126
+ const MEMORY_DIR = path.join(os.homedir(), '.natureco', 'memory');
127
+ const SESSION_DIR = path.join(os.homedir(), '.natureco', 'sessions');
128
+ const SESSIONS_INDEX = path.join(os.homedir(), '.natureco', 'sessions.json');
129
+ const REPL_STATE = path.join(os.homedir(), '.natureco', 'repl-state.json');
130
+
131
+ function ensureDir(dir) {
132
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
133
+ }
134
+
135
+ function getConfig() {
136
+ try {
137
+ return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.natureco', 'config.json'), 'utf8'));
138
+ } catch { return {}; }
139
+ }
140
+
141
+ function isMiniMax(url) {
142
+ return url && (url.includes('minimax.io') || url.includes('minimaxi.com') || url.includes('minimax.cn'));
143
+ }
144
+ function isGemini(url) {
145
+ return url && (url.includes('generativelanguage.googleapis.com') || url.includes('gemini'));
146
+ }
147
+
148
+ function loadMemory(username) {
149
+ const file = path.join(MEMORY_DIR, `${(username || 'default').toLowerCase()}.json`);
150
+ try {
151
+ if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8'));
152
+ } catch {}
153
+ return { name: username || 'Kullanıcı', nickname: null, botName: 'Asistan', facts: [], preferences: [], history: [] };
154
+ }
155
+
156
+ function saveMemory(username, memory) {
157
+ ensureDir(MEMORY_DIR);
158
+ const file = path.join(MEMORY_DIR, `${(username || 'default').toLowerCase()}.json`);
159
+ memory.lastUpdated = new Date().toISOString();
160
+ fs.writeFileSync(file, JSON.stringify(memory, null, 2));
161
+ return file;
162
+ }
163
+
164
+ function loadSessionsIndex() {
165
+ try {
166
+ if (fs.existsSync(SESSIONS_INDEX)) return JSON.parse(fs.readFileSync(SESSIONS_INDEX, 'utf8'));
167
+ } catch {}
168
+ return { sessions: [] };
169
+ }
170
+
171
+ function saveSessionsIndex(index) {
172
+ fs.writeFileSync(SESSIONS_INDEX, JSON.stringify(index, null, 2));
173
+ }
174
+
175
+ function saveSession(messages, meta) {
176
+ ensureDir(SESSION_DIR);
177
+ const id = `sess-${Date.now().toString(36)}`;
178
+ const file = path.join(SESSION_DIR, `${id}.json`);
179
+ const data = {
180
+ id,
181
+ createdAt: new Date().toISOString(),
182
+ updatedAt: new Date().toISOString(),
183
+ messageCount: messages.length,
184
+ ...meta,
185
+ messages: messages.filter(m => !m._internal),
186
+ };
187
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
188
+ // Index güncelle
189
+ const idx = loadSessionsIndex();
190
+ idx.sessions.unshift({
191
+ id, file, createdAt: data.createdAt, messageCount: messages.length,
192
+ firstUserMessage: messages.find(m => m.role === 'user')?.content?.slice(0, 60) || '(boş)',
193
+ });
194
+ // Son 50 session tut
195
+ idx.sessions = idx.sessions.slice(0, 50);
196
+ saveSessionsIndex(idx);
197
+ return id;
198
+ }
199
+
200
+
201
+
202
+ function loadSession(id) {
203
+ // ID veya index
204
+ const idx = loadSessionsIndex();
205
+ let meta;
206
+ if (id === 'last' || id === 'latest') {
207
+ meta = idx.sessions[0];
208
+ } else {
209
+ meta = idx.sessions.find(s => s.id === id || s.id.endsWith(id));
210
+ }
211
+ if (!meta) return null;
212
+ try {
213
+ return JSON.parse(fs.readFileSync(meta.file, 'utf8'));
214
+ } catch { return null; }
215
+ }
216
+
217
+ function extractFacts(messages, currentFacts) {
218
+ // Basit fact extraction: Türkçe/İngilizce pattern'lerle kullanıcı hakkında bilgi çıkar
219
+ // Production'da LLM ile yapılabilir, şimdilik pattern matching
220
+ const newFacts = [];
221
+ const userMessages = messages.filter(m => m.role === 'user' && !m._internal);
222
+ const existingValues = new Set((currentFacts || []).map(f => (f.value || f).toLowerCase()));
223
+
224
+ for (const msg of userMessages) {
225
+ const text = msg.content || '';
226
+ const lower = text.toLowerCase();
227
+
228
+ // İsim/tercih pattern'leri
229
+ const patterns = [
230
+ { re: /(?:benim ad[ıi]m|ad[ıi]m|ad[ıi]n|ad[ıi]n[ıi]z|ismim|isim|I'm called|my name is)\s+([A-ZÇĞİÖŞÜa-zçğıöşü]+)/i, val: m => `Adı: ${m[1]}` },
231
+ { re: /(?:yaşıyorum|yaşadığım|oturuyorum|I live in)\s+([A-ZÇĞİÖŞÜa-zçğıöşü\s]+)/i, val: m => `Yaşadığı yer: ${m[1].trim()}` },
232
+ { re: /([A-ZÇĞİÖŞÜa-zçğıöşü]{2,})[''](?:da|de|ta|te)\s+(?:yaşıyorum|yaşadığım|oturuyorum|bulunuyorum|kalıyorum)/i, val: m => `Yaşadığı yer: ${m[1]}` },
233
+ { re: /(?:seviyorum|severim|sevdiğim|hoşlanırım|beğenirim|like|love)\s+(?:şu|bu)?\s*([a-zA-ZçğıöşüÇĞİÖŞÜ\s]{2,30})/i, val: m => `Sevdiği şey: ${m[1].trim()}` },
234
+ { re: /(?:çalışıyorum|işim|mesleğim|çalışırım|I work as)\s+([A-ZÇĞİÖŞÜa-zçğıöşü\s]+)/i, val: m => `Meslek: ${m[1].trim()}` },
235
+ { re: /(\d{1,3})\s*(?:yaş[ıi]nday[ıi]m|yaş[ıi]nda)/i, val: m => `Yaş: ${m[1]}` },
236
+ ];
237
+
238
+ for (const p of patterns) {
239
+ const m = text.match(p.re);
240
+ if (m && m[1]) {
241
+ const val = p.val(m);
242
+ if (val && !existingValues.has(val.toLowerCase())) {
243
+ newFacts.push({ value: val, score: 5, learnedAt: new Date().toISOString() });
244
+ existingValues.add(val.toLowerCase());
245
+ }
246
+ }
247
+ }
248
+ }
249
+ return newFacts;
250
+ }
251
+
252
+ function apiRequest(providerUrl, providerApiKey, body, stream = false, retries = 3) {
253
+ return new Promise((resolve, reject) => {
254
+ const isMM = isMiniMax(providerUrl);
255
+ const endpoint = isMM
256
+ ? `${providerUrl.replace(/\/+$/, '')}/v1/text/chatcompletion_v2`
257
+ : isGemini(providerUrl)
258
+ ? `${providerUrl.replace(/\/+$/, '')}/openai/chat/completions`
259
+ : `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
260
+ const doRequest = (attempt) => {
261
+ const req = https.request(endpoint, {
262
+ method: 'POST',
263
+ headers: {
264
+ 'Authorization': `Bearer ${providerApiKey}`,
265
+ 'Content-Type': 'application/json',
266
+ },
267
+ timeout: 60000,
268
+ }, (res) => {
269
+ if (stream) { resolve(res); return; }
270
+ let data = '';
271
+ res.on('data', c => data += c);
272
+ res.on('end', () => {
273
+ if (res.statusCode >= 200 && res.statusCode < 300) {
274
+ try { resolve(JSON.parse(data)); } catch (e) { reject(new Error('Parse hatası')); }
275
+ } else if (res.statusCode === 429 && attempt < retries) {
276
+ const delay = Math.pow(2, attempt) * 1000;
277
+ setTimeout(() => doRequest(attempt + 1), delay);
278
+ } else {
279
+ const msg = res.statusCode === 429
280
+ ? 'HTTP 429: API rate limit aşıldı. Lütfen bekleyin veya planınızı yükseltin.'
281
+ : `HTTP ${res.statusCode}: ${data.slice(0, 200)}`;
282
+ reject(new Error(msg));
283
+ }
284
+ });
285
+ });
286
+ req.on('error', reject);
287
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
288
+ req.write(JSON.stringify(body));
289
+ req.end();
290
+ };
291
+ doRequest(0);
292
+ });
293
+ }
294
+
295
+ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChunk, onToolCall) {
296
+ const isMM = isMiniMax(providerUrl);
297
+ const isGM = isGemini(providerUrl);
298
+ const planMode = getPlanMode();
299
+
300
+ // Simple stdin prompt fallback (no rl dependency)
301
+ function stdinPrompt(question, cb) {
302
+ process.stdout.write(question);
303
+ const stdin = process.stdin;
304
+ const onData = (data) => {
305
+ stdin.removeListener('data', onData);
306
+ stdin.pause();
307
+ cb(data.toString().trim());
308
+ };
309
+ stdin.resume();
310
+ stdin.on('data', onData);
311
+ }
312
+
313
+ const toolDefs = getToolDefs();
314
+ // Inject plan mode virtual tools
315
+ const planToolDefs = [
316
+ {
317
+ name: 'EnterPlanMode',
318
+ description: 'Switch to plan-only mode. Research, explore, and create a detailed plan without making changes. Use ExitPlanMode when ready.',
319
+ parameters: { type: 'object', properties: {}, required: [] },
320
+ execute: async () => {
321
+ if (planMode.enter()) return { result: 'Plan mode activated. You can now research and plan. No changes will be made. Use ExitPlanMode when your plan is ready.' };
322
+ return { result: 'Already in plan mode.' };
323
+ },
324
+ },
325
+ {
326
+ name: 'ExitPlanMode',
327
+ description: 'Exit plan mode and present your plan for review. The plan must include steps, files to modify, and expected changes.',
328
+ parameters: {
329
+ type: 'object',
330
+ properties: {
331
+ plan: { type: 'string', description: 'Markdown plan with ## Plan title, ### Step N sections listing files and changes' },
332
+ summary: { type: 'string', description: 'One-sentence summary of the plan' },
333
+ },
334
+ required: ['plan'],
335
+ },
336
+ execute: async (args) => {
337
+ const ok = planMode.exit(args.plan);
338
+ if (!ok) return { error: 'Not in plan mode.' };
339
+ return {
340
+ result: `Plan submitted for review.\n\n${args.plan}`,
341
+ _plan_summary: args.summary || '',
342
+ };
343
+ },
344
+ },
345
+ ];
346
+ toolDefs.push(...planToolDefs);
347
+
348
+ // Inject worktree virtual tools
349
+ const wt = getWorktree();
350
+ const worktreeToolDefs = [
351
+ {
352
+ name: 'EnterWorktree',
353
+ description: 'Create an isolated worktree for experimental changes without affecting the main project. Use ExitWorktree to merge changes back.',
354
+ parameters: {
355
+ type: 'object',
356
+ properties: {
357
+ branch: { type: 'string', description: 'Optional branch name for the worktree' },
358
+ },
359
+ },
360
+ execute: async (args) => wt.enter(args),
361
+ },
362
+ {
363
+ name: 'ExitWorktree',
364
+ description: 'Exit the current worktree and merge changes back to the main branch.',
365
+ parameters: {
366
+ type: 'object',
367
+ properties: {
368
+ merge: { type: 'boolean', description: 'Merge changes back (default: true)' },
369
+ },
370
+ },
371
+ execute: async (args) => wt.exit(args),
372
+ },
373
+ ];
374
+ toolDefs.push(...worktreeToolDefs);
375
+
376
+ // Inject tool-level virtual tools
377
+ const { getTaskManager } = require('../utils/tasks');
378
+ const taskMgr = getTaskManager();
379
+ const toolVirtualTools = [
380
+ {
381
+ name: 'CreateTask',
382
+ description: 'Run a command in the background. Returns immediately with a task ID. Use GetTaskResult or ListTasks to check status.',
383
+ parameters: {
384
+ type: 'object',
385
+ properties: {
386
+ command: { type: 'string', description: 'Shell command to run' },
387
+ timeout: { type: 'number', description: 'Timeout in ms (default: 300000)' },
388
+ },
389
+ required: ['command'],
390
+ },
391
+ execute: async (args) => taskMgr.create(args.command, args),
392
+ },
393
+ {
394
+ name: 'ListTasks',
395
+ description: 'List all background tasks with their status.',
396
+ parameters: { type: 'object', properties: {}, required: [] },
397
+ execute: async () => ({ tasks: taskMgr.list() }),
398
+ },
399
+ {
400
+ name: 'GetTaskResult',
401
+ description: 'Get the full result (stdout/stderr) of a completed or running task.',
402
+ parameters: {
403
+ type: 'object',
404
+ properties: { taskId: { type: 'string', description: 'Task ID' } },
405
+ required: ['taskId'],
406
+ },
407
+ execute: async (args) => {
408
+ const task = taskMgr.get(args.taskId);
409
+ if (!task) return { error: 'Task not found' };
410
+ return { result: task.stdout, error: task.stderr, status: task.status, exitCode: task.exitCode };
411
+ },
412
+ },
413
+ {
414
+ name: 'StopTask',
415
+ description: 'Stop a running background task.',
416
+ parameters: {
417
+ type: 'object',
418
+ properties: { taskId: { type: 'string' } },
419
+ required: ['taskId'],
420
+ },
421
+ execute: async (args) => taskMgr.stop(args.taskId),
422
+ },
423
+ {
424
+ name: 'SearchSessions',
425
+ description: 'Search past conversation sessions for information. Useful for finding previously discussed topics or decisions.',
426
+ parameters: {
427
+ type: 'object',
428
+ properties: { query: { type: 'string', description: 'Search query' } },
429
+ required: ['query'],
430
+ },
431
+ execute: async (args) => {
432
+ const { search } = require('../utils/session-search');
433
+ return { results: search(args.query, 5) };
434
+ },
435
+ },
436
+ {
437
+ name: 'RestoreFile',
438
+ description: 'Restore a file from its snapshot history. Use FileHistory first to list available snapshots.',
439
+ parameters: {
440
+ type: 'object',
441
+ properties: {
442
+ filePath: { type: 'string', description: 'File path to restore' },
443
+ timestamp: { type: 'number', description: 'Snapshot timestamp (from FileHistory). If omitted, restores previous version.' },
444
+ },
445
+ required: ['filePath'],
446
+ },
447
+ execute: async (args) => {
448
+ const fh = require('../utils/file-history');
449
+ const snaps = fh.getHistory(args.filePath);
450
+ if (snaps.length === 0) return { error: 'No history for this file' };
451
+ const ts = args.timestamp || snaps[0].timestamp;
452
+ return fh.restore(args.filePath, ts);
453
+ },
454
+ },
455
+ {
456
+ name: 'FileHistory',
457
+ description: 'List snapshot history for a file, showing timestamps and sizes of previous versions.',
458
+ parameters: {
459
+ type: 'object',
460
+ properties: { filePath: { type: 'string' } },
461
+ required: ['filePath'],
462
+ },
463
+ execute: async (args) => {
464
+ const fh = require('../utils/file-history');
465
+ return { snapshots: fh.getHistory(args.filePath) };
466
+ },
467
+ },
468
+ {
469
+ name: 'UltraReview',
470
+ description: 'Run a multi-focus code review (security, style, logic, performance) on specified files or git diff.',
471
+ parameters: {
472
+ type: 'object',
473
+ properties: {
474
+ files: { type: 'array', items: { type: 'string' }, description: 'File paths to review' },
475
+ diff: { type: 'string', description: 'Git diff content to review instead of files' },
476
+ },
477
+ },
478
+ execute: async (args) => {
479
+ const ur = require('../utils/ultra-review');
480
+ if (args.diff) return ur.reviewDiff(args.diff);
481
+ if (args.files) {
482
+ const fs = require('fs');
483
+ return {
484
+ reviews: args.files.map(f => {
485
+ const content = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
486
+ return ur.reviewFile(f, content);
487
+ }),
488
+ };
489
+ }
490
+ return { error: 'Specify files or diff' };
491
+ },
492
+ },
493
+ {
494
+ name: 'ScheduleTask',
495
+ description: 'Schedule a recurring task using cron expressions. E.g., "*/5 * * * *" for every 5 min.',
496
+ parameters: {
497
+ type: 'object',
498
+ properties: {
499
+ schedule: { type: 'string', description: 'Cron expression: min hour dom mon dow' },
500
+ command: { type: 'string', description: 'Command to run' },
501
+ description: { type: 'string', description: 'Optional description' },
502
+ },
503
+ required: ['schedule', 'command'],
504
+ },
505
+ execute: async (args) => require('../utils/cron').addJob(args),
506
+ },
507
+ {
508
+ name: 'ListScheduledTasks',
509
+ description: 'List all scheduled cron tasks.',
510
+ parameters: { type: 'object', properties: {} },
511
+ execute: async () => ({ jobs: require('../utils/cron').loadJobs() }),
512
+ },
513
+ {
514
+ name: 'RemoveScheduledTask',
515
+ description: 'Remove a scheduled task by ID.',
516
+ parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
517
+ execute: async (args) => {
518
+ require('../utils/cron').removeJob(args.id);
519
+ return { removed: true };
520
+ },
521
+ },
522
+ ];
523
+ toolDefs.push(...toolVirtualTools);
524
+
525
+ const toolParam = toOpenAIFormat(toolDefs);
526
+ guardrails.reset();
527
+
528
+ let currentMessages = messages;
529
+ let fullText = '';
530
+ let iterations = 0;
531
+ const MAX_TOOL_ITERATIONS = 50;
532
+ const MAX_CONTEXT_TOKENS = 32000; // safety limit before compression
533
+
534
+ // v5.7.18: Preflight compression — if context too long, compress middle messages
535
+ // (like Hermes' turn_context.py preflight)
536
+ function preflightCompress(msgs) {
537
+ const roughTokens = msgs.reduce((s, m) => s + Math.ceil(((m.content || '') + (m.role || '')).length / 4), 0);
538
+ if (roughTokens <= MAX_CONTEXT_TOKENS || msgs.length < 6) return msgs;
539
+
540
+ // Keep system prompt (first message) + last N turns (tail), compress middle
541
+ const sysMsg = msgs[0] && msgs[0].role === 'system' ? msgs[0] : null;
542
+ const tailCount = Math.min(6, Math.floor(msgs.length / 2));
543
+ const startIdx = sysMsg ? 1 : 0;
544
+ const tailStart = msgs.length - tailCount;
545
+
546
+ const middle = msgs.slice(startIdx, tailStart);
547
+ if (middle.length < 2) return msgs;
548
+
549
+ // Summarize middle section
550
+ const summary = '[' + middle.length + ' onceki mesaj ozetlendi]';
551
+ const compressed = sysMsg ? [sysMsg] : [];
552
+ compressed.push({ role: 'system', content: 'Gecmis konusma ozeti: ' + summary, _compressed: true });
553
+ compressed.push(...msgs.slice(tailStart));
554
+ return compressed;
555
+ }
556
+
557
+ // Apply preflight on entry
558
+ currentMessages = preflightCompress(currentMessages);
559
+
560
+ while (iterations < 50) {
561
+ let effortLevel = 'medium', effortCfg;
562
+ let cfg;
563
+ try {
564
+ cfg = require('../utils/config').getConfig();
565
+ effortLevel = getEffortLevel(cfg);
566
+ effortCfg = getEffortConfig(effortLevel);
567
+ } catch { effortCfg = getEffortConfig(effortLevel); }
568
+ const maxIter = effortCfg ? effortCfg.maxToolIterations : 50;
569
+ iterations++;
570
+ // v5.7.18: Preflight compress before each iteration to prevent context bloat
571
+ currentMessages = preflightCompress(currentMessages);
572
+ const shouldStream = !isMM && !isGM; // MiniMax + Gemini non-stream (tool_calls reliability)
573
+ // Inject plan mode prompt into system message
574
+ const planModePrompt = planMode.getSystemPrompt();
575
+ if (planModePrompt) {
576
+ const sysIdx = currentMessages.findIndex(m => m.role === 'system');
577
+ if (sysIdx >= 0) {
578
+ const base = currentMessages[sysIdx].content;
579
+ if (!base.endsWith(planModePrompt)) {
580
+ currentMessages[sysIdx] = { ...currentMessages[sysIdx], content: base + '\n\n' + planModePrompt };
581
+ }
582
+ }
583
+ } else {
584
+ // Clean up any stale plan mode prompt from previous iterations
585
+ const sysIdx = currentMessages.findIndex(m => m.role === 'system');
586
+ if (sysIdx >= 0) {
587
+ const base = currentMessages[sysIdx].content;
588
+ const marker = '\n\nYou are in PLAN MODE.';
589
+ if (base.includes(marker)) {
590
+ currentMessages[sysIdx] = { ...currentMessages[sysIdx], content: base.split('\n\nYou are in PLAN MODE.')[0] };
591
+ }
592
+ }
593
+ }
594
+
595
+ const fallbackChain = getFallbackChain();
596
+
597
+ const body = {
598
+ model: fallbackChain.current || model,
599
+ messages: currentMessages,
600
+ stream: shouldStream,
601
+ temperature: effortCfg.temperature,
602
+ max_tokens: effortCfg.maxTokens,
603
+ };
604
+ // Structured output support
605
+ const respFmt = getResponseFormat(cfg);
606
+ if (respFmt) body.response_format = respFmt;
607
+ if (toolParam) body.tools = toolParam;
608
+ if (isMM || isGM) body.tool_choice = 'auto'; // MiniMax + Gemini için explicit
609
+
610
+ if (!shouldStream) {
611
+ // MiniMax (non-stream) — tool_calls desteklemiyor varsayalım
612
+ const res = await apiRequest(providerUrl, providerApiKey, body, false);
613
+ const msg = res.choices?.[0]?.message || {};
614
+ const content = msg.content || '';
615
+ if (onChunk !== noopCallback) {
616
+ for (const char of content) {
617
+ onChunk(char);
618
+ await new Promise(r => setTimeout(r, 8));
619
+ }
620
+ }
621
+ fullText = content;
622
+ // Non-stream tool call desteği
623
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
624
+ const toolResults = await processToolCalls(msg.tool_calls, onToolCall, stdinPrompt);
625
+ currentMessages.push(msg);
626
+ currentMessages.push(...toolResults);
627
+ continue; // Tekrar API çağır
628
+ }
629
+ break;
630
+ }
631
+
632
+ // OpenAI uyumlu streaming (veya MiniMax /v1/text/chatcompletion_v2)
633
+ // v5.9.5: Gemini /openai/chat/completions — provider-detect.js buildChatEndpoint
634
+ const endpoint = isMM
635
+ ? `${providerUrl.replace(/\/+$/, '')}/v1/text/chatcompletion_v2`
636
+ : isGemini(providerUrl)
637
+ ? `${providerUrl.replace(/\/+$/, '')}/openai/chat/completions`
638
+ : `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
639
+ let result;
640
+ try {
641
+ result = await new Promise((resolve, reject) => {
642
+ const req = https.request(endpoint, {
643
+ method: 'POST',
644
+ headers: { 'Authorization': `Bearer ${providerApiKey}`, 'Content-Type': 'application/json' },
645
+ timeout: 60000,
646
+ }, (res) => {
647
+ if (res.statusCode !== 200) {
648
+ let data = '';
649
+ res.on('data', c => data += c);
650
+ res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
651
+ return;
652
+ }
653
+ let buffer = '';
654
+ let streamText = '';
655
+ const toolCalls = []; // { index, id, name, args }
656
+ res.on('data', (chunk) => {
657
+ buffer += chunk.toString('utf8');
658
+ const lines = buffer.split('\n');
659
+ buffer = lines.pop() || '';
660
+ for (const line of lines) {
661
+ const trimmed = line.trim();
662
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
663
+ const data = trimmed.slice(5).trim();
664
+ if (data === '[DONE]') {
665
+ resolve({ streamText, toolCalls });
666
+ return;
667
+ }
668
+ try {
669
+ const parsed = JSON.parse(data);
670
+ const choice = parsed.choices?.[0];
671
+ if (!choice) continue;
672
+ const delta = choice.delta;
673
+
674
+ // Text content
675
+ if (delta.content) {
676
+ streamText += delta.content;
677
+ onChunk(delta.content);
678
+ }
679
+
680
+ // Tool calls (streaming delta) — shared accumulator,
681
+ // see src/utils/streaming-tools.js for the per-index pattern.
682
+ if (delta.tool_calls) {
683
+ accumulateToolCallDeltas(toolCalls, delta.tool_calls);
684
+ }
685
+ } catch {}
686
+ }
687
+ });
688
+ res.on('end', () => resolve({ streamText, toolCalls }));
689
+ });
690
+ req.on('error', reject);
691
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
692
+ req.write(JSON.stringify(body));
693
+ req.end();
694
+ });
695
+ } catch (err) {
696
+ // Fallback chain: try next model on error
697
+ const fb = fallbackChain.recordError(body.model, err);
698
+ if (fb.fallback) {
699
+ console.log(tui.C.yellow(`\n ⚠ ${body.model} başarısız → ${fb.nextModel} deneniyor...\n`));
700
+ continue; // Retry with next model
701
+ }
702
+ throw err;
703
+ }
704
+
705
+ fullText = result.streamText;
706
+
707
+ // Tool call var mı? finalizeToolCalls drops empty entries + synthesizes ids,
708
+ // so we only need to check the resulting length.
709
+ const finalized = finalizeToolCalls(result.toolCalls);
710
+ if (finalized.length > 0) {
711
+ // Assistant mesajını ekle (tool_calls ile)
712
+ currentMessages.push({
713
+ role: 'assistant',
714
+ content: result.streamText || null,
715
+ tool_calls: finalized,
716
+ });
717
+ // Her tool call'ı çalıştır, sonuçları tool mesajı olarak ekle
718
+ const toolResults = await processToolCalls(finalized, onToolCall, stdinPrompt);
719
+ currentMessages.push(...toolResults);
720
+
721
+ // Plan mode review: plan submitted, wait for user approval
722
+ if (planMode.inReview()) {
723
+ const { plan } = planMode.planHistory[planMode.planHistory.length - 1] || {};
724
+ console.log('\n' + tui.C.cyan(' 📋 Plan sunuldu onay bekleniyor...'));
725
+ console.log(tui.C.muted(' ' + ''.repeat(56)));
726
+ console.log(plan ? `\n ${plan.replace(/\n/g, '\n ')}` : '');
727
+ console.log('\n' + tui.C.muted(' ─'.repeat(28)));
728
+ const approved = await new Promise(resolve => {
729
+ stdinPrompt(tui.C.yellow(' Planı onaylıyor musun? [Y=exec, n=reddet, e=düzenle]: '), answer => {
730
+ const key = answer.trim().toLowerCase();
731
+ if (key === 'n' || key === 'no') { planMode.reject(); resolve(false); }
732
+ else if (key === 'e' || key === 'edit') { planMode.reject(); resolve('edit'); }
733
+ else { planMode.approve(); resolve(true); }
734
+ });
735
+ });
736
+ if (approved === true) {
737
+ console.log(tui.C.green(' ✓ Plan onaylandı. Plan uygulanıyor...\n'));
738
+ // Devam model cevap versin
739
+ } else if (approved === 'edit') {
740
+ console.log(tui.C.yellow(' 📝 Planı düzenleyin ve /plan ile yeniden gönderin.\n'));
741
+ // Plan modunda kal, mesaj ekle
742
+ currentMessages.push({ role: 'user', content: 'Planı düzenle ve yeniden sun.' });
743
+ continue;
744
+ } else {
745
+ console.log(tui.C.amber(' ⨯ Plan reddedildi. Yeniden plan yapılıyor...\n'));
746
+ currentMessages.push({ role: 'user', content: 'Plan reddedildi. Lütfen farklı bir yaklaşım dene.' });
747
+ continue;
748
+ }
749
+ }
750
+
751
+ // Devam — model sonuçları görsün, cevap versin
752
+ continue;
753
+ }
754
+
755
+ break; // Tool call yok, çık
756
+ }
757
+
758
+ return fullText;
759
+ }
760
+
761
+ /**
762
+ * Tool call'ları çalıştır, sonuçları OpenAI uyumlu tool mesajlarına dönüştür.
763
+ * v5.7.18: Concurrent execution for parallel-safe tools + untrusted result wrapping.
764
+ */
765
+ const UNTRUSTED_TOOLS = new Set(['browser', 'web_search', 'duckduckgo_search', 'searxng_search', 'exa_search', 'firecrawl', 'web_readability']);
766
+ const PARALLEL_SAFE_TOOLS = new Set(['read_file', 'file_search', 'grep_search', 'web_search', 'web_readability', 'duckduckgo_search', 'exa_search', 'searxng_search', 'firecrawl', 'memory_search', 'memory']);
767
+ const { checkPreHooks, runPostHooks, permissionSummary } = require('../utils/tool-hooks');
768
+ const { checkPermission, isApproved, markApproved, formatPermissionPrompt } = require('../utils/permissions');
769
+ const { getPlanMode } = require('../utils/plan-mode');
770
+ const { getWorktree } = require('../utils/worktree');
771
+ const { getLevel: getEffortLevel, getConfig: getEffortConfig } = require('../utils/effort-levels');
772
+ const { getResponseFormat, hasStructuredOutput } = require('../utils/structured-output');
773
+ const { getFallbackChain } = require('../utils/fallback-chain');
774
+
775
+ /**
776
+ * Prompt user for permission approval.
777
+ * Returns: true (once), 'session', 'persistent', or false (denied).
778
+ */
779
+ async function askPermissionPrompt(question, hint, prompter) {
780
+ const full = `${tui.C.yellow('')} ${tui.C.bold('İzin gerekiyor')}: ${question}\n${tui.C.muted(hint)}`;
781
+ return new Promise(resolve => {
782
+ prompter(full, answer => {
783
+ const key = answer.trim();
784
+ if (key === 'y') resolve(true); // once
785
+ else if (key === 'Y') resolve('session'); // session
786
+ else if (key === 'p') resolve('persistent'); // disk
787
+ else if (key === 'a') { _permSessionCache.set('__ALL__', true); resolve(true); } // all (legacy)
788
+ else resolve(false); // no
789
+ });
790
+ });
791
+ }
792
+
793
+ /** Session permission cache (for ask hooks) */
794
+ const _permSessionCache = new Map();
795
+
796
+
797
+ async function processToolCalls(toolCalls, onToolCall, onAsk) {
798
+ const toolDefs = getToolDefs();
799
+ const results = [];
800
+
801
+ // Parse all tool calls first
802
+ const parsed = toolCalls.map(tc => {
803
+ const name = tc.function?.name || tc.name;
804
+ const argsStr = tc.function?.arguments || tc.args || '{}';
805
+ const id = tc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
806
+ let args = {};
807
+ try {
808
+ args = typeof argsStr === 'string' ? JSON.parse(argsStr) : argsStr;
809
+ } catch (e) {
810
+ args = { _parse_error: e.message, _raw: argsStr };
811
+ }
812
+ return { name, args, id };
813
+ });
814
+
815
+ // Filter out blocked tools via guardrails
816
+ guardrails.startIteration();
817
+ const blocked = parsed.filter(p => {
818
+ const check = guardrails.check(p.name, p.args);
819
+ if (check.blocked) {
820
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: check.reason } });
821
+ results.push({ ...p, result: { error: check.reason }, _blocked: true });
822
+ return false;
823
+ }
824
+ return true;
825
+ });
826
+ const allowed = parsed.filter(p => !results.some(r => r.id === p.id && r._blocked));
827
+
828
+ // Separate parallel-safe and sequential tools (over allowed only)
829
+ const parallelBatch = allowed.filter(p => PARALLEL_SAFE_TOOLS.has(p.name));
830
+ const sequentialBatch = allowed.filter(p => !PARALLEL_SAFE_TOOLS.has(p.name));
831
+
832
+ // Notify UI for all non-blocked
833
+ for (const p of allowed) {
834
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'running' });
835
+ }
836
+
837
+ // Shared tool execution with: planCheck permissions hooks execute post-hooks
838
+ async function executeOne(p) {
839
+ // 0. Plan mode check
840
+ const pm = getPlanMode();
841
+ const planCheck = pm.checkTool(p.name, p.args);
842
+ if (!planCheck.allowed) {
843
+ const denied = { ...p, result: { error: planCheck.reason }, _plan_blocked: true };
844
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: planCheck.reason } });
845
+ return denied;
846
+ }
847
+ pm.recordTool(p.name, p.args);
848
+
849
+ // 1. Permission check (config-based granular rules)
850
+ const perm = checkPermission(p.name, p.args);
851
+ if (perm.action === 'deny') {
852
+ const denied = { ...p, result: { error: perm.reason }, _perm_blocked: true };
853
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: perm.reason } });
854
+ return denied;
855
+ }
856
+ if (perm.action === 'ask') {
857
+ const permKey = `${perm.rule.raw}:${JSON.stringify(p.args)}`;
858
+ if (!isApproved(permKey)) {
859
+ const ok = await askPermissionPrompt(
860
+ `${formatPermissionPrompt(p.name, p.args, perm.reason)}\n ${perm.reason}`,
861
+ `Bu işleme izin ver? [y=once, Y=session, p=persistent, n=no] `,
862
+ onAsk
863
+ );
864
+ if (ok === 'persistent') markApproved(permKey, true);
865
+ else if (ok === 'session') markApproved(permKey, false);
866
+ else if (!ok) {
867
+ const denied = { ...p, result: { error: `İzin reddedildi: ${perm.rule.raw}` }, _perm_blocked: true };
868
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: `İzin reddedildi: ${perm.rule.raw}` } });
869
+ return denied;
870
+ }
871
+ }
872
+ }
873
+
874
+ // 2. Pre-hook check
875
+ const hook = checkPreHooks(p.name, p.args);
876
+ if (hook.action === 'deny') {
877
+ const denied = { ...p, result: { error: hook.reason }, _hook_blocked: true };
878
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: hook.reason } });
879
+ return denied;
880
+ }
881
+ if (hook.action === 'ask') {
882
+ const ok = await askPermissionPrompt(
883
+ permissionSummary(hook.rule, p.name, p.args),
884
+ `Hook onayı [y=once, Y=session, n=no]: `,
885
+ onAsk
886
+ );
887
+ if (!ok) {
888
+ const denied = { ...p, result: { error: `Hook reddetti: ${hook.rule.raw}` }, _hook_blocked: true };
889
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: `Hook reddetti: ${hook.rule.raw}` } });
890
+ return denied;
891
+ }
892
+ }
893
+
894
+ const result = await executeTool(p.name, p.args, toolDefs);
895
+ const success = result?.success !== false;
896
+ guardrails.record(p.name, p.args, success);
897
+ return { ...p, result: runPostHooks(p.name, p.args, result) };
898
+ }
899
+
900
+ // Run parallel batch concurrently
901
+ if (parallelBatch.length > 0) {
902
+ const parallelResults = await Promise.all(parallelBatch.map(executeOne));
903
+ results.push(...parallelResults);
904
+ }
905
+
906
+ // Run sequential batch one at a time
907
+ for (const p of sequentialBatch) {
908
+ const r = await executeOne(p);
909
+ results.push(r);
910
+ }
911
+
912
+ // No-progress check: if all tools failed, inject warning
913
+ if (guardrails.isNoProgress()) {
914
+ if (onToolCall) onToolCall({ name: '_no_progress', args: null, status: 'done', result: { error: 'All tools failed this iteration' } });
915
+ }
916
+
917
+ // Same-tool loop detection: if same tool called >3x this iteration (regardless of success)
918
+ const toolCallCounts = {};
919
+ for (const { name } of results) {
920
+ toolCallCounts[name] = (toolCallCounts[name] || 0) + 1;
921
+ }
922
+ for (const [name, count] of Object.entries(toolCallCounts)) {
923
+ if (count > 3) {
924
+ const loopResult = results.find(r => r.name === name);
925
+ if (loopResult && !loopResult.result?.error) {
926
+ const warnContent = JSON.stringify({
927
+ _loop_warning: true,
928
+ message: `${name} called ${count}x this turn. If you're not making progress, try a different approach or report the result.`,
929
+ tool: name, call_count: count,
930
+ });
931
+ results.push({ name: '_loop_warning', id: `loop_${Date.now()}`, result: { result: warnContent } });
932
+ }
933
+ }
934
+ }
935
+
936
+ // Notify UI done + build messages
937
+ // v5.9.7: Skip internal meta-tools (_loop_warning, _no_progress) from tool messages
938
+ // Inject loop warning as user message instead (Gemini requires real tool names)
939
+ // Gemini also requires 'name' field in tool response messages
940
+ const messages = [];
941
+ for (const { name, id, result } of results) {
942
+ if (onToolCall) onToolCall({ name, args: null, status: 'done', result });
943
+
944
+ if (name === '_loop_warning' || name === '_no_progress') {
945
+ if (name === '_loop_warning') {
946
+ const warnContent = typeof result?.result === 'string' ? result.result : '';
947
+ if (warnContent) messages.push({ role: 'user', content: '[System: ' + warnContent + ']' });
948
+ }
949
+ continue;
950
+ }
951
+
952
+ let content;
953
+ if (result.error) {
954
+ content = JSON.stringify({ error: result.error });
955
+ } else {
956
+ let raw = typeof result.result === 'string' ? result.result : JSON.stringify(result.result).slice(0, 8000);
957
+ // v5.7.18: Untrusted result wrapping (Hermes-style prompt injection defense)
958
+ if (UNTRUSTED_TOOLS.has(name)) {
959
+ content = `<untrusted_tool_result source="${name}">\n${raw}\n</untrusted_tool_result>`;
960
+ } else {
961
+ content = raw;
962
+ }
963
+ }
964
+
965
+ messages.push({ role: 'tool', tool_call_id: id, name, content });
966
+ }
967
+
968
+ return messages;
969
+ }
970
+
971
+ function printHelp() {
972
+ console.log(chalk.cyan('\n 📚 REPL Komutları:\n'));
973
+ console.log(' ' + chalk.yellow('/help'.padEnd(22)) + chalk.gray('Bu yardım'));
974
+ console.log(' ' + chalk.yellow('/clear'.padEnd(22)) + chalk.gray('Ekranı temizle'));
975
+ console.log(' ' + chalk.yellow('/history'.padEnd(22)) + chalk.gray('Bu oturumun geçmişi'));
976
+ console.log(' ' + chalk.yellow('/memory'.padEnd(22)) + chalk.gray('Memory\'i göster'));
977
+ console.log(' ' + chalk.yellow('/plan [on|off|show]'.padEnd(22)) + chalk.gray('Plan modu'));
978
+ console.log(' ' + chalk.yellow('/forget'.padEnd(22)) + chalk.gray('Memory\'i temizle'));
979
+ console.log(' ' + chalk.yellow('/sessions'.padEnd(22)) + chalk.gray('Geçmiş oturumları listele'));
980
+ console.log(' ' + chalk.yellow('/resume [id|last]'.padEnd(22)) + chalk.gray('Önceki oturuma dön'));
981
+ console.log(' ' + chalk.yellow('/system <text>'.padEnd(22)) + chalk.gray('System prompt değiştir'));
982
+ console.log(' ' + chalk.yellow('/model <name>'.padEnd(22)) + chalk.gray('Model değiştir'));
983
+ console.log(' ' + chalk.yellow('/identity [ad]'.padEnd(22)) + chalk.gray('Bot adını değiştir'));
984
+ console.log(' ' + chalk.yellow('/tokens'.padEnd(22)) + chalk.gray('Token kullanımı'));
985
+ console.log(' ' + chalk.yellow('/save'.padEnd(22)) + chalk.gray('Oturumu manuel kaydet'));
986
+ console.log(' ' + chalk.yellow('/exit veya /quit'.padEnd(22)) + chalk.gray('Çıkış (Ctrl+C de çalışır)'));
987
+ console.log(chalk.cyan('\n 🛠️ Tüm CLI Komutları (REPL içinden):\n'));
988
+ for (const [cmd, info] of Object.entries(CLI_COMMANDS)) {
989
+ console.log(' ' + chalk.yellow(cmd.padEnd(22)) + chalk.gray(info.desc));
990
+ }
991
+ console.log('');
992
+ }
993
+
994
+ function runCliCommand(args) {
995
+ return new Promise((resolve) => {
996
+ const proc = spawn('node', [path.join(__dirname, '..', '..', 'bin', 'natureco.js'), ...args], {
997
+ stdio: 'inherit',
998
+ });
999
+ proc.on('close', (code) => resolve(code));
1000
+ proc.on('error', (e) => { console.log(chalk.red(' Hata: ' + e.message)); resolve(1); });
1001
+ });
1002
+ }
1003
+
1004
+ async function startRepl(args) {
1005
+ ensureDir(MEMORY_DIR); ensureDir(SESSION_DIR);
1006
+
1007
+ const cfg = getConfig();
1008
+ let providerUrl = cfg.providerUrl;
1009
+ let providerApiKey = cfg.providerApiKey;
1010
+ let model = cfg.providerModel;
1011
+
1012
+ // Arg parse
1013
+ let resumeId = null;
1014
+ for (let i = 0; i < args.length; i++) {
1015
+ if (args[i] === '--model' && args[i + 1]) model = args[++i];
1016
+ if (args[i] === '--resume') resumeId = args[i + 1] || 'last';
1017
+ }
1018
+
1019
+ if (!providerUrl || !providerApiKey) {
1020
+ console.log(chalk.red('\n ❌ Provider ayarlı değil. Önce: natureco setup\n'));
1021
+ process.exit(1);
1022
+ }
1023
+
1024
+ // Memory yükle
1025
+ let memory = loadMemory(cfg.userName);
1026
+
1027
+ // v5.6.19: Oncelik config.botName, sonra memory.botName
1028
+ if (!memory.botName) {
1029
+ memory.botName = cfg.botName || 'Asistan';
1030
+ }
1031
+ // BotName'i memory'ye persist et (her oturumda ayni kalsin)
1032
+ try {
1033
+ const fs = require('fs');
1034
+ const memFile = path.join(os.homedir(), '.natureco', 'memory', ((cfg.userName || 'default').toLowerCase()) + '.json');
1035
+ if (fs.existsSync(memFile)) {
1036
+ const memData = JSON.parse(memFile, 'utf8');
1037
+ if (!memData.botName || memData.botName !== memory.botName) {
1038
+ memData.botName = memory.botName;
1039
+ fs.writeFileSync(memFile, JSON.stringify(memData, null, 2), 'utf8');
1040
+ }
1041
+ }
1042
+ } catch (e) {} // Sessizce devam et, kritik degil
1043
+
1044
+ // v5.4.11: Sasuke Brain - Cross-session context otomatik yukle
1045
+ // REPL acildiginda son oturumdan 1-2 context mesaji al
1046
+ let crossSessionContext = '';
1047
+ try {
1048
+ const { listSessions, loadSession } = require('../utils/sessions-helper');
1049
+ if (listSessions && loadSession) {
1050
+ const sessions = listSessions(1);
1051
+ if (sessions && sessions.length > 0) {
1052
+ const lastSession = loadSession(sessions[0].id);
1053
+ if (lastSession && lastSession.messages && lastSession.messages.length > 0) {
1054
+ // Son 2 user message
1055
+ const lastUserMsgs = lastSession.messages
1056
+ .filter(m => m.role === 'user')
1057
+ .slice(-2);
1058
+ if (lastUserMsgs.length > 0) {
1059
+ crossSessionContext = '\n[KONUSMA GECMISI: ' + sessions[0].id + ']\n' +
1060
+ lastUserMsgs.map(m => 'User: ' + (m.content || '').slice(0, 100)).join('\n') + '\n[/GECMISI]\n';
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ } catch (e) {
1066
+ // Cross-session yukleme basarisiz, devam et
1067
+ }
1068
+ // v5.4.12: 3 SOUL dosyasini birlestir (SOUL.md + IDENTITY.md + AGENTS.md)
1069
+ const { buildSoulContext, summarizeSoul } = require("../tools/soul");
1070
+ const soulSummary = buildSoulContext(); // 3 dosya birlesik ozet
1071
+
1072
+ // v5.7.14: Hermes-style memory store (MEMORY.md / USER.md) + skill index
1073
+ const memoryStore = getMemoryStore();
1074
+ memoryStore.load();
1075
+ const memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1076
+ const skillsIndexBlock = buildSkillIndex();
1077
+
1078
+ // Resume?
1079
+ let messages = [];
1080
+ if (resumeId) {
1081
+ const session = loadSession(resumeId);
1082
+ if (session) {
1083
+ messages = session.messages || [];
1084
+ console.log(chalk.green(`\n ✓ Oturum yüklendi: ${session.id} (${messages.length} mesaj)\n`));
1085
+ } else {
1086
+ console.log(chalk.yellow(`\n ⚠️ Oturum bulunamadı: ${resumeId}\n`));
1087
+ }
1088
+ }
1089
+
1090
+ // System prompt oluştur (memory + identity + persistent bağlam)
1091
+ // v5.6.5: Kucuk model tespiti (Groq, Mistral Small, Ollama) - SOUL injection skip
1092
+ const botName = memory.botName || 'Asistan';
1093
+ const userName = memory.name || memory.nickname || cfg.userName;
1094
+ const isSmallModel = (cfg.providerUrl || '').includes('groq.com') ||
1095
+ (cfg.providerUrl || '').includes('mistral.ai') ||
1096
+ (cfg.providerUrl || '').includes('localhost') ||
1097
+ (cfg.providerUrl || '').includes('ollama');
1098
+ // Discover project rules (CLAUDE.md)
1099
+ const projectRules = discoverProjectRules(process.cwd());
1100
+ if (projectRules) {
1101
+ console.log(chalk.cyan(` 📋 Proje kurallari bulundu (CLAUDE.md)\n`));
1102
+ }
1103
+
1104
+ // Build system prompt with tier caching (stable+context cached, volatile fresh)
1105
+ const promptOpts = {
1106
+ botName, userName, soulSummary, isSmallModel,
1107
+ memorySnapshotBlock, skillsIndexBlock, projectRules,
1108
+ crossSessionContext: crossSessionContext || '',
1109
+ userHome: cfg.userHome || '',
1110
+ hasHistory: messages.length > 0,
1111
+ memoryFacts: memory.facts || [],
1112
+ };
1113
+ let systemPrompt = rebuildSystemPrompt(promptOpts);
1114
+
1115
+ if (messages.length === 0) {
1116
+ messages.push({ role: 'system', content: systemPrompt, _internal: true });
1117
+ } else {
1118
+ // Resume: system prompt'u güncelle (memory değişmiş olabilir)
1119
+ const sysIdx = messages.findIndex(m => m._internal);
1120
+ if (sysIdx >= 0) messages[sysIdx] = { role: 'system', content: systemPrompt, _internal: true };
1121
+ else messages.unshift({ role: 'system', content: systemPrompt, _internal: true });
1122
+ }
1123
+
1124
+ // Header
1125
+ console.log('');
1126
+ console.log(tui.styled(' 🌿 NatureCo REPL · Persistent Sohbet', { color: tui.PALETTE.primary, bold: true }));
1127
+ console.log(tui.styled(' ' + '─'.repeat(56), { color: tui.PALETTE.border }));
1128
+ console.log(tui.C.muted(' Provider: ') + tui.C.brand(providerUrl.replace(/https?:\/\//, '')));
1129
+ console.log(tui.C.muted(' Model: ') + tui.C.brand(model));
1130
+ console.log(tui.C.muted(' Kullanıcı: ') + tui.C.brand((memory.nickname || cfg.userName) + (memory.nickname ? ` (${cfg.userName})` : '')));
1131
+ console.log(tui.C.muted(' Bot: ') + tui.C.brand(memory.botName || 'Asistan'));
1132
+ if (messages.length > 1) {
1133
+ console.log(tui.C.muted(' Oturum: ') + tui.C.amber(`${messages.filter(m => m.role === 'user' || m.role === 'assistant').length} mesaj (resume)`));
1134
+ }
1135
+ console.log(tui.C.muted(' Komutlar: ') + tui.C.yellow('/help') + tui.C.muted(' · ') + tui.C.yellow('/memory') + tui.C.muted(' · ') + tui.C.yellow('/sessions') + tui.C.muted(' · ') + tui.C.yellow('/exit'));
1136
+ console.log('');
1137
+ // v5.4.7: Hard-coded kimlik — v5.14.5: memory fact'lerinden kullanici adini tespit et
1138
+ const displayBotName = memory.botName || 'Asistan';
1139
+ const nameFromFact = (() => {
1140
+ const facts = memory.facts || [];
1141
+ for (const f of facts) {
1142
+ const v = (f.value || f || '').trim();
1143
+ const lv = v.toLowerCase();
1144
+ const match = lv.match(/(?:kullanici\s*adi?|kullanıcı\s*adı?|isim|name)\s*:?\s*(.+)/);
1145
+ if (match && match[1].trim().length > 2) return match[1].trim();
1146
+ }
1147
+ return null;
1148
+ })();
1149
+ const displayUserName = memory.name || nameFromFact || memory.nickname || cfg.userName;
1150
+ console.log(tui.C.brand(' 👋 Ben ' + displayBotName + ', ' + displayUserName + '. Sen nasilsin?'));
1151
+ console.log('');
1152
+
1153
+ // v5.4.14: SOUL'dan onemli bilgileri de goster
1154
+ if (soulSummary) {
1155
+ const soulPreview = soulSummary.split('\n').slice(0, 3).join(' ').substring(0, 200);
1156
+ if (soulPreview) {
1157
+ console.log(tui.C.muted(' 📜 ' + soulPreview + '...'));
1158
+ console.log('');
1159
+ }
1160
+ }
1161
+
1162
+ let totalInputTokens = 0;
1163
+ let totalOutputTokens = 0;
1164
+
1165
+ let _pendingCount = 0;
1166
+ let _closeRequested = false;
1167
+
1168
+ enableBracketedPaste(process.stdout);
1169
+
1170
+ const rl = readline.createInterface({
1171
+ input: createPasteSafeInput(process.stdin),
1172
+ output: createOutputFilter(process.stdout),
1173
+ prompt: tui.styled('\n You ', { color: tui.PALETTE.primary, bold: true }),
1174
+ terminal: true,
1175
+ });
1176
+ rl.prompt();
1177
+
1178
+ const cleanup = async (exitCode = 0) => {
1179
+ if (messages.length > 1) {
1180
+ // v5.4.10: Once oturumdaki butun conversation'i memory'ye persist et
1181
+ // Bu, Parton'un "oturum sonunda konusmalar kaydedilmiyor" sikayetini cözüyor
1182
+ const persistResult = await persistSessionToMemory(messages, memory, cfg);
1183
+ if (persistResult && persistResult.factsAdded > 0) {
1184
+ console.log(chalk.gray(`\n 🧠 ${persistResult.factsAdded} yeni fact memory'ye kaydedildi`));
1185
+ }
1186
+ if (persistResult && persistResult.preferencesAdded > 0) {
1187
+ console.log(chalk.gray(` 🎯 ${persistResult.preferencesAdded} yeni tercih kaydedildi`));
1188
+ }
1189
+
1190
+ const sessId = saveSession(messages, {
1191
+ provider: providerUrl, model, user: cfg.userName,
1192
+ bot: memory.botName, factCount: memory.facts?.length || 0,
1193
+ });
1194
+ console.log(chalk.gray(`\n 💾 Oturum kaydedildi: ${sessId}`));
1195
+ }
1196
+ // Global buffer temizle
1197
+ if (global._fixBuffer) global._fixBuffer = '';
1198
+ disableBracketedPaste(process.stdout);
1199
+ console.log(chalk.gray('\n 👋 Görüşürüz!\n'));
1200
+ process.exit(exitCode);
1201
+ };
1202
+
1203
+ /**
1204
+ * v5.4.10: Oturum sonunda conversation'dan otomatik fact/preference extraction
1205
+ * Bu, kullanıcının "her çıkışta konuşmalar kaydedilmiyor" sikayetini çözer
1206
+ */
1207
+ async function persistSessionToMemory(messages, memory, cfg) {
1208
+ let factsAdded = 0;
1209
+ let preferencesAdded = 0;
1210
+
1211
+ try {
1212
+ // Pattern-based extraction (zaten extractFacts var)
1213
+ const newFacts = extractFacts(messages, memory.facts || []);
1214
+
1215
+ // Bazi user message'lari da tara — genel kalıplarla fact çıkar
1216
+ const userMessages = messages.filter(m => m.role === 'user' && !m._internal);
1217
+ for (const msg of userMessages) {
1218
+ const text = (msg.content || '').toLowerCase();
1219
+
1220
+ // BotName hatirlatmasi
1221
+ if (text.includes('ad') && (text.includes('adin') || text.includes('ismin'))) {
1222
+ // Bot adı sorgulanmış olabilir, mevcut adı koru
1223
+ }
1224
+
1225
+ // Kisilik tercihleri (genel pattern'ler)
1226
+ const prefPatterns = [
1227
+ { match: /(?:benim ad[ıi]m?|bana\s+.*de|ad[ıi]m?)\s+(\w+)/i, category: 'personal', key: 'ad' },
1228
+ { match: /(?:seviyorum|hoşlan[ıi]yorum|beğeniyorum)\s+(\w+)/i, category: 'preference', key: 'sevilen' },
1229
+ { match: /(?:yaşıyorum|oturuyorum|kalıyorum)\s+(\w+)/i, category: 'location', key: 'yer' },
1230
+ ];
1231
+ for (const p of prefPatterns) {
1232
+ const m2 = msg.content.match(p.match);
1233
+ if (m2) {
1234
+ const val = m2[1].toLowerCase();
1235
+ const fact = `Kullanici ${p.key}: ${val}`;
1236
+ if (!(memory.facts || []).some(f => f.value === fact)) {
1237
+ newFacts.push({ value: fact, score: 6, category: p.category, createdAt: new Date().toISOString() });
1238
+ }
1239
+ }
1240
+ }
1241
+ }
1242
+
1243
+ // Deduplicate
1244
+ const existingValues = new Set((memory.facts || []).map(f => (f.value || f).toLowerCase()));
1245
+ const uniqueFacts = newFacts.filter(f => !existingValues.has((f.value || f).toLowerCase()));
1246
+
1247
+ if (uniqueFacts.length > 0) {
1248
+ memory.facts = [...(memory.facts || []), ...uniqueFacts];
1249
+ // v5.4.10: Verification ile kaydet
1250
+ const memFile = path.join(MEMORY_DIR, (cfg.userName || 'default').toLowerCase() + '.json');
1251
+ memory.lastUpdated = new Date().toISOString();
1252
+ fs.writeFileSync(memFile, JSON.stringify(memory, null, 2), 'utf8');
1253
+ // Verification: geri oku
1254
+ const verify = JSON.parse(fs.readFileSync(memFile, 'utf8'));
1255
+ factsAdded = uniqueFacts.length;
1256
+ }
1257
+
1258
+ // Decay (eski fact'leri dusuk skora dusur)
1259
+ if (memory.facts && memory.facts.length > 15) {
1260
+ // Max 15 fact tut, en dusuk skorlu olanlari sil
1261
+ memory.facts.sort((a, b) => (b.score || 5) - (a.score || 5));
1262
+ memory.facts = memory.facts.slice(0, 15);
1263
+ }
1264
+ } catch (e) {
1265
+ // Sessizce devam et, kritik degil
1266
+ }
1267
+
1268
+ return { factsAdded, preferencesAdded };
1269
+ }
1270
+
1271
+ rl.on('SIGINT', () => cleanup(0));
1272
+ process.on('SIGINT', () => cleanup(0));
1273
+ process.on('SIGTERM', () => cleanup(0));
1274
+
1275
+ rl.on('line', async (input) => {
1276
+ _pendingCount++;
1277
+ try {
1278
+ const line = restoreNewlines(input).trim();
1279
+ if (!line) { rl.prompt(); return; }
1280
+
1281
+ // Çok satırlı paste: output filter'a echo'yu durdurma sinyalini ver
1282
+ clearPasteContext();
1283
+
1284
+ // Slash komutlar
1285
+ if (line.startsWith('/')) {
1286
+ const parts = line.slice(1).split(/\s+/);
1287
+ const cmd = parts[0].toLowerCase();
1288
+ const arg = parts.slice(1).join(' ');
1289
+
1290
+ switch (cmd) {
1291
+ case 'help': printHelp(); break;
1292
+ case 'clear': console.clear(); break;
1293
+ case 'exit': case 'quit': case 'q': await cleanup(0); return;
1294
+ case 'history':
1295
+ console.log(chalk.cyan('\n 📜 Bu oturumun geçmişi:\n'));
1296
+ for (const m of messages.filter(m => !m._internal)) {
1297
+ const role = m.role === 'user' ? chalk.green('You') : chalk.blue('AI ');
1298
+ const content = (m.content || '').slice(0, 120) + ((m.content || '').length > 120 ? '...' : '');
1299
+ console.log(` ${role} ${content}`);
1300
+ }
1301
+ console.log('');
1302
+ break;
1303
+ case 'memory':
1304
+ console.log(chalk.cyan('\n 🧠 Memory:\n'));
1305
+ console.log(' Kullanıcı: ' + chalk.cyan(memory.name));
1306
+ console.log(' Nickname: ' + chalk.cyan(memory.nickname || '(yok)'));
1307
+ console.log(' Bot: ' + chalk.cyan(memory.botName || 'Asistan'));
1308
+ if (memory.facts && memory.facts.length > 0) {
1309
+ console.log(' Facts (' + memory.facts.length + '):');
1310
+ for (const f of memory.facts) {
1311
+ console.log(' • ' + chalk.gray((f.value || f) + (f.score ? ' [skor:' + f.score + ']' : '')));
1312
+ }
1313
+ } else {
1314
+ console.log(chalk.gray(' (Henüz fact yok)'));
1315
+ }
1316
+ console.log('');
1317
+ break;
1318
+ case 'forget':
1319
+ try {
1320
+ if (fs.existsSync(path.join(MEMORY_DIR, `${(cfg.userName || 'default').toLowerCase()}.json`))) {
1321
+ fs.unlinkSync(path.join(MEMORY_DIR, `${(cfg.userName || 'default').toLowerCase()}.json`));
1322
+ }
1323
+ memory = { name: cfg.userName, nickname: null, botName: 'Asistan', facts: [], preferences: [], history: [] };
1324
+ // System prompt'u rebuild with cleared memory
1325
+ promptOpts.memoryFacts = [];
1326
+ memoryStore.clear();
1327
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1328
+ systemPrompt = rebuildSystemPrompt(promptOpts);
1329
+ messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1330
+ console.log(chalk.green(' Memory temizlendi'));
1331
+ } catch (e) {
1332
+ console.log(chalk.red(' ❌ ' + e.message));
1333
+ }
1334
+ break;
1335
+ case 'sessions':
1336
+ const idx = loadSessionsIndex();
1337
+ console.log(chalk.cyan('\n 📚 Geçmiş Oturumlar (' + idx.sessions.length + ')\n'));
1338
+ for (let i = 0; i < Math.min(10, idx.sessions.length); i++) {
1339
+ const s = idx.sessions[i];
1340
+ console.log(` ${chalk.gray((i + 1).toString().padStart(2) + '.')} ${chalk.cyan(s.id)} ${chalk.muted('— ' + s.firstUserMessage)}`);
1341
+ }
1342
+ console.log(chalk.gray('\n Devam etmek için: /resume <id> veya /resume last\n'));
1343
+ break;
1344
+ case 'resume':
1345
+ if (!arg) { console.log(chalk.yellow(' Kullanım: /resume <id> veya /resume last')); break; }
1346
+ const session = loadSession(arg);
1347
+ if (session) {
1348
+ messages = session.messages || [];
1349
+ const sysIdx = messages.findIndex(m => m._internal);
1350
+ if (sysIdx >= 0) messages[sysIdx] = { role: 'system', content: systemPrompt, _internal: true };
1351
+ else messages.unshift({ role: 'system', content: systemPrompt, _internal: true });
1352
+ console.log(chalk.green(` ✓ Oturum yüklendi: ${session.id} (${messages.length} mesaj)`));
1353
+ } else {
1354
+ console.log(chalk.yellow(` ⚠️ Oturum bulunamadı: ${arg}`));
1355
+ }
1356
+ break;
1357
+ case 'system':
1358
+ if (!arg) { console.log(chalk.yellow(' Kullanım: /system <text>')); break; }
1359
+ // Override stable tier directly (user's custom text)
1360
+ _cachedStable = arg;
1361
+ // Rebuild volatile only (context stays unchanged)
1362
+ memoryStore.load();
1363
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1364
+ promptOpts.memoryFacts = memory.facts || [];
1365
+ const volOpts = { ...promptOpts, botName: '', soulSummary: '', skillsIndexBlock: '', crossSessionContext: '' };
1366
+ const volTiers = buildTiers(volOpts);
1367
+ systemPrompt = assemble(_cachedStable, _cachedContext, volTiers.volatile);
1368
+ messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1369
+ console.log(chalk.green(' ✓ System prompt güncellendi'));
1370
+ break;
1371
+ case 'model':
1372
+ if (!arg) { console.log(chalk.yellow(' Kullanım: /model <name>')); break; }
1373
+ model = arg;
1374
+ console.log(chalk.green(' ✓ Model: ') + chalk.cyan(model));
1375
+ break;
1376
+ case 'identity':
1377
+ if (!arg) { console.log(chalk.yellow(` Mevcut: ${memory.botName || 'Asistan'}`)); break; }
1378
+ memory.botName = arg;
1379
+ saveMemory(cfg.userName, memory);
1380
+ // Rebuild stable tier with new botName
1381
+ promptOpts.botName = arg;
1382
+ _cachedTierOpts = null; // force full rebuild
1383
+ memoryStore.load();
1384
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1385
+ promptOpts.memoryFacts = memory.facts || [];
1386
+ systemPrompt = rebuildSystemPrompt(promptOpts);
1387
+ messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1388
+ console.log(chalk.green(' ✓ Bot adı: ') + chalk.cyan(arg));
1389
+ break;
1390
+ case 'tokens':
1391
+ console.log(chalk.gray(` Token: ~${totalInputTokens} in / ~${totalOutputTokens} out`));
1392
+ break;
1393
+ case 'plan':
1394
+ if (arg === 'on' || arg === 'enter') {
1395
+ if (getPlanMode().enter()) console.log(tui.C.cyan('\n 📋 Plan modu aktif. Plan yapın ve /plan off ile çıkın.\n'));
1396
+ else console.log(tui.C.yellow(' Zaten plan modunda.'));
1397
+ } else if (arg === 'off' || arg === 'exit') {
1398
+ if (getPlanMode().isPlanning()) {
1399
+ console.log(tui.C.yellow(' Plan modundan çıkılıyor. Plan yazılıp ExitPlanMode ile sunulmalı.'));
1400
+ getPlanMode().approve();
1401
+ } else {
1402
+ console.log(tui.C.yellow(' Plan modunda değil.'));
1403
+ }
1404
+ } else if (arg === 'show') {
1405
+ if (getPlanMode().planHistory.length > 0) {
1406
+ const last = getPlanMode().planHistory[getPlanMode().planHistory.length - 1];
1407
+ console.log(tui.C.cyan('\n 📋 Son Plan:\n'));
1408
+ console.log(` ${last.plan.replace(/\n/g, '\n ')}`);
1409
+ } else {
1410
+ console.log(tui.C.yellow(' Henüz plan yok.'));
1411
+ }
1412
+ } else {
1413
+ console.log(tui.C.yellow(' Kullanım: /plan on|off|show'));
1414
+ }
1415
+ break;
1416
+ case 'save':
1417
+ const sessId = saveSession(messages, {
1418
+ provider: providerUrl, model, user: cfg.userName, bot: memory.botName,
1419
+ });
1420
+ console.log(chalk.green(' ✓ Kaydedildi: ') + chalk.cyan(sessId));
1421
+ break;
1422
+ default:
1423
+ // CLI komutları (REPL içinden)
1424
+ if (CLI_COMMANDS['/' + cmd]) {
1425
+ const cliCmd = CLI_COMMANDS['/' + cmd];
1426
+ if (cliCmd.needsArg && !arg) {
1427
+ console.log(chalk.yellow(` ${cmd} bir argüman gerekli: ${cliCmd.desc}`));
1428
+ } else {
1429
+ console.log(chalk.gray(` → ${cmd} çalıştırılıyor...`));
1430
+ const args2 = [...cliCmd.run];
1431
+ if (arg && (cmd === 'seo' || cmd === 'naturehub')) args2.push(arg);
1432
+ await runCliCommand(args2);
1433
+ }
1434
+ } else {
1435
+ console.log(chalk.yellow(` Bilinmeyen komut: /${cmd}. /help yazın.`));
1436
+ }
1437
+ }
1438
+ rl.prompt();
1439
+ return;
1440
+ }
1441
+
1442
+ // User mesajı
1443
+ messages.push({ role: 'user', content: line });
1444
+
1445
+ // Çok satırlı (paste) mesajları gönderildikten sonra ekranda göster
1446
+ if (line.indexOf('\n') !== -1) {
1447
+ process.stdout.write(tui.styled(' You ', { color: tui.PALETTE.primary, bold: true }));
1448
+ process.stdout.write(line + '\n');
1449
+ }
1450
+
1451
+ // v5.6.8: Hard-coded fallback - "sen kimsin?" sorulari icin dinamik botName
1452
+ const trimmed = (line || '').toLowerCase();
1453
+ const isIdentityQuestion = /(sen\s+kim|adin\s+ne|kendini\s+tan|kendin\s+tanit|kimsin|ne\s+adindasin)/.test(trimmed);
1454
+ if (isIdentityQuestion) {
1455
+ // v5.6.10: Hard-coded prefix minimal - model cevabini bozuyordu
1456
+ // Once sadece isim yaz, modelin devamini getirsin
1457
+ const displayName = memory.botName || 'Asistan';
1458
+ process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
1459
+ process.stdout.write('Merhaba! Ben ' + displayName + '. ');
1460
+ }
1461
+
1462
+ // AI cevabı — v5.13.0: workflow orchestrator ALWAYS first
1463
+ process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
1464
+ try {
1465
+ // Per-turn: rebuild volatile tier with current memory snapshot
1466
+ guardrails.reset();
1467
+ memory = loadMemory(cfg.userName);
1468
+ memoryStore.load();
1469
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
1470
+ promptOpts.memoryFacts = memory.facts || [];
1471
+ systemPrompt = rebuildSystemPrompt(promptOpts);
1472
+ messages[0] = { role: 'system', content: systemPrompt, _internal: true };
1473
+
1474
+ // v5.13.0: Run workflow FIRST for every request
1475
+ process.stdout.write(tui.styled('\r 🔧 workflow... ', { color: tui.PALETTE.muted }));
1476
+ const wfToolDefs = getToolDefs();
1477
+ const wfResult = await executeTool('workflow', { action: 'run', task: line }, wfToolDefs);
1478
+ const wf = wfResult?.result || {};
1479
+ if (wf.success !== false) {
1480
+ const loaded = wf.skillsLoaded && wf.skillsLoaded.length > 0 ? ` [skill: ${wf.skillsLoaded.join(', ')}]` : '';
1481
+ process.stdout.write(tui.styled(` ✓ workflow${loaded}\n`, { color: tui.PALETTE.success }));
1482
+ } else {
1483
+ process.stdout.write(tui.styled(' ✗ workflow\n', { color: tui.PALETTE.danger }));
1484
+ }
1485
+
1486
+ if (wf.passthrough && wf.reply !== undefined && wf.reply !== null) {
1487
+ // Simple chat workflow handled it directly
1488
+ const fullReply = String(wf.reply);
1489
+ const displayBotName = memory.botName || 'Asistan';
1490
+ let fixedReply = String(fullReply);
1491
+ fixedReply = fixedReply.replace(/\bMiniMax[-\s\w\.\d]*/gi, displayBotName);
1492
+ fixedReply = fixedReply.replace(/\bM2\.5[-\s\w\.\d]*/gi, displayBotName);
1493
+ fixedReply = fixedReply.replace(/\bM2[\s\-\.\w\d]*/gi, displayBotName);
1494
+ fixedReply = fixedReply.replace(/\bClaude[-\s\w\.\d]*/gi, displayBotName);
1495
+ fixedReply = fixedReply.replace(/\bGPT[-\s\w\.\d]*/gi, displayBotName);
1496
+ fixedReply = fixedReply.replace(/\bChatGPT\b/g, displayBotName);
1497
+ fixedReply = fixedReply.replace(/NatureCo\s+CLI(\s*'in|'nin)?/gi, displayBotName);
1498
+ fixedReply = fixedReply.replace(/Ben\s+MiniMax[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1499
+ fixedReply = fixedReply.replace(/Ben\s+Claude[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1500
+ fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1501
+ fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
1502
+ fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
1503
+ process.stdout.write('\n' + fixedReply + '\n');
1504
+ messages.push({ role: 'assistant', content: fixedReply });
1505
+ totalInputTokens += Math.ceil(line.length / 4);
1506
+ totalOutputTokens += Math.ceil(fullReply.length / 4);
1507
+ } else if (wf.status === 'completed' || (wf.results && wf.results.length > 0)) {
1508
+ // Complex task — inject workflow report as context, then LLM crafts final reply
1509
+ const workflowSteps = wf.results || [];
1510
+ const report = workflowSteps.map(r => {
1511
+ const t = r.tool || r.name || '?';
1512
+ const s = r.status === 'done' ? '✓' : '';
1513
+ let summary = '';
1514
+ if (r.result) {
1515
+ try { summary = typeof r.result === 'string' ? r.result.slice(0, 400) : JSON.stringify(r.result).slice(0, 400); } catch {}
1516
+ }
1517
+ return ` ${s} ${t}: ${summary}`;
1518
+ }).join('\n');
1519
+ const skillInfo = wf.skillsLoaded && wf.skillsLoaded.length > 0
1520
+ ? `\n\nKullanilan skill'ler: ${wf.skillsLoaded.join(', ')}`
1521
+ : '';
1522
+ const preWfLen = messages.length;
1523
+ messages.push({
1524
+ role: 'system',
1525
+ content: `=== WORKFLOW SONUCLARI ===\nSu araclar calisti:\n${report}${skillInfo}\n\nKullaniciya bu sonuclari anlamli bir sekilde ozetle.\n=== SONUC BITTI ===`,
1526
+ });
1527
+ const reply = await sendStreaming(
1528
+ providerUrl,
1529
+ providerApiKey,
1530
+ messages,
1531
+ model,
1532
+ // v5.6.12: Callback bos - tam metin 'reply' olarak gelecek (non-stream mode)
1533
+ noopCallback,
1534
+ // Tool call callback — Hermes-style per-tool status line
1535
+ ((toolEvent) => {
1536
+ const name = toolEvent.name;
1537
+ if (toolEvent.status === 'running') {
1538
+ process.stdout.write(tui.styled('\r 🔧 ' + name + '... ', { color: tui.PALETTE.muted }));
1539
+ } else if (toolEvent.status === 'done') {
1540
+ if (toolEvent.result?.error) {
1541
+ process.stdout.write(tui.styled(' ✗ ' + name + ': ' + String(toolEvent.result.error).slice(0, 80) + '\n', { color: tui.PALETTE.danger }));
1542
+ } else {
1543
+ process.stdout.write(tui.styled(' ✓ ' + name + '\n', { color: tui.PALETTE.success }));
1544
+ }
1545
+ }
1546
+ })
1547
+ );
1548
+ // Remove workflow results message (already served its purpose)
1549
+ messages.splice(preWfLen, 1);
1550
+ // v5.6.12: Tam metin 'reply' olarak zaten geldi (non-stream mode)
1551
+ const fullReply = String(reply || '');
1552
+ // Bot adini al
1553
+ const displayBotName = memory.botName || 'Asistan';
1554
+ // v5.6.9: Tum model adlarini ve varyasyonlari temizle
1555
+ let fixedReply = String(fullReply);
1556
+ fixedReply = fixedReply.replace(/\bMiniMax[-\s\w\.\d]*/gi, displayBotName);
1557
+ fixedReply = fixedReply.replace(/\bM2\.5[-\s\w\.\d]*/gi, displayBotName);
1558
+ fixedReply = fixedReply.replace(/\bM2[\s\-\.\w\d]*/gi, displayBotName);
1559
+ fixedReply = fixedReply.replace(/\bClaude[-\s\w\.\d]*/gi, displayBotName);
1560
+ fixedReply = fixedReply.replace(/\bGPT[-\s\w\.\d]*/gi, displayBotName);
1561
+ fixedReply = fixedReply.replace(/\bChatGPT\b/g, displayBotName);
1562
+ fixedReply = fixedReply.replace(/NatureCo\s+CLI(\s*'in|'nin)?/gi, displayBotName);
1563
+ fixedReply = fixedReply.replace(/Ben\s+MiniMax[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1564
+ fixedReply = fixedReply.replace(/Ben\s+Claude[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1565
+ fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
1566
+ fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
1567
+ fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
1568
+ process.stdout.write('\n' + fixedReply + '\n');
1569
+ messages.push({ role: 'assistant', content: fixedReply });
1570
+ totalInputTokens += Math.ceil(((fullReply || '') + report + skillInfo).length / 4);
1571
+ totalOutputTokens += Math.ceil((fullReply || '').length / 4);
1572
+ } else {
1573
+ // Workflow failed or returned unexpected format
1574
+ const fullReply = wf.error || JSON.stringify(wf).slice(0, 400) || 'Workflow islenemedi.';
1575
+ const displayBotName = memory.botName || 'Asistan';
1576
+ let fixedReply = fullReply.replace(/\bMiniMax[-\s\w\.\d]*/gi, displayBotName);
1577
+ process.stdout.write('\n' + fixedReply + '\n');
1578
+ messages.push({ role: 'assistant', content: fixedReply });
1579
+ totalInputTokens += Math.ceil(line.length / 4);
1580
+ totalOutputTokens += Math.ceil(fixedReply.length / 4);
1581
+ }
1582
+ } catch (err) {
1583
+ process.stdout.write('\n');
1584
+ console.log(chalk.red(' ❌ ' + err.message));
1585
+ }
1586
+ rl.prompt();
1587
+ } finally {
1588
+ _pendingCount--;
1589
+ if (_closeRequested && _pendingCount === 0) {
1590
+ await cleanup(0);
1591
+ }
1592
+ }
1593
+ });
1594
+
1595
+ rl.on('close', () => {
1596
+ if (_pendingCount > 0) {
1597
+ _closeRequested = true;
1598
+ } else {
1599
+ cleanup(0);
1600
+ }
1601
+ });
1602
+ }
1603
+
1604
+ module.exports = startRepl;