wave-agent-sdk 0.11.5 → 0.11.7

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.
Files changed (110) hide show
  1. package/builtin/skills/init/SKILL.md +2 -0
  2. package/builtin/skills/settings/SKILLS.md +3 -2
  3. package/builtin/skills/settings/SUBAGENTS.md +1 -3
  4. package/dist/agent.d.ts +6 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +18 -1
  7. package/dist/constants/tools.d.ts +1 -1
  8. package/dist/constants/tools.d.ts.map +1 -1
  9. package/dist/constants/tools.js +1 -1
  10. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  11. package/dist/managers/MemoryRuleManager.js +1 -9
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +22 -3
  14. package/dist/managers/mcpManager.d.ts.map +1 -1
  15. package/dist/managers/mcpManager.js +32 -13
  16. package/dist/managers/messageManager.d.ts +13 -5
  17. package/dist/managers/messageManager.d.ts.map +1 -1
  18. package/dist/managers/messageManager.js +62 -34
  19. package/dist/managers/permissionManager.js +4 -4
  20. package/dist/managers/pluginManager.d.ts.map +1 -1
  21. package/dist/managers/pluginManager.js +4 -2
  22. package/dist/managers/slashCommandManager.d.ts +2 -0
  23. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  24. package/dist/managers/slashCommandManager.js +98 -4
  25. package/dist/managers/toolManager.d.ts.map +1 -1
  26. package/dist/managers/toolManager.js +8 -2
  27. package/dist/prompts/index.d.ts +2 -0
  28. package/dist/prompts/index.d.ts.map +1 -1
  29. package/dist/prompts/index.js +5 -0
  30. package/dist/services/GitService.d.ts +1 -0
  31. package/dist/services/GitService.d.ts.map +1 -1
  32. package/dist/services/GitService.js +16 -0
  33. package/dist/services/MarketplaceService.d.ts +7 -0
  34. package/dist/services/MarketplaceService.d.ts.map +1 -1
  35. package/dist/services/MarketplaceService.js +321 -252
  36. package/dist/services/aiService.d.ts +34 -0
  37. package/dist/services/aiService.d.ts.map +1 -1
  38. package/dist/services/aiService.js +124 -1
  39. package/dist/services/initializationService.d.ts.map +1 -1
  40. package/dist/services/initializationService.js +18 -0
  41. package/dist/tools/agentTool.js +3 -3
  42. package/dist/tools/bashTool.d.ts.map +1 -1
  43. package/dist/tools/bashTool.js +4 -4
  44. package/dist/tools/editTool.d.ts.map +1 -1
  45. package/dist/tools/editTool.js +2 -0
  46. package/dist/tools/globTool.d.ts.map +1 -1
  47. package/dist/tools/globTool.js +15 -3
  48. package/dist/tools/grepTool.d.ts.map +1 -1
  49. package/dist/tools/grepTool.js +38 -12
  50. package/dist/tools/readTool.d.ts.map +1 -1
  51. package/dist/tools/readTool.js +61 -0
  52. package/dist/tools/skillTool.js +2 -2
  53. package/dist/tools/types.d.ts +16 -0
  54. package/dist/tools/types.d.ts.map +1 -1
  55. package/dist/tools/webFetchTool.d.ts +3 -0
  56. package/dist/tools/webFetchTool.d.ts.map +1 -0
  57. package/dist/tools/webFetchTool.js +171 -0
  58. package/dist/tools/writeTool.d.ts.map +1 -1
  59. package/dist/tools/writeTool.js +2 -0
  60. package/dist/types/commands.d.ts +1 -1
  61. package/dist/types/commands.d.ts.map +1 -1
  62. package/dist/types/messaging.d.ts +1 -0
  63. package/dist/types/messaging.d.ts.map +1 -1
  64. package/dist/utils/bashParser.d.ts +20 -2
  65. package/dist/utils/bashParser.d.ts.map +1 -1
  66. package/dist/utils/bashParser.js +281 -146
  67. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  68. package/dist/utils/convertMessagesForAPI.js +7 -0
  69. package/dist/utils/fileUtils.d.ts +8 -0
  70. package/dist/utils/fileUtils.d.ts.map +1 -1
  71. package/dist/utils/fileUtils.js +52 -0
  72. package/dist/utils/messageOperations.d.ts +12 -3
  73. package/dist/utils/messageOperations.d.ts.map +1 -1
  74. package/dist/utils/messageOperations.js +77 -9
  75. package/package.json +4 -2
  76. package/src/agent.ts +19 -1
  77. package/src/constants/tools.ts +1 -1
  78. package/src/managers/MemoryRuleManager.ts +1 -10
  79. package/src/managers/aiManager.ts +23 -3
  80. package/src/managers/mcpManager.ts +37 -16
  81. package/src/managers/messageManager.ts +76 -38
  82. package/src/managers/permissionManager.ts +4 -4
  83. package/src/managers/pluginManager.ts +4 -2
  84. package/src/managers/slashCommandManager.ts +130 -4
  85. package/src/managers/toolManager.ts +11 -2
  86. package/src/prompts/index.ts +6 -0
  87. package/src/services/GitService.ts +20 -0
  88. package/src/services/MarketplaceService.ts +397 -324
  89. package/src/services/aiService.ts +197 -1
  90. package/src/services/initializationService.ts +38 -0
  91. package/src/tools/agentTool.ts +3 -3
  92. package/src/tools/bashTool.ts +3 -4
  93. package/src/tools/editTool.ts +3 -0
  94. package/src/tools/globTool.ts +16 -3
  95. package/src/tools/grepTool.ts +41 -13
  96. package/src/tools/readTool.ts +69 -0
  97. package/src/tools/skillTool.ts +2 -2
  98. package/src/tools/types.ts +13 -0
  99. package/src/tools/webFetchTool.ts +194 -0
  100. package/src/tools/writeTool.ts +3 -0
  101. package/src/types/commands.ts +1 -1
  102. package/src/types/messaging.ts +1 -0
  103. package/src/utils/bashParser.ts +316 -161
  104. package/src/utils/convertMessagesForAPI.ts +8 -0
  105. package/src/utils/fileUtils.ts +69 -0
  106. package/src/utils/messageOperations.ts +84 -9
  107. package/dist/tools/taskOutputTool.d.ts +0 -3
  108. package/dist/tools/taskOutputTool.d.ts.map +0 -1
  109. package/dist/tools/taskOutputTool.js +0 -198
  110. package/src/tools/taskOutputTool.ts +0 -222
@@ -58,7 +58,8 @@ export function splitBashCommand(command: string): string[] {
58
58
  else if (char === "|" && nextChar === "&") opLen = 2;
59
59
  else if (char === ";") opLen = 1;
60
60
  else if (char === "|") opLen = 1;
61
- else if (char === "&" && nextChar !== ">") opLen = 1;
61
+ else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
62
+ opLen = 1;
62
63
 
63
64
  if (opLen > 0) {
64
65
  // Check if preceded by an odd number of backslashes
@@ -395,10 +396,53 @@ export function hasWriteRedirections(command: string): boolean {
395
396
  }
396
397
 
397
398
  /**
398
- * Alias for hasWriteRedirections, used for semantic clarity in permission checks.
399
+ * Checks if a bash command contains any heredocs (<<, <<-).
399
400
  */
400
- export function isBashWriteRedirect(command: string): boolean {
401
- return hasWriteRedirections(command);
401
+ export function hasHeredoc(command: string): boolean {
402
+ let inSingleQuote = false;
403
+ let inDoubleQuote = false;
404
+ let escaped = false;
405
+
406
+ for (let i = 0; i < command.length; i++) {
407
+ const char = command[i];
408
+
409
+ if (escaped) {
410
+ escaped = false;
411
+ continue;
412
+ }
413
+
414
+ if (char === "\\") {
415
+ escaped = true;
416
+ continue;
417
+ }
418
+
419
+ if (char === "'" && !inDoubleQuote) {
420
+ inSingleQuote = !inSingleQuote;
421
+ continue;
422
+ }
423
+
424
+ if (char === '"' && !inSingleQuote) {
425
+ inDoubleQuote = !inDoubleQuote;
426
+ continue;
427
+ }
428
+
429
+ if (inSingleQuote || inDoubleQuote) {
430
+ continue;
431
+ }
432
+
433
+ if (char === "<" && command[i + 1] === "<") {
434
+ return true;
435
+ }
436
+ }
437
+
438
+ return false;
439
+ }
440
+
441
+ /**
442
+ * Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
443
+ */
444
+ export function isBashHeredocWrite(command: string): boolean {
445
+ return hasHeredoc(command) && hasWriteRedirections(command);
402
446
  }
403
447
 
404
448
  /**
@@ -412,14 +456,160 @@ export const DANGEROUS_COMMANDS = [
412
456
  "chown",
413
457
  "sh",
414
458
  "bash",
459
+ "zsh",
460
+ "fish",
461
+ "pwsh",
462
+ "cmd.exe",
463
+ "powershell.exe",
415
464
  "sudo",
416
465
  "dd",
417
466
  "apt",
418
467
  "apt-get",
419
468
  "yum",
420
469
  "dnf",
470
+ "ssh",
471
+ "scp",
472
+ "sftp",
473
+ "ftp",
474
+ "telnet",
475
+ "nc",
476
+ "netcat",
421
477
  ];
422
478
 
479
+ /**
480
+ * Registry of commands and their expected subcommand depth for smart prefix extraction.
481
+ * For example, 'git: 2' means 'git commit' is a valid prefix, but 'git' alone is not.
482
+ * Multi-word keys can be used for more specific rules.
483
+ */
484
+ export interface ToolRule {
485
+ depth: number;
486
+ scopeFlags?: string[];
487
+ }
488
+
489
+ export const TOOL_RULES: Record<string, ToolRule> = {
490
+ // Node/JS
491
+ npm: { depth: 2, scopeFlags: ["--prefix", "-C", "--registry"] },
492
+ "npm run": { depth: 3, scopeFlags: ["--prefix", "-C", "--registry"] },
493
+ pnpm: { depth: 2, scopeFlags: ["-C", "--dir", "-F", "--filter"] },
494
+ "pnpm run": { depth: 3, scopeFlags: ["-C", "--dir", "-F", "--filter"] },
495
+ yarn: { depth: 2, scopeFlags: ["workspace", "--cwd"] },
496
+ "yarn run": { depth: 3, scopeFlags: ["workspace", "--cwd"] },
497
+ "yarn workspace": { depth: 4, scopeFlags: ["--cwd"] },
498
+ bun: { depth: 2 },
499
+ "bun run": { depth: 3 },
500
+ deno: { depth: 2 },
501
+ "deno run": { depth: 3 },
502
+ "deno task": { depth: 3 },
503
+
504
+ // Git
505
+ git: {
506
+ depth: 2,
507
+ scopeFlags: ["-C", "-c", "--directory", "--work-tree", "--git-dir"],
508
+ },
509
+
510
+ // Python
511
+ python: { depth: 2 },
512
+ python3: { depth: 2 },
513
+ "python -m": { depth: 2 },
514
+ "python3 -m": { depth: 2 },
515
+ "python -m pip install": { depth: 3 },
516
+ "python3 -m pip install": { depth: 3 },
517
+ pip: { depth: 2 },
518
+ pip3: { depth: 2 },
519
+ poetry: { depth: 2 },
520
+ conda: { depth: 2 },
521
+
522
+ // Java
523
+ mvn: { depth: 2 },
524
+ gradle: { depth: 2 },
525
+ java: { depth: 1 },
526
+ "java -jar": { depth: 1 },
527
+
528
+ // Rust & Go
529
+ cargo: { depth: 2 },
530
+ go: { depth: 2 },
531
+
532
+ // Containers & Infrastructure
533
+ docker: { depth: 2 },
534
+ "docker-compose": { depth: 2 },
535
+ kubectl: { depth: 2 },
536
+ terraform: { depth: 2 },
537
+ gcloud: { depth: 2 },
538
+ "gcloud compute": { depth: 4 },
539
+ "gcloud container": { depth: 4 },
540
+ aws: { depth: 2 },
541
+ };
542
+
543
+ /**
544
+ * Registry of dangerous subcommands for specific tools.
545
+ */
546
+ export const DANGEROUS_SUBCOMMANDS: Record<string, string[]> = {
547
+ docker: ["rm", "rmi", "system", "volume", "network", "image", "container"],
548
+ git: ["reset", "clean"],
549
+ npm: ["uninstall", "un", "remove", "rm"],
550
+ pnpm: ["uninstall", "un", "remove", "rm"],
551
+ yarn: ["remove"],
552
+ deno: ["uninstall"],
553
+ bun: ["remove", "rm"],
554
+ };
555
+
556
+ /**
557
+ * Heuristic to determine if a flag takes an argument.
558
+ * If nextArg doesn't start with '-' and isn't a known subcommand, assume it's a flag value.
559
+ */
560
+ function flagTakesArg(flag: string, nextArg: string | undefined): boolean {
561
+ if (!nextArg) return false;
562
+ if (nextArg.startsWith("-")) return false;
563
+ // If it's a common subcommand, it's probably not a flag argument
564
+ const commonSubcommands = [
565
+ "install",
566
+ "add",
567
+ "remove",
568
+ "run",
569
+ "test",
570
+ "build",
571
+ "status",
572
+ "diff",
573
+ "commit",
574
+ "push",
575
+ "pull",
576
+ "checkout",
577
+ "log",
578
+ "fetch",
579
+ "merge",
580
+ "rebase",
581
+ ];
582
+ if (commonSubcommands.includes(nextArg)) return false;
583
+ return true;
584
+ }
585
+
586
+ /**
587
+ * Detects if an argument is a file path or URL.
588
+ */
589
+ function shouldStopAtArg(arg: string): boolean {
590
+ if (!arg) return false;
591
+ // URLs
592
+ if (/^(https?|ftp|ssh|git):\/\//.test(arg)) return true;
593
+ // File paths (starts with /, ./, ../, or ~/)
594
+ if (
595
+ arg.startsWith("/") ||
596
+ arg.startsWith("./") ||
597
+ arg.startsWith("../") ||
598
+ arg.startsWith("~/")
599
+ )
600
+ return true;
601
+ // Common file extensions (but not scoped packages or common subcommands)
602
+ if (
603
+ /\.(ts|js|py|sh|md|txt|json|yml|yaml|html|css|go|rs|java|cpp|c|h|php|rb|pl|sql)$/.test(
604
+ arg,
605
+ ) &&
606
+ !arg.includes("@") &&
607
+ !arg.includes("/")
608
+ )
609
+ return true;
610
+ return false;
611
+ }
612
+
423
613
  /**
424
614
  * Extracts a "smart prefix" from a bash command based on common developer tools.
425
615
  * Returns null if the command is blacklisted or cannot be safely prefix-matched.
@@ -431,187 +621,152 @@ export function getSmartPrefix(command: string): string | null {
431
621
  // For now, we only support prefix matching for single commands or the first command in a chain
432
622
  // to keep it simple and safe.
433
623
  const firstCommand = parts[0];
434
- let stripped = stripRedirections(stripEnvVars(firstCommand));
435
624
 
436
- // Handle sudo
437
- if (stripped.startsWith("sudo ")) {
438
- stripped = stripped.substring(5).trim();
439
- }
625
+ // Safety check: don't allow heredoc writes
626
+ if (isBashHeredocWrite(firstCommand)) return null;
440
627
 
628
+ const stripped = stripRedirections(stripEnvVars(firstCommand));
441
629
  const tokens = stripped.split(/\s+/);
442
630
  if (tokens.length === 0) return null;
443
631
 
444
- const exe = tokens[0];
445
- const sub = tokens[1];
632
+ const prefixParts: string[] = [];
633
+ let i = 0;
634
+
635
+ // Handle prefix tools like sudo
636
+ const prefixTools = ["sudo", "time", "stdbuf", "timeout"];
637
+ while (i < tokens.length && prefixTools.includes(tokens[i])) {
638
+ prefixParts.push(tokens[i]);
639
+ i++;
640
+ }
641
+
642
+ if (i >= tokens.length) return null;
446
643
 
644
+ const exe = tokens[i];
447
645
  // Blacklist - Hard blacklist for dangerous commands
448
646
  if (DANGEROUS_COMMANDS.includes(exe)) return null;
449
647
 
450
- // Node/JS
451
- if (["npm", "pnpm", "yarn", "deno", "bun"].includes(exe)) {
452
- let currentIdx = 1;
453
- const prefixParts = [exe];
454
-
455
- // Handle workspace/filter flags
456
- if (exe === "pnpm") {
457
- while (
458
- (tokens[currentIdx] === "-F" || tokens[currentIdx] === "--filter") &&
459
- tokens[currentIdx + 1]
460
- ) {
461
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
462
- currentIdx += 2;
648
+ // Find the longest matching rule in TOOL_RULES
649
+ let bestRuleKey = "";
650
+ let rule: ToolRule | undefined;
651
+
652
+ for (const [key, r] of Object.entries(TOOL_RULES)) {
653
+ const keyTokens = key.split(/\s+/);
654
+ let match = true;
655
+ for (let j = 0; j < keyTokens.length; j++) {
656
+ if (tokens[i + j] !== keyTokens[j]) {
657
+ match = false;
658
+ break;
463
659
  }
464
- } else if (
465
- exe === "npm" &&
466
- (tokens[currentIdx] === "--prefix" || tokens[currentIdx] === "-C") &&
467
- tokens[currentIdx + 1]
468
- ) {
469
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
470
- currentIdx += 2;
471
- } else if (
472
- exe === "yarn" &&
473
- tokens[currentIdx] === "workspace" &&
474
- tokens[currentIdx + 1]
475
- ) {
476
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
477
- currentIdx += 2;
478
- }
479
-
480
- const subCommand = tokens[currentIdx];
481
- const safeSubcommands = [
482
- "install",
483
- "i",
484
- "add",
485
- "remove",
486
- "rm",
487
- "uninstall",
488
- "un",
489
- "test",
490
- "t",
491
- "build",
492
- "start",
493
- "dev",
494
- ];
495
-
496
- if (safeSubcommands.includes(subCommand)) {
497
- prefixParts.push(subCommand);
498
- return prefixParts.join(" ");
499
660
  }
500
- if (
501
- (subCommand === "run" || (exe === "deno" && subCommand === "task")) &&
502
- tokens[currentIdx + 1]
503
- ) {
504
- prefixParts.push(subCommand, tokens[currentIdx + 1]);
505
- return prefixParts.join(" ");
661
+ if (match && key.length > bestRuleKey.length) {
662
+ bestRuleKey = key;
663
+ rule = r;
506
664
  }
507
- return null;
508
665
  }
509
666
 
510
- // Git
511
- if (exe === "git") {
512
- let currentIdx = 1;
513
- const prefixParts = [exe];
514
-
515
- // Handle -C <path>
516
- if (tokens[currentIdx] === "-C" && tokens[currentIdx + 1]) {
517
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
518
- currentIdx += 2;
519
- }
520
-
521
- const subCommand = tokens[currentIdx];
522
- const safeGitSubcommands = [
523
- "commit",
524
- "push",
525
- "pull",
526
- "checkout",
527
- "add",
528
- "status",
529
- "diff",
530
- "branch",
531
- "merge",
532
- "rebase",
533
- "log",
534
- "fetch",
535
- "remote",
536
- "stash",
537
- ];
538
-
539
- if (safeGitSubcommands.includes(subCommand)) {
540
- if (subCommand === "branch") {
541
- // Check for destructive flags
542
- const destructiveFlags = ["-d", "-D", "--delete"];
543
- if (tokens.some((t) => destructiveFlags.includes(t))) {
544
- return null;
545
- }
546
- }
547
- prefixParts.push(subCommand);
548
- return prefixParts.join(" ");
667
+ // If no rule found, we don't suggest a prefix
668
+ if (!rule) return null;
669
+
670
+ const depth = rule.depth;
671
+ const scopeFlags = rule.scopeFlags || [];
672
+ let currentDepth = 0;
673
+
674
+ // Safety check: only allow safe subcommands for git
675
+ const safeGitSubcommands = [
676
+ "commit",
677
+ "push",
678
+ "pull",
679
+ "checkout",
680
+ "add",
681
+ "status",
682
+ "diff",
683
+ "branch",
684
+ "merge",
685
+ "rebase",
686
+ "log",
687
+ "fetch",
688
+ "remote",
689
+ "stash",
690
+ ];
691
+
692
+ const destructiveGitFlags = [
693
+ "-d",
694
+ "-D",
695
+ "--delete",
696
+ "--hard",
697
+ "--force",
698
+ "-f",
699
+ ];
700
+
701
+ // Global safety check: scan ALL tokens for dangerous flags/subcommands
702
+ for (let j = i; j < tokens.length; j++) {
703
+ const token = tokens[j];
704
+ if (token.startsWith("-")) {
705
+ if (exe === "git" && destructiveGitFlags.includes(token)) return null;
706
+ } else {
707
+ if (DANGEROUS_SUBCOMMANDS[exe]?.includes(token)) return null;
549
708
  }
550
- return null;
551
709
  }
552
710
 
553
- // Python
554
- if (["python", "python3", "pip", "pip3", "poetry", "conda"].includes(exe)) {
555
- if (exe === "python" || exe === "python3") {
556
- if (tokens[1] === "-m" && tokens[2]) {
557
- if (tokens[2] === "pip" && tokens[3] === "install") {
558
- return `${exe} -m pip install`;
559
- }
560
- return `${exe} -m ${tokens[2]}`;
711
+ // Include all tokens from the best matching rule
712
+ const ruleTokens = bestRuleKey.split(/\s+/);
713
+ for (let j = 0; j < ruleTokens.length; j++) {
714
+ const token = tokens[i];
715
+ if (!token) break;
716
+
717
+ if (token.startsWith("-")) {
718
+ if (exe === "git" && destructiveGitFlags.includes(token)) return null;
719
+ } else {
720
+ if (DANGEROUS_SUBCOMMANDS[exe]?.includes(token)) return null;
721
+ if (
722
+ exe === "git" &&
723
+ currentDepth > 0 &&
724
+ !safeGitSubcommands.includes(token)
725
+ ) {
726
+ return null;
561
727
  }
562
- return null;
563
- }
564
- if (["install", "add", "remove", "test", "run"].includes(sub)) {
565
- return `${exe} ${sub}`;
728
+ currentDepth++;
566
729
  }
567
- return null;
568
- }
569
730
 
570
- // Java
571
- if (["mvn", "gradle"].includes(exe)) {
572
- if (sub && !sub.startsWith("-")) {
573
- return `${exe} ${sub}`;
574
- }
575
- return null;
576
- }
577
- if (exe === "java") {
578
- if (sub === "-jar") return "java -jar";
579
- return "java";
731
+ prefixParts.push(token);
732
+ i++;
580
733
  }
581
734
 
582
- // Rust & Go
583
- if (exe === "cargo") {
584
- if (["build", "test", "run", "add", "check"].includes(sub)) {
585
- return `${exe} ${sub}`;
586
- }
587
- return null;
588
- }
589
- if (exe === "go") {
590
- if (["build", "test", "run", "get", "mod"].includes(sub)) {
591
- return `${exe} ${sub}`;
592
- }
593
- return null;
594
- }
735
+ // Continue until we reach the required depth
736
+ while (i < tokens.length && currentDepth < depth) {
737
+ const token = tokens[i];
595
738
 
596
- // Containers & Infrastructure
597
- if (exe === "docker" || exe === "docker-compose") {
598
- if (["run", "build", "ps", "exec", "up", "down"].includes(sub)) {
599
- return `${exe} ${sub}`;
600
- }
601
- return null;
602
- }
603
- if (exe === "kubectl") {
604
- if (["get", "describe", "apply", "logs"].includes(sub)) {
605
- return `${exe} ${sub}`;
606
- }
607
- return null;
608
- }
609
- if (exe === "terraform") {
610
- if (["plan", "apply", "destroy", "init"].includes(sub)) {
611
- return `${exe} ${sub}`;
739
+ if (token.startsWith("-")) {
740
+ // Safety checks for flags
741
+ if (exe === "git" && destructiveGitFlags.includes(token)) return null;
742
+
743
+ prefixParts.push(token);
744
+ if (scopeFlags.includes(token) || flagTakesArg(token, tokens[i + 1])) {
745
+ if (i + 1 < tokens.length) {
746
+ prefixParts.push(tokens[++i]);
747
+ }
748
+ }
749
+ } else {
750
+ // Safety checks for subcommands
751
+ if (DANGEROUS_SUBCOMMANDS[exe]?.includes(token)) return null;
752
+ if (
753
+ exe === "git" &&
754
+ currentDepth > 0 &&
755
+ !safeGitSubcommands.includes(token)
756
+ ) {
757
+ return null;
758
+ }
759
+
760
+ // Stop at data/paths
761
+ if (shouldStopAtArg(token) && currentDepth > 0) break;
762
+
763
+ prefixParts.push(token);
764
+ currentDepth++;
612
765
  }
613
- return null;
766
+ i++;
614
767
  }
615
768
 
616
- return null;
769
+ if (currentDepth < depth) return null;
770
+
771
+ return prefixParts.join(" ");
617
772
  }
@@ -230,6 +230,14 @@ export function convertMessagesForAPI(
230
230
  });
231
231
  });
232
232
  }
233
+
234
+ // If there is a tool block in user message, add its result
235
+ if (block.type === "tool" && block.stage === "end" && block.result) {
236
+ contentParts.push({
237
+ type: "text",
238
+ text: `<local-command-stdout>\n${stripAnsiColors(block.result)}\n</local-command-stdout>`,
239
+ });
240
+ }
233
241
  });
234
242
 
235
243
  // Only add user message if there is meaningful content
@@ -3,6 +3,7 @@ import { createReadStream } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { execSync } from "node:child_process";
5
5
  import { homedir } from "node:os";
6
+ import { glob } from "glob";
6
7
 
7
8
  /**
8
9
  * Reads the first line of a file efficiently using Node.js readline.
@@ -158,3 +159,71 @@ export async function ensureGlobalGitIgnore(pattern: string): Promise<void> {
158
159
  // Ignore errors
159
160
  }
160
161
  }
162
+
163
+ /**
164
+ * Simple Levenshtein distance implementation
165
+ */
166
+ function levenshtein(a: string, b: string): number {
167
+ const matrix = Array.from({ length: a.length + 1 }, () =>
168
+ Array.from({ length: b.length + 1 }, () => 0),
169
+ );
170
+
171
+ for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
172
+ for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
173
+
174
+ for (let i = 1; i <= a.length; i++) {
175
+ for (let j = 1; j <= b.length; j++) {
176
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
177
+ matrix[i][j] = Math.min(
178
+ matrix[i - 1][j] + 1,
179
+ matrix[i][j - 1] + 1,
180
+ matrix[i - 1][j - 1] + cost,
181
+ );
182
+ }
183
+ }
184
+
185
+ return matrix[a.length][b.length];
186
+ }
187
+
188
+ /**
189
+ * Suggests similar paths if a file is not found.
190
+ */
191
+ export async function suggestPathUnderCwd(
192
+ targetPath: string,
193
+ workdir: string,
194
+ ): Promise<string[]> {
195
+ try {
196
+ const allFiles = await glob("**/*", {
197
+ cwd: workdir,
198
+ nodir: true,
199
+ dot: true,
200
+ ignore: ["**/.git/**", "**/node_modules/**"],
201
+ });
202
+
203
+ const targetBasename = path.basename(targetPath);
204
+ const suggestions = allFiles
205
+ .map((file) => ({
206
+ path: file,
207
+ distance: levenshtein(targetBasename, path.basename(file)),
208
+ }))
209
+ .filter((item) => item.distance <= 3) // Threshold for similarity
210
+ .sort((a, b) => a.distance - b.distance)
211
+ .slice(0, 5)
212
+ .map((item) => item.path);
213
+
214
+ return suggestions;
215
+ } catch {
216
+ return [];
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Uses fuzzy matching to find the intended file.
222
+ */
223
+ export async function findSimilarFile(
224
+ targetPath: string,
225
+ workdir: string,
226
+ ): Promise<string | null> {
227
+ const suggestions = await suggestPathUnderCwd(targetPath, workdir);
228
+ return suggestions.length > 0 ? suggestions[0] : null;
229
+ }