wave-agent-sdk 0.11.6 → 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 (105) 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/messageManager.d.ts +13 -5
  15. package/dist/managers/messageManager.d.ts.map +1 -1
  16. package/dist/managers/messageManager.js +62 -34
  17. package/dist/managers/pluginManager.d.ts.map +1 -1
  18. package/dist/managers/pluginManager.js +4 -2
  19. package/dist/managers/slashCommandManager.d.ts +2 -0
  20. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  21. package/dist/managers/slashCommandManager.js +98 -4
  22. package/dist/managers/toolManager.d.ts.map +1 -1
  23. package/dist/managers/toolManager.js +8 -2
  24. package/dist/prompts/index.d.ts +2 -0
  25. package/dist/prompts/index.d.ts.map +1 -1
  26. package/dist/prompts/index.js +5 -0
  27. package/dist/services/GitService.d.ts +1 -0
  28. package/dist/services/GitService.d.ts.map +1 -1
  29. package/dist/services/GitService.js +16 -0
  30. package/dist/services/MarketplaceService.d.ts +7 -0
  31. package/dist/services/MarketplaceService.d.ts.map +1 -1
  32. package/dist/services/MarketplaceService.js +321 -252
  33. package/dist/services/aiService.d.ts +34 -0
  34. package/dist/services/aiService.d.ts.map +1 -1
  35. package/dist/services/aiService.js +124 -1
  36. package/dist/services/initializationService.d.ts.map +1 -1
  37. package/dist/services/initializationService.js +18 -0
  38. package/dist/tools/agentTool.js +3 -3
  39. package/dist/tools/bashTool.d.ts.map +1 -1
  40. package/dist/tools/bashTool.js +4 -4
  41. package/dist/tools/editTool.d.ts.map +1 -1
  42. package/dist/tools/editTool.js +2 -0
  43. package/dist/tools/globTool.d.ts.map +1 -1
  44. package/dist/tools/globTool.js +15 -3
  45. package/dist/tools/grepTool.d.ts.map +1 -1
  46. package/dist/tools/grepTool.js +38 -12
  47. package/dist/tools/readTool.d.ts.map +1 -1
  48. package/dist/tools/readTool.js +61 -0
  49. package/dist/tools/skillTool.js +2 -2
  50. package/dist/tools/types.d.ts +16 -0
  51. package/dist/tools/types.d.ts.map +1 -1
  52. package/dist/tools/webFetchTool.d.ts +3 -0
  53. package/dist/tools/webFetchTool.d.ts.map +1 -0
  54. package/dist/tools/webFetchTool.js +171 -0
  55. package/dist/tools/writeTool.d.ts.map +1 -1
  56. package/dist/tools/writeTool.js +2 -0
  57. package/dist/types/commands.d.ts +1 -1
  58. package/dist/types/commands.d.ts.map +1 -1
  59. package/dist/types/messaging.d.ts +1 -0
  60. package/dist/types/messaging.d.ts.map +1 -1
  61. package/dist/utils/bashParser.d.ts +14 -0
  62. package/dist/utils/bashParser.d.ts.map +1 -1
  63. package/dist/utils/bashParser.js +243 -142
  64. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  65. package/dist/utils/convertMessagesForAPI.js +7 -0
  66. package/dist/utils/fileUtils.d.ts +8 -0
  67. package/dist/utils/fileUtils.d.ts.map +1 -1
  68. package/dist/utils/fileUtils.js +52 -0
  69. package/dist/utils/messageOperations.d.ts +12 -3
  70. package/dist/utils/messageOperations.d.ts.map +1 -1
  71. package/dist/utils/messageOperations.js +77 -9
  72. package/package.json +4 -2
  73. package/src/agent.ts +19 -1
  74. package/src/constants/tools.ts +1 -1
  75. package/src/managers/MemoryRuleManager.ts +1 -10
  76. package/src/managers/aiManager.ts +23 -3
  77. package/src/managers/messageManager.ts +76 -38
  78. package/src/managers/pluginManager.ts +4 -2
  79. package/src/managers/slashCommandManager.ts +130 -4
  80. package/src/managers/toolManager.ts +11 -2
  81. package/src/prompts/index.ts +6 -0
  82. package/src/services/GitService.ts +20 -0
  83. package/src/services/MarketplaceService.ts +397 -324
  84. package/src/services/aiService.ts +197 -1
  85. package/src/services/initializationService.ts +38 -0
  86. package/src/tools/agentTool.ts +3 -3
  87. package/src/tools/bashTool.ts +3 -4
  88. package/src/tools/editTool.ts +3 -0
  89. package/src/tools/globTool.ts +16 -3
  90. package/src/tools/grepTool.ts +41 -13
  91. package/src/tools/readTool.ts +69 -0
  92. package/src/tools/skillTool.ts +2 -2
  93. package/src/tools/types.ts +13 -0
  94. package/src/tools/webFetchTool.ts +194 -0
  95. package/src/tools/writeTool.ts +3 -0
  96. package/src/types/commands.ts +1 -1
  97. package/src/types/messaging.ts +1 -0
  98. package/src/utils/bashParser.ts +268 -157
  99. package/src/utils/convertMessagesForAPI.ts +8 -0
  100. package/src/utils/fileUtils.ts +69 -0
  101. package/src/utils/messageOperations.ts +84 -9
  102. package/dist/tools/taskOutputTool.d.ts +0 -3
  103. package/dist/tools/taskOutputTool.d.ts.map +0 -1
  104. package/dist/tools/taskOutputTool.js +0 -198
  105. package/src/tools/taskOutputTool.ts +0 -222
@@ -456,14 +456,160 @@ export const DANGEROUS_COMMANDS = [
456
456
  "chown",
457
457
  "sh",
458
458
  "bash",
459
+ "zsh",
460
+ "fish",
461
+ "pwsh",
462
+ "cmd.exe",
463
+ "powershell.exe",
459
464
  "sudo",
460
465
  "dd",
461
466
  "apt",
462
467
  "apt-get",
463
468
  "yum",
464
469
  "dnf",
470
+ "ssh",
471
+ "scp",
472
+ "sftp",
473
+ "ftp",
474
+ "telnet",
475
+ "nc",
476
+ "netcat",
465
477
  ];
466
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
+
467
613
  /**
468
614
  * Extracts a "smart prefix" from a bash command based on common developer tools.
469
615
  * Returns null if the command is blacklisted or cannot be safely prefix-matched.
@@ -475,187 +621,152 @@ export function getSmartPrefix(command: string): string | null {
475
621
  // For now, we only support prefix matching for single commands or the first command in a chain
476
622
  // to keep it simple and safe.
477
623
  const firstCommand = parts[0];
478
- let stripped = stripRedirections(stripEnvVars(firstCommand));
479
624
 
480
- // Handle sudo
481
- if (stripped.startsWith("sudo ")) {
482
- stripped = stripped.substring(5).trim();
483
- }
625
+ // Safety check: don't allow heredoc writes
626
+ if (isBashHeredocWrite(firstCommand)) return null;
484
627
 
628
+ const stripped = stripRedirections(stripEnvVars(firstCommand));
485
629
  const tokens = stripped.split(/\s+/);
486
630
  if (tokens.length === 0) return null;
487
631
 
488
- const exe = tokens[0];
489
- 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;
490
643
 
644
+ const exe = tokens[i];
491
645
  // Blacklist - Hard blacklist for dangerous commands
492
646
  if (DANGEROUS_COMMANDS.includes(exe)) return null;
493
647
 
494
- // Node/JS
495
- if (["npm", "pnpm", "yarn", "deno", "bun"].includes(exe)) {
496
- let currentIdx = 1;
497
- const prefixParts = [exe];
498
-
499
- // Handle workspace/filter flags
500
- if (exe === "pnpm") {
501
- while (
502
- (tokens[currentIdx] === "-F" || tokens[currentIdx] === "--filter") &&
503
- tokens[currentIdx + 1]
504
- ) {
505
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
506
- 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;
507
659
  }
508
- } else if (
509
- exe === "npm" &&
510
- (tokens[currentIdx] === "--prefix" || tokens[currentIdx] === "-C") &&
511
- tokens[currentIdx + 1]
512
- ) {
513
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
514
- currentIdx += 2;
515
- } else if (
516
- exe === "yarn" &&
517
- tokens[currentIdx] === "workspace" &&
518
- tokens[currentIdx + 1]
519
- ) {
520
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
521
- currentIdx += 2;
522
- }
523
-
524
- const subCommand = tokens[currentIdx];
525
- const safeSubcommands = [
526
- "install",
527
- "i",
528
- "add",
529
- "remove",
530
- "rm",
531
- "uninstall",
532
- "un",
533
- "test",
534
- "t",
535
- "build",
536
- "start",
537
- "dev",
538
- ];
539
-
540
- if (safeSubcommands.includes(subCommand)) {
541
- prefixParts.push(subCommand);
542
- return prefixParts.join(" ");
543
660
  }
544
- if (
545
- (subCommand === "run" || (exe === "deno" && subCommand === "task")) &&
546
- tokens[currentIdx + 1]
547
- ) {
548
- prefixParts.push(subCommand, tokens[currentIdx + 1]);
549
- return prefixParts.join(" ");
661
+ if (match && key.length > bestRuleKey.length) {
662
+ bestRuleKey = key;
663
+ rule = r;
550
664
  }
551
- return null;
552
665
  }
553
666
 
554
- // Git
555
- if (exe === "git") {
556
- let currentIdx = 1;
557
- const prefixParts = [exe];
558
-
559
- // Handle -C <path>
560
- if (tokens[currentIdx] === "-C" && tokens[currentIdx + 1]) {
561
- prefixParts.push(tokens[currentIdx], tokens[currentIdx + 1]);
562
- currentIdx += 2;
563
- }
564
-
565
- const subCommand = tokens[currentIdx];
566
- const safeGitSubcommands = [
567
- "commit",
568
- "push",
569
- "pull",
570
- "checkout",
571
- "add",
572
- "status",
573
- "diff",
574
- "branch",
575
- "merge",
576
- "rebase",
577
- "log",
578
- "fetch",
579
- "remote",
580
- "stash",
581
- ];
582
-
583
- if (safeGitSubcommands.includes(subCommand)) {
584
- if (subCommand === "branch") {
585
- // Check for destructive flags
586
- const destructiveFlags = ["-d", "-D", "--delete"];
587
- if (tokens.some((t) => destructiveFlags.includes(t))) {
588
- return null;
589
- }
590
- }
591
- prefixParts.push(subCommand);
592
- 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;
593
708
  }
594
- return null;
595
709
  }
596
710
 
597
- // Python
598
- if (["python", "python3", "pip", "pip3", "poetry", "conda"].includes(exe)) {
599
- if (exe === "python" || exe === "python3") {
600
- if (tokens[1] === "-m" && tokens[2]) {
601
- if (tokens[2] === "pip" && tokens[3] === "install") {
602
- return `${exe} -m pip install`;
603
- }
604
- 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;
605
727
  }
606
- return null;
607
- }
608
- if (["install", "add", "remove", "test", "run"].includes(sub)) {
609
- return `${exe} ${sub}`;
728
+ currentDepth++;
610
729
  }
611
- return null;
612
- }
613
730
 
614
- // Java
615
- if (["mvn", "gradle"].includes(exe)) {
616
- if (sub && !sub.startsWith("-")) {
617
- return `${exe} ${sub}`;
618
- }
619
- return null;
620
- }
621
- if (exe === "java") {
622
- if (sub === "-jar") return "java -jar";
623
- return "java";
731
+ prefixParts.push(token);
732
+ i++;
624
733
  }
625
734
 
626
- // Rust & Go
627
- if (exe === "cargo") {
628
- if (["build", "test", "run", "add", "check"].includes(sub)) {
629
- return `${exe} ${sub}`;
630
- }
631
- return null;
632
- }
633
- if (exe === "go") {
634
- if (["build", "test", "run", "get", "mod"].includes(sub)) {
635
- return `${exe} ${sub}`;
636
- }
637
- return null;
638
- }
735
+ // Continue until we reach the required depth
736
+ while (i < tokens.length && currentDepth < depth) {
737
+ const token = tokens[i];
639
738
 
640
- // Containers & Infrastructure
641
- if (exe === "docker" || exe === "docker-compose") {
642
- if (["run", "build", "ps", "exec", "up", "down"].includes(sub)) {
643
- return `${exe} ${sub}`;
644
- }
645
- return null;
646
- }
647
- if (exe === "kubectl") {
648
- if (["get", "describe", "apply", "logs"].includes(sub)) {
649
- return `${exe} ${sub}`;
650
- }
651
- return null;
652
- }
653
- if (exe === "terraform") {
654
- if (["plan", "apply", "destroy", "init"].includes(sub)) {
655
- 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++;
656
765
  }
657
- return null;
766
+ i++;
658
767
  }
659
768
 
660
- return null;
769
+ if (currentDepth < depth) return null;
770
+
771
+ return prefixParts.join(" ");
661
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
+ }