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.
- package/builtin/skills/init/SKILL.md +2 -0
- package/builtin/skills/settings/SKILLS.md +3 -2
- package/builtin/skills/settings/SUBAGENTS.md +1 -3
- package/dist/agent.d.ts +6 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +18 -1
- package/dist/constants/tools.d.ts +1 -1
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +1 -1
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +1 -9
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +22 -3
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +32 -13
- package/dist/managers/messageManager.d.ts +13 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +62 -34
- package/dist/managers/permissionManager.js +4 -4
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +4 -2
- package/dist/managers/slashCommandManager.d.ts +2 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +98 -4
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +8 -2
- package/dist/prompts/index.d.ts +2 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +5 -0
- package/dist/services/GitService.d.ts +1 -0
- package/dist/services/GitService.d.ts.map +1 -1
- package/dist/services/GitService.js +16 -0
- package/dist/services/MarketplaceService.d.ts +7 -0
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +321 -252
- package/dist/services/aiService.d.ts +34 -0
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +124 -1
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +18 -0
- package/dist/tools/agentTool.js +3 -3
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +4 -4
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +2 -0
- package/dist/tools/globTool.d.ts.map +1 -1
- package/dist/tools/globTool.js +15 -3
- package/dist/tools/grepTool.d.ts.map +1 -1
- package/dist/tools/grepTool.js +38 -12
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +61 -0
- package/dist/tools/skillTool.js +2 -2
- package/dist/tools/types.d.ts +16 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts +3 -0
- package/dist/tools/webFetchTool.d.ts.map +1 -0
- package/dist/tools/webFetchTool.js +171 -0
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +2 -0
- package/dist/types/commands.d.ts +1 -1
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/utils/bashParser.d.ts +20 -2
- package/dist/utils/bashParser.d.ts.map +1 -1
- package/dist/utils/bashParser.js +281 -146
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +7 -0
- package/dist/utils/fileUtils.d.ts +8 -0
- package/dist/utils/fileUtils.d.ts.map +1 -1
- package/dist/utils/fileUtils.js +52 -0
- package/dist/utils/messageOperations.d.ts +12 -3
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +77 -9
- package/package.json +4 -2
- package/src/agent.ts +19 -1
- package/src/constants/tools.ts +1 -1
- package/src/managers/MemoryRuleManager.ts +1 -10
- package/src/managers/aiManager.ts +23 -3
- package/src/managers/mcpManager.ts +37 -16
- package/src/managers/messageManager.ts +76 -38
- package/src/managers/permissionManager.ts +4 -4
- package/src/managers/pluginManager.ts +4 -2
- package/src/managers/slashCommandManager.ts +130 -4
- package/src/managers/toolManager.ts +11 -2
- package/src/prompts/index.ts +6 -0
- package/src/services/GitService.ts +20 -0
- package/src/services/MarketplaceService.ts +397 -324
- package/src/services/aiService.ts +197 -1
- package/src/services/initializationService.ts +38 -0
- package/src/tools/agentTool.ts +3 -3
- package/src/tools/bashTool.ts +3 -4
- package/src/tools/editTool.ts +3 -0
- package/src/tools/globTool.ts +16 -3
- package/src/tools/grepTool.ts +41 -13
- package/src/tools/readTool.ts +69 -0
- package/src/tools/skillTool.ts +2 -2
- package/src/tools/types.ts +13 -0
- package/src/tools/webFetchTool.ts +194 -0
- package/src/tools/writeTool.ts +3 -0
- package/src/types/commands.ts +1 -1
- package/src/types/messaging.ts +1 -0
- package/src/utils/bashParser.ts +316 -161
- package/src/utils/convertMessagesForAPI.ts +8 -0
- package/src/utils/fileUtils.ts +69 -0
- package/src/utils/messageOperations.ts +84 -9
- package/dist/tools/taskOutputTool.d.ts +0 -3
- package/dist/tools/taskOutputTool.d.ts.map +0 -1
- package/dist/tools/taskOutputTool.js +0 -198
- package/src/tools/taskOutputTool.ts +0 -222
package/src/utils/bashParser.ts
CHANGED
|
@@ -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 !== ">"
|
|
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
|
-
*
|
|
399
|
+
* Checks if a bash command contains any heredocs (<<, <<-).
|
|
399
400
|
*/
|
|
400
|
-
export function
|
|
401
|
-
|
|
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
|
-
//
|
|
437
|
-
if (
|
|
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
|
|
445
|
-
|
|
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
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
//
|
|
511
|
-
if (
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
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
|
package/src/utils/fileUtils.ts
CHANGED
|
@@ -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
|
+
}
|