wormclaude 1.0.13 → 1.0.15

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/dist/tools.js CHANGED
@@ -8,10 +8,14 @@ import * as os from 'node:os';
8
8
  import * as path from 'node:path';
9
9
  import { loadConfig } from './api.js';
10
10
  import { runAgentLoop } from './agent.js';
11
+ import { resolveSubagent, subagentTypesHint } from './subagents.js';
12
+ import { saveMemoryFact } from './memory.js';
11
13
  import { tasks } from './tasks.js';
12
14
  import { getMcpToolSchemas, callMcpTool } from './mcp.js';
13
15
  import { getSkills, getSkill, buildSkillPrompt } from './skills.js';
14
16
  import { getExcludedTools } from './extensions.js';
17
+ import { checkCommand } from './cmdsec.js';
18
+ import * as Diff from 'diff';
15
19
  import * as computer from './computer.js';
16
20
  // Agent/alt-agent araçlarının backend'e ulaşması için config. cli.tsx başlangıçta
17
21
  // setToolConfig ile aynı (mutable) config nesnesini verir → /config değişiklikleri görülür.
@@ -299,12 +303,13 @@ export const toolSchemas = [
299
303
  type: 'function',
300
304
  function: {
301
305
  name: 'Agent',
302
- description: AGENT_DESCRIPTION,
306
+ description: AGENT_DESCRIPTION + '\n\nAvailable subagent_type values (specialized prompt + restricted tools):\n' + subagentTypesHint(),
303
307
  parameters: {
304
308
  type: 'object',
305
309
  properties: {
306
310
  description: { type: 'string', description: 'A short (3-5 word) description of the task' },
307
311
  prompt: { type: 'string', description: 'The detailed, self-contained task for the sub-agent to perform' },
312
+ subagent_type: { type: 'string', description: 'Optional: specialized agent to use (e.g. general-purpose, security-recon, code-explorer). Selects a tailored system prompt and a restricted tool set. Omit for a general sub-agent.' },
308
313
  run_in_background: { type: 'boolean', description: 'Run the sub-agent in the background and return a task id (default false)' },
309
314
  },
310
315
  required: ['description', 'prompt'],
@@ -325,6 +330,21 @@ export const toolSchemas = [
325
330
  },
326
331
  },
327
332
  },
333
+ {
334
+ type: 'function',
335
+ function: {
336
+ name: 'SaveMemory',
337
+ description: 'Save a single concise fact to long-term memory so it persists across sessions. Use when the user explicitly asks you to remember something, or when you detect a durable preference, decision, project convention, or constraint worth keeping. Keep the fact short and self-contained. Do NOT use for transient or session-only details.',
338
+ parameters: {
339
+ type: 'object',
340
+ properties: {
341
+ fact: { type: 'string', description: 'The specific, self-contained fact to remember (e.g. "User prefers Turkish responses", "Project uses pnpm not npm").' },
342
+ scope: { type: 'string', enum: ['project', 'global'], description: 'project = this project only (default), global = all projects.' },
343
+ },
344
+ required: ['fact'],
345
+ },
346
+ },
347
+ },
328
348
  {
329
349
  type: 'function',
330
350
  function: {
@@ -538,6 +558,7 @@ const TOOL_META = {
538
558
  Scroll: { needsPermission: true, validate: (a) => (a && a.direction ? null : 'direction gerekli') },
539
559
  WebSearch: { readOnly: true, needsPermission: true, validate: (a) => (a && a.query ? null : 'query gerekli') },
540
560
  TodoWrite: { readOnly: true, validate: (a) => (a && Array.isArray(a.todos) ? null : 'todos dizisi gerekli') },
561
+ SaveMemory: { validate: (a) => (a && a.fact && String(a.fact).trim() ? null : 'fact gerekli') },
541
562
  PowerShell: { needsPermission: true, validate: (a) => (a && a.command ? null : 'command gerekli') },
542
563
  NotebookEdit: { needsPermission: true, validate: (a) => (a && a.notebook_path ? null : 'notebook_path gerekli') },
543
564
  REPL: { needsPermission: true, validate: (a) => (a && a.language && a.code ? null : 'language ve code gerekli') },
@@ -602,6 +623,17 @@ async function execOne(call, hooks) {
602
623
  if (planMode && !isReadOnly(call.name)) {
603
624
  return { ok: false, output: 'Plan modunda — yazma/komut engellendi. Önce ExitPlanMode ile planı onaylat.', args };
604
625
  }
626
+ // 3.5) Komut güvenliği (Bash/PowerShell) — cmdsec: deny→blokla, allow→izinsiz, confirm→izin akışı
627
+ if ((call.name === 'Bash' || call.name === 'PowerShell') && args && args.command) {
628
+ const chk = checkCommand(String(args.command));
629
+ if (chk.decision === 'deny') {
630
+ return { ok: false, output: `⛔ Güvenlik: komut engellendi — ${chk.reason || 'tehlikeli komut'}`, args };
631
+ }
632
+ if (chk.decision === 'allow') {
633
+ const res = await executeTool(call.name, args);
634
+ return { ...res, args };
635
+ }
636
+ }
605
637
  // 4) İzin (gerekiyorsa)
606
638
  if (needsPermission(call.name) && hooks?.confirm) {
607
639
  const decision = await hooks.confirm(call, args);
@@ -651,13 +683,53 @@ export async function executeToolCalls(calls, hooks) {
651
683
  return results;
652
684
  }
653
685
  // Alt-agent'a verilecek araç seti (özyineleme/iç içe agent engellenir).
654
- function subAgentTools() {
655
- return allToolSchemas().filter((t) => t.function.name !== 'Agent' && t.function.name !== 'Skill');
686
+ // allow verilirse yalnız o adlardaki araçlar bırakılır (uzman ajan tool-kısıtı).
687
+ function subAgentTools(allow) {
688
+ let list = allToolSchemas().filter((t) => t.function.name !== 'Agent' && t.function.name !== 'Skill');
689
+ if (allow && allow.length) {
690
+ const set = new Set(allow);
691
+ const filtered = list.filter((t) => set.has(t.function.name));
692
+ if (filtered.length)
693
+ list = filtered; // boş kalırsa kısıtı yok say (güvenli geri dönüş)
694
+ }
695
+ return list;
656
696
  }
657
697
  const SUBAGENT_SYSTEM = 'You are a WormClaude sub-agent: an autonomous worker spawned to complete one specific task. ' +
658
698
  'Use your tools (Bash, Read, Write, Edit, Glob, Grep, WebFetch, TaskOutput) to do the work, ' +
659
699
  'then return a concise final report of what you did and any results requested. ' +
660
700
  'You have no memory beyond this task. Be thorough and finish the task end-to-end.';
701
+ // Levenshtein mesafesi (küçük, dep'siz) — bilinmeyen tool adına en yakın öneriler.
702
+ function levenshtein(a, b) {
703
+ const m = a.length, n = b.length;
704
+ if (!m)
705
+ return n;
706
+ if (!n)
707
+ return m;
708
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
709
+ let cur = new Array(n + 1);
710
+ for (let i = 1; i <= m; i++) {
711
+ cur[0] = i;
712
+ for (let j = 1; j <= n; j++) {
713
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
714
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
715
+ }
716
+ [prev, cur] = [cur, prev];
717
+ }
718
+ return prev[n];
719
+ }
720
+ // Bilinmeyen tool adı için " Did you mean 'X'?" önerisi (en yakın ≤3, makul mesafe).
721
+ function suggestTools(unknown) {
722
+ const names = allToolSchemas().map((t) => t.function.name);
723
+ const ranked = names
724
+ .map((n) => ({ n, d: levenshtein(unknown.toLowerCase(), n.toLowerCase()) }))
725
+ .filter((x) => x.d <= Math.max(3, Math.floor(unknown.length / 2)))
726
+ .sort((a, b) => a.d - b.d)
727
+ .slice(0, 3)
728
+ .map((x) => `"${x.n}"`);
729
+ if (!ranked.length)
730
+ return '';
731
+ return ranked.length > 1 ? ` Did you mean one of: ${ranked.join(', ')}?` : ` Did you mean ${ranked[0]}?`;
732
+ }
661
733
  export function toolLabel(name, args) {
662
734
  try {
663
735
  if (name === 'Bash')
@@ -675,9 +747,11 @@ export function toolLabel(name, args) {
675
747
  if (name === 'WebFetch')
676
748
  return `WebFetch(${args.url})`;
677
749
  if (name === 'Agent')
678
- return `Agent(${args.description || ''}${args.run_in_background ? ', bg' : ''})`;
750
+ return `Agent(${args.description || ''}${args.subagent_type ? ':' + args.subagent_type : ''}${args.run_in_background ? ', bg' : ''})`;
679
751
  if (name === 'TaskOutput')
680
752
  return `TaskOutput(${args.task_id})`;
753
+ if (name === 'SaveMemory')
754
+ return `SaveMemory(${String(args.fact || '').slice(0, 50)})`;
681
755
  if (name === 'Skill')
682
756
  return `Skill(${args.name})`;
683
757
  if (name === 'WebSearch')
@@ -742,9 +816,9 @@ function walk(dir, out, depth = 0) {
742
816
  function globToRegex(pattern) {
743
817
  let re = pattern
744
818
  .replace(/[.+^${}()|[\]\\]/g, '\\$&')
745
- .replace(/\*\*/g, '')
819
+ .replace(/\*\*/g, '\u0000')
746
820
  .replace(/\*/g, '[^/\\\\]*')
747
- .replace(//g, '.*')
821
+ .replace(/\u0000/g, '.*')
748
822
  .replace(/\?/g, '.');
749
823
  return new RegExp(re + '$', 'i');
750
824
  }
@@ -764,6 +838,23 @@ const TYPE_EXT = {
764
838
  sh: ['sh', 'bash', 'zsh'],
765
839
  };
766
840
  // ── Executor ──────────────────────────────────────────────────────────────────
841
+ // Edit/Write icin +/- satir ozeti (Diff paketiyle).
842
+ function diffStat(oldStr, newStr) {
843
+ try {
844
+ let added = 0, removed = 0;
845
+ for (const part of Diff.diffLines(oldStr || '', newStr || '')) {
846
+ const n = part.count || (part.value ? part.value.split('\n').filter(Boolean).length : 0);
847
+ if (part.added)
848
+ added += n;
849
+ else if (part.removed)
850
+ removed += n;
851
+ }
852
+ return (added || removed) ? ` (+${added} -${removed})` : '';
853
+ }
854
+ catch {
855
+ return '';
856
+ }
857
+ }
767
858
  export async function executeTool(name, args) {
768
859
  try {
769
860
  if (name === 'See') {
@@ -809,8 +900,12 @@ export async function executeTool(name, args) {
809
900
  return { ok: true, output: (out || '(no output)').slice(0, 20000) };
810
901
  }
811
902
  if (name === 'Agent') {
903
+ // Uzman ajan seçimi: subagent_type verilirse özel prompt + tool-kısıtı uygulanır.
904
+ const def = resolveSubagent(args.subagent_type);
905
+ const sysPrompt = def ? def.system : SUBAGENT_SYSTEM;
906
+ const subTools = subAgentTools(def?.tools);
812
907
  const subMessages = [
813
- { role: 'system', content: SUBAGENT_SYSTEM },
908
+ { role: 'system', content: sysPrompt },
814
909
  { role: 'user', content: String(args.prompt || '') },
815
910
  ];
816
911
  if (args.run_in_background) {
@@ -820,7 +915,7 @@ export async function executeTool(name, args) {
820
915
  const { finalText } = await runAgentLoop({
821
916
  config: cfg(),
822
917
  messages: subMessages,
823
- tools: subAgentTools(),
918
+ tools: subTools,
824
919
  executeTool,
825
920
  hooks: {
826
921
  onText: (t) => tasks.append(task.id, t),
@@ -840,7 +935,7 @@ export async function executeTool(name, args) {
840
935
  const { finalText } = await runAgentLoop({
841
936
  config: cfg(),
842
937
  messages: subMessages,
843
- tools: subAgentTools(),
938
+ tools: subTools,
844
939
  executeTool,
845
940
  });
846
941
  return { ok: true, output: finalText || '(sub-agent returned no text)' };
@@ -852,6 +947,16 @@ export async function executeTool(name, args) {
852
947
  const body = t.output.slice(-9000) || '(no output yet)';
853
948
  return { ok: true, output: `[${t.id}] ${t.kind} · ${t.status} · ${t.label}\n\n${body}` };
854
949
  }
950
+ if (name === 'SaveMemory') {
951
+ try {
952
+ const scope = args.scope === 'global' ? 'global' : 'project';
953
+ const file = saveMemoryFact(String(args.fact || ''), scope);
954
+ return { ok: true, output: `Hatıra kaydedildi (${scope}): "${String(args.fact).trim()}" → ${file}` };
955
+ }
956
+ catch (e) {
957
+ return { ok: false, output: `Hatıra kaydedilemedi: ${e?.message || e}` };
958
+ }
959
+ }
855
960
  if (name === 'Read') {
856
961
  const fp = args.file_path;
857
962
  if (!fs.existsSync(fp))
@@ -877,9 +982,16 @@ export async function executeTool(name, args) {
877
982
  if (fs.existsSync(fp) && !readFiles.has(norm(fp)))
878
983
  return { ok: false, output: 'Error: existing file must be read first. Use the Read tool before overwriting.' };
879
984
  fs.mkdirSync(path.dirname(path.resolve(fp)), { recursive: true });
880
- fs.writeFileSync(fp, args.content ?? '');
985
+ const _wnew = args.content ?? '';
986
+ const _wold = (() => { try {
987
+ return fs.readFileSync(fp, 'utf8');
988
+ }
989
+ catch {
990
+ return '';
991
+ } })();
992
+ fs.writeFileSync(fp, _wnew);
881
993
  readFiles.add(norm(fp));
882
- return { ok: true, output: `Wrote ${fp} (${(args.content || '').length} chars)` };
994
+ return { ok: true, output: `Wrote ${fp} (${_wnew.length} chars)${diffStat(_wold, _wnew)}` };
883
995
  }
884
996
  if (name === 'Edit') {
885
997
  const fp = args.file_path;
@@ -900,9 +1012,10 @@ export async function executeTool(name, args) {
900
1012
  ok: false,
901
1013
  output: `Error: old_string is not unique (${count} matches). Provide more surrounding context or set replace_all: true.`,
902
1014
  };
1015
+ const _ebefore = c;
903
1016
  c = args.replace_all ? c.split(oldStr).join(newStr) : c.replace(oldStr, newStr);
904
1017
  fs.writeFileSync(fp, c);
905
- return { ok: true, output: `Edited ${fp}${args.replace_all ? ` (${count} occurrences)` : ''}` };
1018
+ return { ok: true, output: `Edited ${fp}${args.replace_all ? ` (${count} occurrences)` : ''}${diffStat(_ebefore, c)}` };
906
1019
  }
907
1020
  if (name === 'Glob') {
908
1021
  const base = args.path || process.cwd();
@@ -1182,7 +1295,7 @@ export async function executeTool(name, args) {
1182
1295
  }
1183
1296
  if (name.startsWith('mcp__'))
1184
1297
  return await callMcpTool(name, args);
1185
- return { ok: false, output: `Unknown tool: ${name}` };
1298
+ return { ok: false, output: `Unknown tool: ${name}.${suggestTools(name)} Use exactly the registered tool names.` };
1186
1299
  }
1187
1300
  catch (e) {
1188
1301
  return { ok: false, output: `Error: ${e?.message || String(e)}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@modelcontextprotocol/sdk": "^1.29.0",
17
+ "@types/diff": "^7.0.2",
18
+ "diff": "^9.0.0",
17
19
  "ink": "^5.0.1",
18
20
  "ink-spinner": "^5.0.0",
19
21
  "ink-text-input": "^6.0.0",