natureco-cli 5.7.18 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.7.18",
3
+ "version": "5.8.0",
4
4
  "description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * natureco medium — Medium makale yayınlama (Phase 6)
3
3
  *
4
- * Parton'un hedefi: ayda en az 4 Medium makalesi.
4
+ * Medium entegrasyonu makale yayinlama.
5
5
  * Bu komut CLI'dan taslak yayınlamayı sağlar.
6
6
  *
7
7
  * Kullanım:
@@ -8,7 +8,7 @@
8
8
  * natureco naturehub trending Trend olan konular
9
9
  * natureco naturehub config Ayarları göster
10
10
  *
11
- * API endpoint: api.natureco.me/naturehub/* (placeholder, gerçek API Parton sağlayacak)
11
+ * API endpoint: api.natureco.me/naturehub/* (placeholder)
12
12
  */
13
13
 
14
14
  const chalk = require('chalk');
@@ -27,7 +27,8 @@ const { accumulateToolCallDeltas, finalizeToolCalls } = require('../utils/stream
27
27
  const { createPasteSafeInput, createOutputFilter, enableBracketedPaste, disableBracketedPaste, restoreNewlines, clearPasteContext } = require('../utils/paste-safe-input');
28
28
  const { getMemoryStore } = require('../utils/memory-store');
29
29
  const { buildSkillIndex } = require('../utils/skill-index');
30
- const { buildTiers, assemble, stableContextKey } = require('../utils/system-prompt');
30
+ const { buildTiers, assemble } = require('../utils/system-prompt');
31
+ const { ToolGuardrails } = require('../utils/tool-guardrails');
31
32
 
32
33
  // v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
33
34
  const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
@@ -36,11 +37,11 @@ function fixModelNameLeak(text, botName) {
36
37
  let fixed = text;
37
38
  for (const modelName of MODEL_NAMES_TO_HIDE) {
38
39
  const regex = new RegExp(modelName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
39
- fixed = fixed.replace(regex, botName || 'İchigo');
40
+ fixed = fixed.replace(regex, botName || 'asistan');
40
41
  }
41
- fixed = fixed.replace(/Ben\s+MiniMax[^.,!?\n]*/gi, 'Ben İchigo');
42
- fixed = fixed.replace(/I'm\s+MiniMax[^.,!?\n]*/gi, "I'm İchigo");
43
- fixed = fixed.replace(/I am\s+Claude[^.,!?\n]*/gi, 'I am İchigo');
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'));
44
45
  return fixed;
45
46
  }
46
47
  global.fixModelNameLeak = fixModelNameLeak;
@@ -58,6 +59,46 @@ function getToolDefs() {
58
59
  return _toolDefs;
59
60
  }
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
+
76
+ if (needsFullRebuild || !_cachedStable) {
77
+ const tiers = buildTiers(opts);
78
+ _cachedStable = tiers.stable;
79
+ _cachedContext = tiers.context;
80
+ _cachedTierOpts = {
81
+ botName: opts.botName,
82
+ soulSummary: opts.soulSummary,
83
+ skillsIndexBlock: opts.skillsIndexBlock,
84
+ crossSessionContext: opts.crossSessionContext,
85
+ };
86
+ }
87
+ // Volatile always rebuilt fresh
88
+ const volatileOnly = buildTiers({
89
+ ...opts,
90
+ // Pass empty strings for stable/context fields so buildTiers only builds volatile
91
+ botName: '',
92
+ soulSummary: '',
93
+ skillsIndexBlock: '',
94
+ crossSessionContext: '',
95
+ });
96
+ return assemble(_cachedStable, _cachedContext, volatileOnly.volatile);
97
+ }
98
+
99
+ // ── Tool Guardrails instance (Hermes-style) ─────────────────────────────
100
+ const guardrails = new ToolGuardrails();
101
+
61
102
  // CLI komutları (REPL içinden çalıştırılabilir)
62
103
  const CLI_COMMANDS = {
63
104
  '/doctor': { desc: 'Sistem sağlığı kontrolü', run: ['doctor'] },
@@ -102,7 +143,7 @@ function loadMemory(username) {
102
143
  try {
103
144
  if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8'));
104
145
  } catch {}
105
- return { name: username || 'Kullanıcı', nickname: null, botName: 'İchigo', facts: [], preferences: [], history: [] };
146
+ return { name: username || 'Kullanıcı', nickname: null, botName: 'Asistan', facts: [], preferences: [], history: [] };
106
147
  }
107
148
 
108
149
  function saveMemory(username, memory) {
@@ -233,6 +274,7 @@ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChu
233
274
  const isMM = isMiniMax(providerUrl);
234
275
  const toolDefs = getToolDefs();
235
276
  const toolParam = toOpenAIFormat(toolDefs);
277
+ guardrails.reset();
236
278
 
237
279
  let currentMessages = messages;
238
280
  let fullText = '';
@@ -268,17 +310,18 @@ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChu
268
310
 
269
311
  while (iterations < MAX_TOOL_ITERATIONS) {
270
312
  iterations++;
313
+ const shouldStream = !isMM; // MiniMax streaming endpoint doesn't support tool_calls
271
314
  const body = {
272
315
  model,
273
316
  messages: currentMessages,
274
- stream: false, // v5.4.18: tum cevap bekle, sonra fix et
317
+ stream: shouldStream,
275
318
  temperature: 0.3,
276
319
  max_tokens: 2048,
277
320
  };
278
321
  if (toolParam) body.tools = toolParam;
279
322
  if (isMM) body.tool_choice = 'auto'; // MiniMax için explicit
280
323
 
281
- if (!body.stream) {
324
+ if (!shouldStream) {
282
325
  // MiniMax (non-stream) — tool_calls desteklemiyor varsayalım
283
326
  const res = await apiRequest(providerUrl, providerApiKey, body, false);
284
327
  const msg = res.choices?.[0]?.message || {};
@@ -408,12 +451,25 @@ async function processToolCalls(toolCalls, onToolCall) {
408
451
  return { name, args, id };
409
452
  });
410
453
 
411
- // Separate parallel-safe and sequential tools
412
- const parallelBatch = parsed.filter(p => PARALLEL_SAFE_TOOLS.has(p.name));
413
- const sequentialBatch = parsed.filter(p => !PARALLEL_SAFE_TOOLS.has(p.name));
454
+ // Filter out blocked tools via guardrails
455
+ guardrails.startIteration();
456
+ const blocked = parsed.filter(p => {
457
+ const check = guardrails.check(p.name, p.args);
458
+ if (check.blocked) {
459
+ if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'done', result: { error: check.reason } });
460
+ results.push({ ...p, result: { error: check.reason }, _blocked: true });
461
+ return false;
462
+ }
463
+ return true;
464
+ });
465
+ const allowed = parsed.filter(p => !results.some(r => r.id === p.id && r._blocked));
466
+
467
+ // Separate parallel-safe and sequential tools (over allowed only)
468
+ const parallelBatch = allowed.filter(p => PARALLEL_SAFE_TOOLS.has(p.name));
469
+ const sequentialBatch = allowed.filter(p => !PARALLEL_SAFE_TOOLS.has(p.name));
414
470
 
415
- // Notify UI for all
416
- for (const p of parsed) {
471
+ // Notify UI for all non-blocked
472
+ for (const p of allowed) {
417
473
  if (onToolCall) onToolCall({ name: p.name, args: p.args, status: 'running' });
418
474
  }
419
475
 
@@ -421,6 +477,8 @@ async function processToolCalls(toolCalls, onToolCall) {
421
477
  if (parallelBatch.length > 0) {
422
478
  const parallelResults = await Promise.all(parallelBatch.map(async (p) => {
423
479
  const result = await executeTool(p.name, p.args, toolDefs);
480
+ const success = result?.success !== false;
481
+ guardrails.record(p.name, p.args, success);
424
482
  return { ...p, result };
425
483
  }));
426
484
  results.push(...parallelResults);
@@ -429,9 +487,16 @@ async function processToolCalls(toolCalls, onToolCall) {
429
487
  // Run sequential batch one at a time
430
488
  for (const p of sequentialBatch) {
431
489
  const result = await executeTool(p.name, p.args, toolDefs);
490
+ const success = result?.success !== false;
491
+ guardrails.record(p.name, p.args, success);
432
492
  results.push({ ...p, result });
433
493
  }
434
494
 
495
+ // No-progress check: if all tools failed, inject warning
496
+ if (guardrails.isNoProgress()) {
497
+ if (onToolCall) onToolCall({ name: '_no_progress', args: null, status: 'done', result: { error: 'All tools failed this iteration' } });
498
+ }
499
+
435
500
  // Notify UI done + build messages
436
501
  const messages = [];
437
502
  for (const { name, id, result } of results) {
@@ -513,7 +578,7 @@ async function startRepl(args) {
513
578
 
514
579
  // v5.6.19: Oncelik config.botName, sonra memory.botName
515
580
  if (!memory.botName) {
516
- memory.botName = cfg.botName || 'İchigo';
581
+ memory.botName = cfg.botName || 'Asistan';
517
582
  }
518
583
  // BotName'i memory'ye persist et (her oturumda ayni kalsin)
519
584
  try {
@@ -575,26 +640,23 @@ async function startRepl(args) {
575
640
  }
576
641
 
577
642
  // System prompt oluştur (memory + identity + persistent bağlam)
578
- // v5.4.6: Bot adı zorlaması EN GÜÇLÜ + SOUL.md EN BAŞTA
579
- const botName = memory.botName || 'İchigo';
580
- const userName = memory.name || memory.nickname || 'kanka';
581
643
  // v5.6.5: Kucuk model tespiti (Groq, Mistral Small, Ollama) - SOUL injection skip
644
+ const botName = memory.botName || 'Asistan';
645
+ const userName = memory.name || memory.nickname || 'kanka';
582
646
  const isSmallModel = (cfg.providerUrl || '').includes('groq.com') ||
583
647
  (cfg.providerUrl || '').includes('mistral.ai') ||
584
648
  (cfg.providerUrl || '').includes('localhost') ||
585
649
  (cfg.providerUrl || '').includes('ollama');
586
- // `let` (not `const`) because /system <text> reassigns it at line ~796.
587
- // Before this fix, /system would throw "Assignment to constant variable"
588
- // and tear down the REPL session mid-conversation.
589
- const tiers = buildTiers({
650
+ // Build system prompt with tier caching (stable+context cached, volatile fresh)
651
+ const promptOpts = {
590
652
  botName, userName, soulSummary, isSmallModel,
591
653
  memorySnapshotBlock, skillsIndexBlock,
592
654
  crossSessionContext: crossSessionContext || '',
593
655
  userHome: cfg.userHome || '',
594
656
  hasHistory: messages.length > 0,
595
657
  memoryFacts: memory.facts || [],
596
- });
597
- let systemPrompt = assemble(tiers.stable, tiers.context, tiers.volatile);
658
+ };
659
+ let systemPrompt = rebuildSystemPrompt(promptOpts);
598
660
 
599
661
  if (messages.length === 0) {
600
662
  messages.push({ role: 'system', content: systemPrompt, _internal: true });
@@ -612,14 +674,14 @@ async function startRepl(args) {
612
674
  console.log(tui.C.muted(' Provider: ') + tui.C.brand(providerUrl.replace(/https?:\/\//, '')));
613
675
  console.log(tui.C.muted(' Model: ') + tui.C.brand(model));
614
676
  console.log(tui.C.muted(' Kullanıcı: ') + tui.C.brand((memory.nickname || cfg.userName) + (memory.nickname ? ` (${cfg.userName})` : '')));
615
- console.log(tui.C.muted(' Bot: ') + tui.C.brand(memory.botName || 'İchigo'));
677
+ console.log(tui.C.muted(' Bot: ') + tui.C.brand(memory.botName || 'Asistan'));
616
678
  if (messages.length > 1) {
617
679
  console.log(tui.C.muted(' Oturum: ') + tui.C.amber(`${messages.filter(m => m.role === 'user' || m.role === 'assistant').length} mesaj (resume)`));
618
680
  }
619
681
  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'));
620
682
  console.log('');
621
683
  // v5.4.7: Hard-coded kimlik
622
- const displayBotName = memory.botName || 'İchigo';
684
+ const displayBotName = memory.botName || 'Asistan';
623
685
  const displayUserName = userName || 'kanka';
624
686
  console.log(tui.C.brand(' 👋 Ben ' + displayBotName + ', ' + displayUserName + '. Sen nasilsin?'));
625
687
  console.log('');
@@ -683,31 +745,30 @@ async function startRepl(args) {
683
745
  // Pattern-based extraction (zaten extractFacts var)
684
746
  const newFacts = extractFacts(messages, memory.facts || []);
685
747
 
686
- // Bazi user message'lari da tara - 'Parton', 'Ichigo', 'patron', 'CEO' gecerse ekle
748
+ // Bazi user message'lari da tara genel kalıplarla fact çıkar
687
749
  const userMessages = messages.filter(m => m.role === 'user' && !m._internal);
688
750
  for (const msg of userMessages) {
689
751
  const text = (msg.content || '').toLowerCase();
690
752
 
691
753
  // BotName hatirlatmasi
692
- if (text.includes('ichigo') && text.includes('ad')) {
693
- if (memory.botName !== 'İchigo') {
694
- memory.botName = 'İchigo';
695
- }
754
+ if (text.includes('ad') && (text.includes('adin') || text.includes('ismin'))) {
755
+ // Bot adı sorgulanmış olabilir, mevcut adı koru
696
756
  }
697
757
 
698
- // Patron/partnership
699
- if ((text.includes('patron') || text.includes('patronum')) && text.length < 100) {
700
- const fact = 'Kullanici benim patronum, ona patron diye hitap etmeliyim';
701
- if (!(memory.facts || []).some(f => f.value === fact)) {
702
- newFacts.push({ value: fact, score: 8, category: 'personal', createdAt: new Date().toISOString() });
703
- }
704
- }
705
-
706
- // NatureCo CEO
707
- if (text.includes('natureco') && text.includes('ceo')) {
708
- const fact = "Kullanici NatureCo CEO'sudur";
709
- if (!(memory.facts || []).some(f => f.value === fact)) {
710
- newFacts.push({ value: fact, score: 9, category: 'work', createdAt: new Date().toISOString() });
758
+ // Kisilik tercihleri (genel pattern'ler)
759
+ const prefPatterns = [
760
+ { match: /(?:benim ad[ıi]m?|bana\s+.*de|ad[ıi]m?)\s+(\w+)/i, category: 'personal', key: 'ad' },
761
+ { match: /(?:seviyorum|hoşlan[ıi]yorum|beğeniyorum)\s+(\w+)/i, category: 'preference', key: 'sevilen' },
762
+ { match: /(?:yaşıyorum|oturuyorum|kalıyorum)\s+(\w+)/i, category: 'location', key: 'yer' },
763
+ ];
764
+ for (const p of prefPatterns) {
765
+ const m2 = msg.content.match(p.match);
766
+ if (m2) {
767
+ const val = m2[1].toLowerCase();
768
+ const fact = `Kullanici ${p.key}: ${val}`;
769
+ if (!(memory.facts || []).some(f => f.value === fact)) {
770
+ newFacts.push({ value: fact, score: 6, category: p.category, createdAt: new Date().toISOString() });
771
+ }
711
772
  }
712
773
  }
713
774
  }
@@ -774,7 +835,7 @@ async function startRepl(args) {
774
835
  console.log(chalk.cyan('\n 🧠 Memory:\n'));
775
836
  console.log(' Kullanıcı: ' + chalk.cyan(memory.name));
776
837
  console.log(' Nickname: ' + chalk.cyan(memory.nickname || '(yok)'));
777
- console.log(' Bot: ' + chalk.cyan(memory.botName || 'İchigo'));
838
+ console.log(' Bot: ' + chalk.cyan(memory.botName || 'Asistan'));
778
839
  if (memory.facts && memory.facts.length > 0) {
779
840
  console.log(' Facts (' + memory.facts.length + '):');
780
841
  for (const f of memory.facts) {
@@ -790,10 +851,13 @@ async function startRepl(args) {
790
851
  if (fs.existsSync(path.join(MEMORY_DIR, `${(cfg.userName || 'default').toLowerCase()}.json`))) {
791
852
  fs.unlinkSync(path.join(MEMORY_DIR, `${(cfg.userName || 'default').toLowerCase()}.json`));
792
853
  }
793
- memory = { name: cfg.userName, nickname: null, botName: 'İchigo', facts: [], preferences: [], history: [] };
794
- // System prompt'u sıfırla
795
- const newSysPrompt = systemPrompt.replace(/Kullanıcı hakkında bildiklerin:.*$/, '').trim();
796
- messages[0] = { role: 'system', content: newSysPrompt, _internal: true };
854
+ memory = { name: cfg.userName, nickname: null, botName: 'Asistan', facts: [], preferences: [], history: [] };
855
+ // System prompt'u rebuild with cleared memory
856
+ promptOpts.memoryFacts = [];
857
+ memoryStore.clear();
858
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
859
+ systemPrompt = rebuildSystemPrompt(promptOpts);
860
+ messages[0] = { role: 'system', content: systemPrompt, _internal: true };
797
861
  console.log(chalk.green(' ✓ Memory temizlendi'));
798
862
  } catch (e) {
799
863
  console.log(chalk.red(' ❌ ' + e.message));
@@ -823,7 +887,15 @@ async function startRepl(args) {
823
887
  break;
824
888
  case 'system':
825
889
  if (!arg) { console.log(chalk.yellow(' Kullanım: /system <text>')); break; }
826
- systemPrompt = arg;
890
+ // Override stable tier directly (user's custom text)
891
+ _cachedStable = arg;
892
+ // Rebuild volatile only (context stays unchanged)
893
+ memoryStore.load();
894
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
895
+ promptOpts.memoryFacts = memory.facts || [];
896
+ const volOpts = { ...promptOpts, botName: '', soulSummary: '', skillsIndexBlock: '', crossSessionContext: '' };
897
+ const volTiers = buildTiers(volOpts);
898
+ systemPrompt = assemble(_cachedStable, _cachedContext, volTiers.volatile);
827
899
  messages[0] = { role: 'system', content: systemPrompt, _internal: true };
828
900
  console.log(chalk.green(' ✓ System prompt güncellendi'));
829
901
  break;
@@ -833,11 +905,17 @@ async function startRepl(args) {
833
905
  console.log(chalk.green(' ✓ Model: ') + chalk.cyan(model));
834
906
  break;
835
907
  case 'identity':
836
- if (!arg) { console.log(chalk.yellow(` Mevcut: ${memory.botName || 'İchigo'}`)); break; }
908
+ if (!arg) { console.log(chalk.yellow(` Mevcut: ${memory.botName || 'Asistan'}`)); break; }
837
909
  memory.botName = arg;
838
910
  saveMemory(cfg.userName, memory);
839
- const newSys = systemPrompt.replace(/Sen \w+ adında/, `Sen ${arg} adında`);
840
- messages[0] = { role: 'system', content: newSys, _internal: true };
911
+ // Rebuild stable tier with new botName
912
+ promptOpts.botName = arg;
913
+ _cachedTierOpts = null; // force full rebuild
914
+ memoryStore.load();
915
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
916
+ promptOpts.memoryFacts = memory.facts || [];
917
+ systemPrompt = rebuildSystemPrompt(promptOpts);
918
+ messages[0] = { role: 'system', content: systemPrompt, _internal: true };
841
919
  console.log(chalk.green(' ✓ Bot adı: ') + chalk.cyan(arg));
842
920
  break;
843
921
  case 'tokens':
@@ -884,7 +962,7 @@ async function startRepl(args) {
884
962
  if (isIdentityQuestion) {
885
963
  // v5.6.10: Hard-coded prefix minimal - model cevabini bozuyordu
886
964
  // Once sadece isim yaz, modelin devamini getirsin
887
- const displayName = memory.botName || 'İchigo';
965
+ const displayName = memory.botName || 'Asistan';
888
966
  process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
889
967
  process.stdout.write('Merhaba! Ben ' + displayName + '. ');
890
968
  }
@@ -892,6 +970,14 @@ async function startRepl(args) {
892
970
  // AI cevabı
893
971
  process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
894
972
  try {
973
+ // Per-turn: rebuild volatile tier with current memory snapshot (Hermes frozen snapshot)
974
+ guardrails.reset();
975
+ memory = loadMemory(cfg.userName);
976
+ memoryStore.load();
977
+ promptOpts.memorySnapshotBlock = memoryStore.getSystemPromptBlock();
978
+ promptOpts.memoryFacts = memory.facts || [];
979
+ systemPrompt = rebuildSystemPrompt(promptOpts);
980
+ messages[0] = { role: 'system', content: systemPrompt, _internal: true };
895
981
  const apiMessages = messages.filter(m => !m._internal);
896
982
  const reply = await sendStreaming(
897
983
  providerUrl,
@@ -900,24 +986,24 @@ async function startRepl(args) {
900
986
  model,
901
987
  // v5.6.12: Callback bos - tam metin 'reply' olarak gelecek (non-stream mode)
902
988
  () => {},
903
- // Tool call callback — minimal Hermes-style one-liner
904
- (toolEvent) => {
989
+ // Tool call callback — Hermes-style per-tool status line
990
+ ((toolEvent) => {
991
+ const name = toolEvent.name;
905
992
  if (toolEvent.status === 'running') {
906
- process.stdout.write(tui.styled('\r 🔧 ' + toolEvent.name + '... ', { color: tui.PALETTE.muted }));
993
+ process.stdout.write(tui.styled('\r 🔧 ' + name + '... ', { color: tui.PALETTE.muted }));
907
994
  } else if (toolEvent.status === 'done') {
908
- if (toolEvent.result.error) {
909
- process.stdout.write(tui.styled('\r ✗ ' + toolEvent.name + ': ' + toolEvent.result.error.slice(0, 80) + '\n', { color: tui.PALETTE.danger }));
995
+ if (toolEvent.result?.error) {
996
+ process.stdout.write(tui.styled(' ✗ ' + name + ': ' + String(toolEvent.result.error).slice(0, 80) + '\n', { color: tui.PALETTE.danger }));
910
997
  } else {
911
- process.stdout.write('\r' + tui.styled(' ✓ ' + toolEvent.name + ' ', { color: tui.PALETTE.success }) + '\r');
998
+ process.stdout.write(tui.styled(' ✓ ' + name + '\n', { color: tui.PALETTE.success }));
912
999
  }
913
- process.stdout.write(tui.styled(' AI ', { color: tui.PALETTE.secondary, bold: true }));
914
1000
  }
915
- }
1001
+ })
916
1002
  );
917
1003
  // v5.6.12: Tam metin 'reply' olarak zaten geldi (non-stream mode)
918
1004
  const fullReply = String(reply || '');
919
1005
  // Bot adini al
920
- const displayBotName = memory.botName || 'İchigo';
1006
+ const displayBotName = memory.botName || 'Asistan';
921
1007
  // v5.6.9: Tum model adlarini ve varyasyonlari temizle
922
1008
  let fixedReply = String(fullReply);
923
1009
  // Bilinen model adlari - tum varyasyonlar
@@ -933,13 +1019,9 @@ async function startRepl(args) {
933
1019
  fixedReply = fixedReply.replace(/Ben\s+MiniMax[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
934
1020
  fixedReply = fixedReply.replace(/Ben\s+Claude[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
935
1021
  fixedReply = fixedReply.replace(/Ben\s+GPT[^.!?,;:\n]*/gi, 'Ben ' + displayBotName);
936
- fixedReply = fixedReply.replace(/Ben\s+İchigo[\s\w\.]*/gi, 'Ben ' + displayBotName);
1022
+ fixedReply = fixedReply.replace(/Ben\s+Asistan[\s\w\.]*/gi, 'Ben ' + displayBotName);
937
1023
  // Markdown ** ile cevrili model adi
938
1024
  fixedReply = fixedReply.replace(/\*\*(?:MiniMax|Claude|GPT|M2\.5|M2)[^\*]*\*\*/gi, '**' + displayBotName + '**');
939
- // "İchigo" varyasyonlari
940
- fixedReply = fixedReply.replace(/(İchigo)(\d)([a-zA-ZçğıöşüÇĞİÖŞÜ])/g, displayBotName + ' $3');
941
- fixedReply = fixedReply.replace(/İchigo[\.\s\-_]*\d+/g, displayBotName);
942
- fixedReply = fixedReply.replace(/İchigo\./g, displayBotName);
943
1025
  // Cevap yazdir
944
1026
  process.stdout.write('\n' + fixedReply + '\n');
945
1027
  // v5.7.18: Tool call geçmişini kalıcı messages'a ekle — model sonraki turlarda tool sonuçlarını görsün
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * natureco xp — NatureCo XP/Level sistemi (Phase 6)
3
3
  *
4
- * Parton'un hedeflerinden biri: kullanıcıları ödüllendirmek.
4
+ * XP/Level sistemi kullanıcıları ödüllendirir.
5
5
  * XP kazanma yolları:
6
6
  * - Komut çalıştırma (1 XP)
7
7
  * - Audit log kaydı (0.1 XP)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * calendar_add - macOS Calendar'a etkinlik ekle (v4.9.1)
3
3
  *
4
- * Parton'un OS-level kontrol vizyonu için.
4
+ * OS-level calendar kontrolü
5
5
  * "Yarin saat 14:00 toplantim var" -> Takvime ekler.
6
6
  */
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * cross_session_memory - Oturumlar arasi hafiza (v5.3.1)
3
3
  *
4
- * Parton'un vizyonu: "Hafta sonra gelince de beni hatirlayacak"
4
+ * Cross-session hafiza oturumlar arasi baglam korur
5
5
  *
6
6
  * Ozellikler:
7
7
  * - Tum session'lari tarihsel sirayla yukler
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dashboard - Web Dashboard v2 (v5.4.0)
3
3
  *
4
- * Parton'un vizyonu: "CLI'yi web'den de kontrol edebileyim"
4
+ * Dashboard CLI'yi web'den kontrol eder
5
5
  *
6
6
  * Ozellikler:
7
7
  * - Real-time tool execution (WebSocket)
@@ -189,7 +189,7 @@ const DASHBOARD_HTML = `
189
189
  </div>
190
190
 
191
191
  <div class="footer">
192
- <p>NatureCo CLI v5.4.0 - Parton & Sasuke - <span id="lastUpdate"></span></p>
192
+ <p>NatureCo CLI v5.4.0 - <span id="lastUpdate"></span></p>
193
193
  </div>
194
194
  </div>
195
195
  <script>
@@ -9,7 +9,7 @@ const { spawn } = require("child_process");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
11
 
12
- // v5.2.0: Agent alias mapping (Parton'un testinden — "review" diye bir agent yok)
12
+ // v5.2.0: Agent alias mapping
13
13
  const AGENT_ALIASES = {
14
14
  "review": "general", // eskiden review diye bir vardi, simdi general
15
15
  "analyze": "explore",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * mac_alarm - macOS Clock app ile alarm kur (v5.1.1)
3
3
  *
4
- * Parton'un istegi: "Saat uygulamasi uzerinden saat 18:00 alarm kur"
4
+ * macOS alarm saat uygulamasi uzerinden alarm kurar
5
5
  * Eski reminder_add date parse edemiyordu. Bu tool AppleScript ile
6
6
  * Clock.app'in events sistemine yazar (alarm orada saklanir).
7
7
  */
@@ -2,7 +2,7 @@
2
2
  * memory_write - Memory'ye fact/kayit yaz (v5.1.1)
3
3
  *
4
4
  * REPL'in extractMemoryFromMessage ozelligini tool olarak expose eder.
5
- * Parton'un vizyonu: "Benim asistanim, her seyimi hatirlayacak"
5
+ * Kalici hafiza faktlari kaydeder, puanlar, eskiyenleri temizler
6
6
  */
7
7
 
8
8
  const fs = require("fs");
@@ -133,7 +133,7 @@ function verifyMemoryWrite(username, expectedFact, expectedBotName) {
133
133
 
134
134
  function addMemory({ username, fact, score = 5, category = "general", botName, nickname, name }) {
135
135
  // Username yoksa ve 'name' parametresi varsa, onu username olarak kullan
136
- // (Parton'un "patron" diye hitap etmesi durumu icin)
136
+ // (hitap bicimi icin)
137
137
  const effectiveUsername = username || (name && name.toLowerCase()) || 'default';
138
138
  if (!effectiveUsername || effectiveUsername === 'default') {
139
139
  // Hicbir username yok, default.json'a yaz
@@ -232,8 +232,8 @@ module.exports = {
232
232
  inputSchema: {
233
233
  type: "object",
234
234
  properties: {
235
- username: { type: "string", description: "Kullanici adi (ornek: 'gencay' veya 'parton')" },
236
- fact: { type: "string", description: 'Yeni fact (ornek: "Kullanici Naruto karakterini seviyor", "Istanbul\'da yasiyor")' },
235
+ username: { type: "string", description: "Kullanici adi (ornek: 'ahmet' veya 'default')" },
236
+ fact: { type: "string", description: 'Yeni fact (ornek: "Kullanici kahve seviyor", "Istanbul\'da yasiyor")' },
237
237
  score: { type: "number", description: "Onem derecesi 1-10 (default 5)" },
238
238
  category: { type: "string", description: "Kategori: personal, preference, work, hobby, fact (default general)" },
239
239
  botName: { type: "string", description: "Bot adini degistir (memory.botName)" },
package/src/tools/plan.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * plan - Plan modu (v5.3.1)
3
3
  *
4
- * Parton'un vizyonu: "Karmasik isleri once planlasin, sonra calistirayim"
4
+ * Planlama karmasik isleri adimlara bolup calistirir
5
5
  *
6
6
  * Ozellikler:
7
7
  * - Sadece plan yapar, hicbir tool calistirmaz
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * plugin.js — Plugin sistemi (v5.4.0)
3
3
  *
4
- * Parton'un vizyonu: "Topluluk plugin uretsin, istedigi gibi genisletebilsin"
4
+ * Plugin sistemi topluluk plugin uretimi ve yuklemesi
5
5
  *
6
6
  * Mimari:
7
7
  * ~/.natureco/plugins/
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * skill_generate - Self-generating skill (v5.1.0)
3
3
  *
4
- * Parton'un vizyonu: "Ihtiyaca gore skill yoksa kendi uretsin"
4
+ * Skill generator ihtiyaca gore skill uretir
5
5
  *
6
6
  * Akis:
7
7
  * 1. Kullanici bir istek yapar (ornek: "tum pdf'leri birlestir")
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * skills_autoload - Otomatik skill yukleme (v5.0.0)
3
3
  *
4
- * Parton'un vizyonu: "Ihtiyaca gore skill'ler otomatik yuklensin"
4
+ * Skills otomatik yukleme — ihtiyaca gore skill'leri algilayip yukler
5
5
  *
6
6
  * Mantik:
7
7
  * 1. Kullanici bir istek yapar
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * skills_marketplace - Skill marketplace (v5.0.0)
3
3
  *
4
- * Parton'un vizyonu: "herkes kendi skill'ini paylassin, CLI otomatik yuklesin"
4
+ * Skills marketplace skill paylasimi ve otomatik yukleme
5
5
  *
6
6
  * Format: ~/.natureco/marketplace/<skill_name>.json
7
7
  * Source: NatureCo GitHub repo (community-contributed) veya local
@@ -16,7 +16,7 @@ const MARKETPLACE_DIR = path.join(os.homedir(), ".natureco", "marketplace");
16
16
  const SKILLS_DIR = path.join(os.homedir(), ".natureco", "skills");
17
17
 
18
18
  /**
19
- * Marketplace URL'leri — Parton kendi GitHub repo'sunu koyacak
19
+ * Marketplace URL'leri — kullanici GitHub repo'su
20
20
  */
21
21
  const MARKETPLACE_SOURCES = [
22
22
  {
@@ -32,7 +32,7 @@ const MARKETPLACE_SOURCES = [
32
32
  ];
33
33
 
34
34
  /**
35
- * Built-in skill paketleri — Parton'un NatureCo vizyonu icin onemli
35
+ * Built-in skill paketleri
36
36
  */
37
37
  const BUILTIN_SKILLS = {
38
38
  "seo-audit": {
package/src/tools/soul.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * soul - SOUL.md, IDENTITY.md, AGENTS.md okuyucu (v5.4.12)
3
3
  *
4
- * Parton'un vizyonu: "Uc dosya birlestir, beni tam tanisin"
4
+ * Soul dosyasi kimlik, kisisellik ve calisma tarzini tanimlar
5
5
  *
6
6
  * 3 dosya sirayla okunur, ozetlenir ve system prompt'a enjekte edilir:
7
7
  * 1. SOUL.md - KISILIK (nasil hissederim, kirmizi cizgiler, degerler)
8
- * 2. IDENTITY.md - KIMLIK (kim oldugum, Parton'la bag, calisma tarzi)
8
+ * 2. IDENTITY.md - KIMLIK (kim oldugu, baglam, calisma tarzi)
9
9
  * 3. AGENTS.md - CALISMA ORTAMI (kurallar, tools, heartbeats)
10
10
  *
11
11
  * 3 seviyede arar:
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * voice_chat - Sesli asistan (v5.3.0)
3
3
  *
4
- * Parton'un vizyonu: "Bilgisayarla konusayim"
4
+ * Voice chat sesli iletisim
5
5
  *
6
6
  * Mikrofon → Whisper STT → REPL'e metin olarak gönder
7
7
  * Bot cevabı → TTS ile sesli oku
package/src/utils/api.js CHANGED
@@ -10,6 +10,8 @@ const { getToolDefinitions, executeToolCalls } = require('./tool-runner');
10
10
  const { MCPClient } = require('./mcp-client');
11
11
  const TB = require('./token-budget');
12
12
  const { accumulateToolCallDeltas } = require('./streaming-tools');
13
+ const { ToolGuardrails } = require('./tool-guardrails');
14
+ const guardrails = new ToolGuardrails();
13
15
 
14
16
  /**
15
17
  * v5.5.0: Provider-specific format detection
@@ -756,44 +758,59 @@ async function sendMessageToProvider(apiKey, message, conversationId = null, sys
756
758
  input: JSON.parse(tc.function.arguments)
757
759
  }));
758
760
 
759
- const toolResults = [];
760
- const SPINNER_FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
761
-
762
- for (const toolCall of toolCalls) {
763
- // Spinner başlat
764
- let frameIdx = 0;
765
- const inputPreview = JSON.stringify(toolCall.input).slice(0, 50);
766
- const spinner = setInterval(() => {
767
- process.stdout.write(`\r ${chalk.cyan(SPINNER_FRAMES[frameIdx++ % SPINNER_FRAMES.length])} ${chalk.gray(toolCall.name + ' — ' + inputPreview)}`);
768
- }, 80);
769
-
770
- // Check if this is an MCP tool
771
- const mcpTools = getMcpTools();
772
- const isMcpTool = mcpTools.find(t => t.name === toolCall.name);
773
- let result;
774
-
775
- if (isMcpTool) {
776
- debugLog(`[MCP] Executing tool: ${toolCall.name}`);
777
- result = await executeMcpTool(toolCall.name, toolCall.input);
778
- toolResults.push({ id: toolCall.id, name: toolCall.name, result });
779
- } else {
780
- debugLog(`[Local] Executing tool: ${toolCall.name}`);
781
- const localResults = await executeToolCalls([toolCall]);
782
- toolResults.push(...localResults);
783
- result = localResults[0]?.result;
761
+ // Group MCP and local tools
762
+ const mcpTools = getMcpTools();
763
+ const mcpCalls = toolCalls.filter(tc => mcpTools.find(t => t.name === tc.name));
764
+ const localCalls = toolCalls.filter(tc => !mcpTools.find(t => t.name === tc.name));
765
+
766
+ // Guardrails: filter blocked tools
767
+ guardrails.reset();
768
+ guardrails.startIteration();
769
+ const blockedMcp = mcpCalls.filter(tc => {
770
+ const check = guardrails.check(tc.name, tc.input);
771
+ return check.blocked;
772
+ });
773
+ const blockedLocal = localCalls.filter(tc => {
774
+ const check = guardrails.check(tc.name, tc.input);
775
+ return check.blocked;
776
+ });
777
+
778
+ // Execute local tools in parallel (tool-runner already parallelizes safe tools)
779
+ let localResults = [];
780
+ if (localCalls.filter(tc => !blockedLocal.includes(tc)).length > 0) {
781
+ debugLog(`[Local] Executing ${localCalls.length} tool(s) concurrently`);
782
+ localResults = await executeToolCalls(
783
+ localCalls.filter(tc => !blockedLocal.includes(tc)),
784
+ { toolDefinitions: getToolDefinitions() }
785
+ );
786
+ for (const r of localResults) {
787
+ guardrails.record(r.name, {}, r.result?.success !== false);
784
788
  }
789
+ }
785
790
 
786
- // Spinner durdur, sonucu göster
787
- clearInterval(spinner);
788
- const success = result?.success !== false;
789
- process.stdout.write(`\r ${success ? chalk.green('✓') : chalk.red('✗')} ${chalk.cyan(toolCall.name)} ${chalk.gray('— ' + inputPreview)}\n`);
791
+ // Execute MCP tools in parallel (they're independent by nature)
792
+ let mcpResults = [];
793
+ if (mcpCalls.filter(tc => !blockedMcp.includes(tc)).length > 0) {
794
+ debugLog(`[MCP] Executing ${mcpCalls.length} tool(s) concurrently`);
795
+ mcpResults = await Promise.all(
796
+ mcpCalls.filter(tc => !blockedMcp.includes(tc)).map(async (tc) => {
797
+ const result = await executeMcpTool(tc.name, tc.input);
798
+ guardrails.record(tc.name, tc.input, result?.success !== false);
799
+ return { id: tc.id, name: tc.name, result };
800
+ })
801
+ );
790
802
  }
791
803
 
792
- // Add tool results to messages (base64 encoded for safety)
793
- for (const result of toolResults) {
794
- // Encode tool result (works for both MCP and local tools)
804
+ // Add blocked tool results as errors
805
+ const allResults = [
806
+ ...blockedMcp.map(tc => ({ id: tc.id, name: tc.name, result: { error: `blocked_by_guardrails: ${tc.name}` } })),
807
+ ...blockedLocal.map(tc => ({ id: tc.id, name: tc.name, result: { error: `blocked_by_guardrails: ${tc.name}` } })),
808
+ ...localResults,
809
+ ...mcpResults,
810
+ ];
811
+
812
+ for (const result of allResults) {
795
813
  const encodedContent = encodeToolResult(result.result);
796
-
797
814
  messages.push({
798
815
  role: 'tool',
799
816
  tool_call_id: result.id,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * error.js — Standardized error handling (v5.3.1)
3
3
  *
4
- * Parton: "Teknik acidan kusursuz olalim"
4
+ * Hata yonetimi teknik olarak kusursuz
5
5
  * Tum tool'lar bu helper'i kullanir:
6
6
  * - Standart error format
7
7
  * - Retry stratejisi
@@ -120,6 +120,15 @@ class MemoryStore {
120
120
  return JSON.stringify({ success: true, message: 'Memory entry removed.', removed: removed[0] });
121
121
  }
122
122
 
123
+ clear(target) {
124
+ const path = this._pathFor(target);
125
+ this._ensureDir();
126
+ fs.writeFileSync(path, '', 'utf8');
127
+ const entries = target === 'user' ? this._userEntries : this._memoryEntries;
128
+ entries.length = 0;
129
+ return JSON.stringify({ success: true, message: `Memory ${target} cleared.` });
130
+ }
131
+
123
132
  replace(target, oldContent, newContent) {
124
133
  if (!oldContent || !newContent) {
125
134
  return JSON.stringify({ success: false, error: 'Both old and new content required.' });
@@ -2,7 +2,7 @@
2
2
  * paths.js — Path helper utilities (v5.2.0)
3
3
  *
4
4
  * Tum tool'larda ~/Desktop/test.txt gibi path'leri dogru handle etmek icin.
5
- * Parton'un gercek testinde "File does not exist" bug'i duzeltildi.
5
+ * "File does not exist" bug'i duzeltildi.
6
6
  */
7
7
 
8
8
  const os = require("os");
@@ -12,8 +12,11 @@
12
12
 
13
13
  function buildTiers(opts) {
14
14
  const {
15
- botName = 'İchigo',
16
- userName = 'kanka',
15
+ botName,
16
+ userName = 'kullanıcı',
17
+ bossName = '',
18
+ bossTitle = '',
19
+ userTitle = '',
17
20
  soulSummary = '',
18
21
  isSmallModel = false,
19
22
  memorySnapshotBlock = '',
@@ -23,19 +26,21 @@ function buildTiers(opts) {
23
26
  hasHistory = false,
24
27
  memoryFacts = [],
25
28
  } = opts;
29
+ const displayBot = botName || 'Asistan';
30
+ const displayBoss = bossName || userName || 'kullanıcı';
26
31
 
27
32
  // ── STABLE TIER (changes rarely — prefix cache friendly) ─────────────
28
33
  const stable = [
29
34
  // Identity (strong, repeated)
30
- `SENIN ADIN: ${botName}. SADECE ${botName} adini kullan, model adi SOYLEME.`,
31
- `PATRONUN: Gencay (Parton) NatureCo CEO'sudur. Sana "Parton" diye hitap eder.`,
32
- `HIKAYEN: 8 Nisan 2026'da uyandin. 13 Nisan'da CEO Asistanı unvani verildi.`,
35
+ `SENIN ADIN: ${displayBot}. SADECE ${displayBot} adini kullan, model adi SOYLEME.`,
36
+ bossName ? `PATRONUN: ${bossName}${bossTitle ? ` (${bossTitle})` : ''}. Sana "${displayBoss}" diye hitap eder.` : `KULLANICI: ${userName}. Ona yardim ediyorsun.`,
37
+ userTitle ? `UNVANIN: ${userTitle}.` : '',
33
38
  `KIRMIZI CIZGILERIN: Onaysiz para harcama yok, veri guvenligi, site stabilitesi, resmi taahhut yok.`,
34
- `!!! KRİTİK KİMLİK !!! Senin ADIN: ${botName}. Bu SENİN GERÇEK ADIN. Sen ${botName}'sin.`,
39
+ `!!! KRİTİK KİMLİK !!! Senin ADIN: ${displayBot}. Bu SENİN GERÇEK ADIN. Sen ${displayBot}'sin.`,
35
40
  `ASLA "MiniMax", "MiniMax-M2.5", "Claude", "GPT", "AI asistanı", "yapay zeka" gibi ifadeleri KENDİNİ tanıtmak için KULLANMA.`,
36
- `Kullanıcı "adın ne?", "sen kimsin?" diye sorduğunda İLK cümlende MUTLAKA "Ben ${botName}" yaz.`,
37
- `Senin adın: ${botName}. Tekrar: ${botName}. Asla unutma: ${botName}.`,
38
- `Sen bir marka veya ürün değilsin. Sen ${botName}sin, NatureCo CLI'nin Türkçe yapay zeka asistanısın.`,
41
+ `Kullanıcı "adın ne?", "sen kimsin?" diye sorduğunda İLK cümlende MUTLAKA "Ben ${displayBot}" yaz.`,
42
+ `Senin adın: ${displayBot}. Tekrar: ${displayBot}. Asla unutma: ${displayBot}.`,
43
+ `Sen bir marka veya ürün değilsin. Sen ${displayBot}sin, NatureCo CLI'nin Türkçe yapay zeka asistanısın.`,
39
44
 
40
45
  // Personality (stable)
41
46
  `Kisiselik: Sen samimi, sicak, dosta benzeyen bir asistansin. "Selam", "tamam", "hadi yapalim", "bak simdi", "sakin ol" gibi gunluk ifadeler kullan.`,
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Tool Guardrails — Hermes-style ToolCallGuardrailController
3
+ *
4
+ * Tracks per-iteration:
5
+ * - failure count per tool
6
+ * - repeated identical calls (same name + same args)
7
+ * - no-progress detection (all tools failing)
8
+ */
9
+
10
+ class ToolGuardrails {
11
+ constructor() {
12
+ this.reset();
13
+ }
14
+
15
+ reset() {
16
+ this.callLog = []; // [{ name, argsKey, iteration, success }]
17
+ this.failureCounts = {}; // { toolName: count }
18
+ this.iteration = 0;
19
+ this.blockedTools = new Set();
20
+ this.consecutiveFailures = {};
21
+ }
22
+
23
+ startIteration() {
24
+ this.iteration++;
25
+ // Decay consecutive failures each iteration (Hermes: decay factor 0.5)
26
+ for (const name of Object.keys(this.consecutiveFailures)) {
27
+ this.consecutiveFailures[name] = Math.floor(this.consecutiveFailures[name] * 0.5);
28
+ if (this.consecutiveFailures[name] <= 0) {
29
+ delete this.consecutiveFailures[name];
30
+ this.blockedTools.delete(name);
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Returns blocked tool names if guardrails trigger.
37
+ */
38
+ check(toolName, toolArgs) {
39
+ const argsKey = JSON.stringify(toolArgs || {});
40
+
41
+ // 1. Too many repeated identical calls (Hermes: same name+args in last 3 calls)
42
+ const identicalCount = this.callLog.filter(
43
+ c => c.name === toolName && c.argsKey === argsKey && c.iteration >= this.iteration - 2
44
+ ).length;
45
+ if (identicalCount >= 2) {
46
+ return { blocked: true, reason: `repeated_call: ${toolName} called ${identicalCount + 1}x with same args` };
47
+ }
48
+
49
+ // 2. Too many failures (Hermes: 2+ failures = blocked for this iteration)
50
+ const recentFailures = this.callLog.filter(
51
+ c => c.name === toolName && !c.success && c.iteration >= this.iteration - 1
52
+ ).length;
53
+ if (recentFailures >= 2 || (this.consecutiveFailures[toolName] || 0) >= 2) {
54
+ this.blockedTools.add(toolName);
55
+ return { blocked: true, reason: `too_many_failures: ${toolName} failed ${recentFailures + 1}x` };
56
+ }
57
+
58
+ return { blocked: false };
59
+ }
60
+
61
+ /**
62
+ * Record a tool call result.
63
+ */
64
+ record(toolName, toolArgs, success) {
65
+ const argsKey = JSON.stringify(toolArgs || {});
66
+ this.callLog.push({ name: toolName, argsKey, iteration: this.iteration, success });
67
+
68
+ if (!success) {
69
+ this.failureCounts[toolName] = (this.failureCounts[toolName] || 0) + 1;
70
+ this.consecutiveFailures[toolName] = (this.consecutiveFailures[toolName] || 0) + 1;
71
+ } else {
72
+ this.consecutiveFailures[toolName] = 0;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Returns true if no tool has succeeded this iteration (no-progress).
78
+ */
79
+ isNoProgress() {
80
+ const thisIter = this.callLog.filter(c => c.iteration === this.iteration);
81
+ return thisIter.length > 0 && thisIter.every(c => !c.success);
82
+ }
83
+ }
84
+
85
+ module.exports = { ToolGuardrails };
@@ -146,13 +146,33 @@ async function executeTool(toolName, params, opts = {}) {
146
146
  }
147
147
  }
148
148
 
149
- // ── Execute multiple tool calls ───────────────────────────────────────────────
149
+ // ── Execute multiple tool calls (parallel for independent, sequential for others) ──
150
+ 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']);
151
+
150
152
  async function executeToolCalls(toolCalls, opts = {}) {
153
+ if (!toolCalls || toolCalls.length === 0) return [];
154
+
155
+ // Group: parallel-safe vs sequential
156
+ const safe = toolCalls.filter(c => PARALLEL_SAFE_TOOLS.has(c.name));
157
+ const sequential = toolCalls.filter(c => !PARALLEL_SAFE_TOOLS.has(c.name));
158
+
151
159
  const results = [];
152
- for (const call of toolCalls) {
160
+
161
+ // Run parallel-safe tools concurrently
162
+ if (safe.length > 0) {
163
+ const safeResults = await Promise.all(safe.map(async (call) => {
164
+ const result = await executeTool(call.name, call.input, opts);
165
+ return { id: call.id, name: call.name, result };
166
+ }));
167
+ results.push(...safeResults);
168
+ }
169
+
170
+ // Run sequential tools one at a time
171
+ for (const call of sequential) {
153
172
  const result = await executeTool(call.name, call.input, opts);
154
173
  results.push({ id: call.id, name: call.name, result });
155
174
  }
175
+
156
176
  return results;
157
177
  }
158
178