ashlrcode 1.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/package.json +46 -0
- package/src/__tests__/branded-types.test.ts +47 -0
- package/src/__tests__/context.test.ts +163 -0
- package/src/__tests__/cost-tracker.test.ts +274 -0
- package/src/__tests__/cron.test.ts +197 -0
- package/src/__tests__/dream.test.ts +204 -0
- package/src/__tests__/error-handler.test.ts +192 -0
- package/src/__tests__/features.test.ts +69 -0
- package/src/__tests__/file-history.test.ts +177 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/keybindings.test.ts +159 -0
- package/src/__tests__/model-patches.test.ts +82 -0
- package/src/__tests__/permissions-rules.test.ts +121 -0
- package/src/__tests__/permissions.test.ts +108 -0
- package/src/__tests__/project-config.test.ts +63 -0
- package/src/__tests__/retry.test.ts +321 -0
- package/src/__tests__/router.test.ts +158 -0
- package/src/__tests__/session-compact.test.ts +191 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/__tests__/skill-registry.test.ts +130 -0
- package/src/__tests__/speculation.test.ts +196 -0
- package/src/__tests__/tasks-v2.test.ts +267 -0
- package/src/__tests__/telemetry.test.ts +149 -0
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-registry.test.ts +166 -0
- package/src/__tests__/undercover.test.ts +93 -0
- package/src/__tests__/workflow.test.ts +195 -0
- package/src/agent/async-context.ts +64 -0
- package/src/agent/context.ts +245 -0
- package/src/agent/cron.ts +189 -0
- package/src/agent/dream.ts +165 -0
- package/src/agent/error-handler.ts +108 -0
- package/src/agent/ipc.ts +256 -0
- package/src/agent/kairos.ts +207 -0
- package/src/agent/loop.ts +314 -0
- package/src/agent/model-patches.ts +68 -0
- package/src/agent/speculation.ts +219 -0
- package/src/agent/sub-agent.ts +125 -0
- package/src/agent/system-prompt.ts +231 -0
- package/src/agent/team.ts +220 -0
- package/src/agent/tool-executor.ts +162 -0
- package/src/agent/workflow.ts +189 -0
- package/src/agent/worktree-manager.ts +86 -0
- package/src/autopilot/queue.ts +186 -0
- package/src/autopilot/scanner.ts +245 -0
- package/src/autopilot/types.ts +58 -0
- package/src/bridge/bridge-client.ts +57 -0
- package/src/bridge/bridge-server.ts +81 -0
- package/src/cli.ts +1120 -0
- package/src/config/features.ts +51 -0
- package/src/config/git.ts +137 -0
- package/src/config/hooks.ts +201 -0
- package/src/config/permissions.ts +251 -0
- package/src/config/project-config.ts +63 -0
- package/src/config/remote-settings.ts +163 -0
- package/src/config/settings-sync.ts +170 -0
- package/src/config/settings.ts +113 -0
- package/src/config/undercover.ts +76 -0
- package/src/config/upgrade-notice.ts +65 -0
- package/src/mcp/client.ts +197 -0
- package/src/mcp/manager.ts +125 -0
- package/src/mcp/oauth.ts +252 -0
- package/src/mcp/types.ts +61 -0
- package/src/persistence/memory.ts +129 -0
- package/src/persistence/session.ts +289 -0
- package/src/planning/plan-mode.ts +128 -0
- package/src/planning/plan-tools.ts +138 -0
- package/src/providers/anthropic.ts +177 -0
- package/src/providers/cost-tracker.ts +184 -0
- package/src/providers/retry.ts +264 -0
- package/src/providers/router.ts +159 -0
- package/src/providers/types.ts +79 -0
- package/src/providers/xai.ts +217 -0
- package/src/repl.tsx +1384 -0
- package/src/setup.ts +119 -0
- package/src/skills/loader.ts +78 -0
- package/src/skills/registry.ts +78 -0
- package/src/skills/types.ts +11 -0
- package/src/state/file-history.ts +264 -0
- package/src/telemetry/event-log.ts +116 -0
- package/src/tools/agent.ts +133 -0
- package/src/tools/ask-user.ts +229 -0
- package/src/tools/bash.ts +146 -0
- package/src/tools/config.ts +147 -0
- package/src/tools/diff.ts +137 -0
- package/src/tools/file-edit.ts +123 -0
- package/src/tools/file-read.ts +82 -0
- package/src/tools/file-write.ts +82 -0
- package/src/tools/glob.ts +76 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/ls.ts +77 -0
- package/src/tools/lsp.ts +375 -0
- package/src/tools/mcp-resources.ts +83 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/memory.ts +148 -0
- package/src/tools/notebook-edit.ts +133 -0
- package/src/tools/peers.ts +113 -0
- package/src/tools/powershell.ts +83 -0
- package/src/tools/registry.ts +114 -0
- package/src/tools/send-message.ts +75 -0
- package/src/tools/sleep.ts +50 -0
- package/src/tools/snip.ts +143 -0
- package/src/tools/tasks.ts +349 -0
- package/src/tools/team.ts +309 -0
- package/src/tools/todo-write.ts +93 -0
- package/src/tools/tool-search.ts +83 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/web-browser.ts +263 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +107 -0
- package/src/tools/workflow.ts +188 -0
- package/src/tools/worktree.ts +143 -0
- package/src/types/branded.ts +22 -0
- package/src/ui/App.tsx +184 -0
- package/src/ui/BuddyPanel.tsx +52 -0
- package/src/ui/PermissionPrompt.tsx +29 -0
- package/src/ui/banner.ts +217 -0
- package/src/ui/buddy-ai.ts +108 -0
- package/src/ui/buddy.ts +466 -0
- package/src/ui/context-bar.ts +60 -0
- package/src/ui/effort.ts +65 -0
- package/src/ui/keybindings.ts +143 -0
- package/src/ui/markdown.ts +271 -0
- package/src/ui/message-renderer.ts +73 -0
- package/src/ui/mode.ts +80 -0
- package/src/ui/notifications.ts +57 -0
- package/src/ui/speech-bubble.ts +95 -0
- package/src/ui/spinner.ts +116 -0
- package/src/ui/theme.ts +98 -0
- package/src/version.ts +5 -0
- package/src/voice/voice-mode.ts +169 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DiffTool — show differences between file versions or git changes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export const diffTool: Tool = {
|
|
11
|
+
name: "Diff",
|
|
12
|
+
|
|
13
|
+
prompt() {
|
|
14
|
+
return `Show differences between files or git changes. Modes:
|
|
15
|
+
- git: show git diff for a file or the whole repo
|
|
16
|
+
- files: compare two files
|
|
17
|
+
- string: compare two strings (useful for showing before/after)`;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
inputSchema() {
|
|
21
|
+
return {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
mode: {
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["git", "files", "string"],
|
|
27
|
+
description: "Diff mode (default: git)",
|
|
28
|
+
},
|
|
29
|
+
file_path: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "File path for git diff, or first file for files mode",
|
|
32
|
+
},
|
|
33
|
+
file_path_2: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Second file path (for files mode)",
|
|
36
|
+
},
|
|
37
|
+
old_string: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Old string (for string mode)",
|
|
40
|
+
},
|
|
41
|
+
new_string: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "New string (for string mode)",
|
|
44
|
+
},
|
|
45
|
+
staged: {
|
|
46
|
+
type: "boolean",
|
|
47
|
+
description: "Show staged changes (git mode, default: false)",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: [],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
isReadOnly() { return true; },
|
|
55
|
+
isDestructive() { return false; },
|
|
56
|
+
isConcurrencySafe() { return true; },
|
|
57
|
+
|
|
58
|
+
validateInput() { return null; },
|
|
59
|
+
|
|
60
|
+
async call(input, context) {
|
|
61
|
+
const mode = (input.mode as string) ?? "git";
|
|
62
|
+
|
|
63
|
+
switch (mode) {
|
|
64
|
+
case "git":
|
|
65
|
+
return await gitDiff(input, context);
|
|
66
|
+
case "files":
|
|
67
|
+
return await filesDiff(input, context);
|
|
68
|
+
case "string":
|
|
69
|
+
return stringDiff(input);
|
|
70
|
+
default:
|
|
71
|
+
return `Unknown mode: ${mode}`;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
async function gitDiff(input: Record<string, unknown>, context: ToolContext): Promise<string> {
|
|
77
|
+
const args = ["git", "diff"];
|
|
78
|
+
if (input.staged) args.push("--staged");
|
|
79
|
+
if (input.file_path) args.push("--", input.file_path as string);
|
|
80
|
+
|
|
81
|
+
const proc = Bun.spawn(args, {
|
|
82
|
+
cwd: context.cwd,
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "pipe",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const stdout = await new Response(proc.stdout).text();
|
|
88
|
+
const exitCode = await proc.exited;
|
|
89
|
+
|
|
90
|
+
if (exitCode !== 0 || !stdout.trim()) {
|
|
91
|
+
return "No changes found.";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return stdout.trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function filesDiff(input: Record<string, unknown>, context: ToolContext): Promise<string> {
|
|
98
|
+
const path1 = resolve(context.cwd, input.file_path as string);
|
|
99
|
+
const path2 = resolve(context.cwd, input.file_path_2 as string);
|
|
100
|
+
|
|
101
|
+
if (!existsSync(path1)) return `File not found: ${path1}`;
|
|
102
|
+
if (!existsSync(path2)) return `File not found: ${path2}`;
|
|
103
|
+
|
|
104
|
+
const proc = Bun.spawn(["diff", "-u", path1, path2], {
|
|
105
|
+
cwd: context.cwd,
|
|
106
|
+
stdout: "pipe",
|
|
107
|
+
stderr: "pipe",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const stdout = await new Response(proc.stdout).text();
|
|
111
|
+
return stdout.trim() || "Files are identical.";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stringDiff(input: Record<string, unknown>): string {
|
|
115
|
+
const oldStr = (input.old_string as string) ?? "";
|
|
116
|
+
const newStr = (input.new_string as string) ?? "";
|
|
117
|
+
|
|
118
|
+
const oldLines = oldStr.split("\n");
|
|
119
|
+
const newLines = newStr.split("\n");
|
|
120
|
+
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < maxLen; i++) {
|
|
125
|
+
const oldLine = oldLines[i];
|
|
126
|
+
const newLine = newLines[i];
|
|
127
|
+
|
|
128
|
+
if (oldLine === newLine) {
|
|
129
|
+
if (oldLine !== undefined) lines.push(` ${oldLine}`);
|
|
130
|
+
} else {
|
|
131
|
+
if (oldLine !== undefined) lines.push(`- ${oldLine}`);
|
|
132
|
+
if (newLine !== undefined) lines.push(`+ ${newLine}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return lines.join("\n") || "No differences.";
|
|
137
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileEditTool — exact string replacement in files.
|
|
3
|
+
* Follows Claude Code's Edit pattern.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, writeFile } from "fs/promises";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
10
|
+
import { getFileHistory } from "../state/file-history.ts";
|
|
11
|
+
|
|
12
|
+
export const fileEditTool: Tool = {
|
|
13
|
+
name: "Edit",
|
|
14
|
+
|
|
15
|
+
prompt() {
|
|
16
|
+
return "Perform exact string replacement in a file. The old_string must be unique in the file. Use replace_all: true to replace all occurrences.";
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
inputSchema() {
|
|
20
|
+
return {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
file_path: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Absolute path to the file to edit",
|
|
26
|
+
},
|
|
27
|
+
old_string: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "The exact text to find and replace",
|
|
30
|
+
},
|
|
31
|
+
new_string: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "The replacement text",
|
|
34
|
+
},
|
|
35
|
+
replace_all: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: "Replace all occurrences (default: false)",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["file_path", "old_string", "new_string"],
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
isReadOnly() {
|
|
45
|
+
return false;
|
|
46
|
+
},
|
|
47
|
+
isDestructive() {
|
|
48
|
+
return false; // Reversible via edit
|
|
49
|
+
},
|
|
50
|
+
isConcurrencySafe() {
|
|
51
|
+
return false;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
validateInput(input) {
|
|
55
|
+
if (!input.file_path || typeof input.file_path !== "string") {
|
|
56
|
+
return "file_path is required";
|
|
57
|
+
}
|
|
58
|
+
if (typeof input.old_string !== "string") {
|
|
59
|
+
return "old_string is required";
|
|
60
|
+
}
|
|
61
|
+
if (typeof input.new_string !== "string") {
|
|
62
|
+
return "new_string is required";
|
|
63
|
+
}
|
|
64
|
+
if (input.old_string === input.new_string) {
|
|
65
|
+
return "old_string and new_string must be different";
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async call(input, context) {
|
|
71
|
+
const filePath = resolve(context.cwd, input.file_path as string);
|
|
72
|
+
const oldString = input.old_string as string;
|
|
73
|
+
const newString = input.new_string as string;
|
|
74
|
+
const replaceAll = (input.replace_all as boolean) ?? false;
|
|
75
|
+
|
|
76
|
+
if (!existsSync(filePath)) {
|
|
77
|
+
return `File not found: ${filePath}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Snapshot before editing
|
|
81
|
+
const history = getFileHistory();
|
|
82
|
+
if (history) {
|
|
83
|
+
await history.capture(filePath, "Edit", context.turnNumber ?? 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const content = await readFile(filePath, "utf-8");
|
|
87
|
+
|
|
88
|
+
if (!replaceAll) {
|
|
89
|
+
const occurrences = content.split(oldString).length - 1;
|
|
90
|
+
if (occurrences === 0) {
|
|
91
|
+
return `old_string not found in ${filePath}`;
|
|
92
|
+
}
|
|
93
|
+
if (occurrences > 1) {
|
|
94
|
+
return `old_string found ${occurrences} times — must be unique. Provide more context or use replace_all: true.`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const updated = replaceAll
|
|
99
|
+
? content.replaceAll(oldString, newString)
|
|
100
|
+
: content.replace(oldString, newString);
|
|
101
|
+
|
|
102
|
+
await writeFile(filePath, updated, "utf-8");
|
|
103
|
+
|
|
104
|
+
const replacements = replaceAll
|
|
105
|
+
? content.split(oldString).length - 1
|
|
106
|
+
: 1;
|
|
107
|
+
|
|
108
|
+
// Show a mini diff
|
|
109
|
+
const oldLines = oldString.split("\n");
|
|
110
|
+
const newLines = newString.split("\n");
|
|
111
|
+
const diffLines: string[] = [];
|
|
112
|
+
for (const line of oldLines.slice(0, 3)) {
|
|
113
|
+
diffLines.push(`- ${line}`);
|
|
114
|
+
}
|
|
115
|
+
if (oldLines.length > 3) diffLines.push(` ... (${oldLines.length} lines)`);
|
|
116
|
+
for (const line of newLines.slice(0, 3)) {
|
|
117
|
+
diffLines.push(`+ ${line}`);
|
|
118
|
+
}
|
|
119
|
+
if (newLines.length > 3) diffLines.push(` ... (${newLines.length} lines)`);
|
|
120
|
+
|
|
121
|
+
return `Replaced ${replacements} occurrence(s) in ${filePath}\n${diffLines.join("\n")}`;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileReadTool — read file contents with line numbers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export const fileReadTool: Tool = {
|
|
11
|
+
name: "Read",
|
|
12
|
+
|
|
13
|
+
prompt() {
|
|
14
|
+
return "Read a file from the filesystem. Returns contents with line numbers. Supports offset and limit for reading specific portions of large files.";
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
inputSchema() {
|
|
18
|
+
return {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
file_path: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Absolute path to the file to read",
|
|
24
|
+
},
|
|
25
|
+
offset: {
|
|
26
|
+
type: "number",
|
|
27
|
+
description: "Line number to start reading from (0-based)",
|
|
28
|
+
},
|
|
29
|
+
limit: {
|
|
30
|
+
type: "number",
|
|
31
|
+
description: "Maximum number of lines to read",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["file_path"],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
isReadOnly() {
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
isDestructive() {
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
isConcurrencySafe() {
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
validateInput(input) {
|
|
49
|
+
if (!input.file_path || typeof input.file_path !== "string") {
|
|
50
|
+
return "file_path is required and must be a string";
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async call(input, context) {
|
|
56
|
+
const filePath = resolve(context.cwd, input.file_path as string);
|
|
57
|
+
|
|
58
|
+
if (!existsSync(filePath)) {
|
|
59
|
+
return `File not found: ${filePath}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const content = await readFile(filePath, "utf-8");
|
|
63
|
+
const lines = content.split("\n");
|
|
64
|
+
|
|
65
|
+
const offset = (input.offset as number) ?? 0;
|
|
66
|
+
const limit = (input.limit as number) ?? 2000;
|
|
67
|
+
const slice = lines.slice(offset, offset + limit);
|
|
68
|
+
|
|
69
|
+
const numbered = slice
|
|
70
|
+
.map((line, i) => `${offset + i + 1}\t${line}`)
|
|
71
|
+
.join("\n");
|
|
72
|
+
|
|
73
|
+
const total = lines.length;
|
|
74
|
+
const showing = slice.length;
|
|
75
|
+
const header =
|
|
76
|
+
showing < total
|
|
77
|
+
? `(Showing lines ${offset + 1}-${offset + showing} of ${total})\n`
|
|
78
|
+
: "";
|
|
79
|
+
|
|
80
|
+
return header + numbered;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileWriteTool — create or overwrite files.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { dirname, resolve } from "path";
|
|
8
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
9
|
+
import { getFileHistory } from "../state/file-history.ts";
|
|
10
|
+
|
|
11
|
+
export const fileWriteTool: Tool = {
|
|
12
|
+
name: "Write",
|
|
13
|
+
|
|
14
|
+
prompt() {
|
|
15
|
+
return "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Creates parent directories as needed.";
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
inputSchema() {
|
|
19
|
+
return {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
file_path: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Absolute path to the file to write",
|
|
25
|
+
},
|
|
26
|
+
content: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "The content to write to the file",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ["file_path", "content"],
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
isReadOnly() {
|
|
36
|
+
return false;
|
|
37
|
+
},
|
|
38
|
+
isDestructive() {
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
isConcurrencySafe() {
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
validateInput(input) {
|
|
46
|
+
if (!input.file_path || typeof input.file_path !== "string") {
|
|
47
|
+
return "file_path is required and must be a string";
|
|
48
|
+
}
|
|
49
|
+
if (typeof input.content !== "string") {
|
|
50
|
+
return "content is required and must be a string";
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
checkPermissions(input: Record<string, unknown>, context: ToolContext): string | null {
|
|
56
|
+
const filePath = input.file_path as string;
|
|
57
|
+
if (!filePath) return null;
|
|
58
|
+
const resolved = resolve(context.cwd, filePath);
|
|
59
|
+
const sensitive = ["/etc/", "/usr/bin/", "/sbin/", "/.ssh/"];
|
|
60
|
+
for (const s of sensitive) {
|
|
61
|
+
if (resolved.includes(s)) return `Cannot write to sensitive path: ${s}`;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async call(input, context) {
|
|
67
|
+
const filePath = resolve(context.cwd, input.file_path as string);
|
|
68
|
+
const content = input.content as string;
|
|
69
|
+
|
|
70
|
+
// Snapshot before overwriting (captures new files too for undo-as-delete)
|
|
71
|
+
const history = getFileHistory();
|
|
72
|
+
if (history) {
|
|
73
|
+
await history.capture(filePath, "Write", context.turnNumber ?? 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
77
|
+
await writeFile(filePath, content, "utf-8");
|
|
78
|
+
|
|
79
|
+
const lines = content.split("\n").length;
|
|
80
|
+
return `Wrote ${lines} lines to ${filePath}`;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlobTool — fast file pattern matching.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fg from "fast-glob";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export const globTool: Tool = {
|
|
10
|
+
name: "Glob",
|
|
11
|
+
|
|
12
|
+
prompt() {
|
|
13
|
+
return 'Find files matching a glob pattern. Returns matching file paths sorted by modification time. Example patterns: "**/*.ts", "src/**/*.tsx", "*.json"';
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
inputSchema() {
|
|
17
|
+
return {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
pattern: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Glob pattern to match files against",
|
|
23
|
+
},
|
|
24
|
+
path: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Directory to search in (defaults to cwd)",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["pattern"],
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
isReadOnly() {
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
isDestructive() {
|
|
37
|
+
return false;
|
|
38
|
+
},
|
|
39
|
+
isConcurrencySafe() {
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
validateInput(input) {
|
|
44
|
+
if (!input.pattern || typeof input.pattern !== "string") {
|
|
45
|
+
return "pattern is required";
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async call(input, context) {
|
|
51
|
+
const pattern = input.pattern as string;
|
|
52
|
+
const searchPath = resolve(context.cwd, (input.path as string) ?? ".");
|
|
53
|
+
|
|
54
|
+
const files = await fg(pattern, {
|
|
55
|
+
cwd: searchPath,
|
|
56
|
+
absolute: true,
|
|
57
|
+
dot: false,
|
|
58
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
59
|
+
stats: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Sort by modification time (most recent first)
|
|
63
|
+
files.sort((a, b) => {
|
|
64
|
+
const aTime = a.stats?.mtimeMs ?? 0;
|
|
65
|
+
const bTime = b.stats?.mtimeMs ?? 0;
|
|
66
|
+
return bTime - aTime;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (files.length === 0) {
|
|
70
|
+
return `No files matching "${pattern}" in ${searchPath}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const paths = files.map((f) => f.path).join("\n");
|
|
74
|
+
return `${files.length} file(s) found:\n${paths}`;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GrepTool — content search. Tries ripgrep first, falls back to grep.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import type { Tool, ToolContext } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export const grepTool: Tool = {
|
|
9
|
+
name: "Grep",
|
|
10
|
+
|
|
11
|
+
prompt() {
|
|
12
|
+
return "Search file contents using regex patterns. Returns matching lines with file paths and line numbers.";
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
inputSchema() {
|
|
16
|
+
return {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
pattern: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Regex pattern to search for",
|
|
22
|
+
},
|
|
23
|
+
path: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "File or directory to search in (defaults to cwd)",
|
|
26
|
+
},
|
|
27
|
+
glob: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: 'Glob pattern to filter files (e.g. "*.ts", "*.{js,jsx}")',
|
|
30
|
+
},
|
|
31
|
+
output_mode: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["content", "files_with_matches", "count"],
|
|
34
|
+
description: "Output mode (default: files_with_matches)",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["pattern"],
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
isReadOnly() {
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
isDestructive() {
|
|
45
|
+
return false;
|
|
46
|
+
},
|
|
47
|
+
isConcurrencySafe() {
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
validateInput(input) {
|
|
52
|
+
if (!input.pattern || typeof input.pattern !== "string") {
|
|
53
|
+
return "pattern is required";
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async call(input, context) {
|
|
59
|
+
const pattern = input.pattern as string;
|
|
60
|
+
const searchPath = resolve(context.cwd, (input.path as string) ?? ".");
|
|
61
|
+
const globFilter = input.glob as string | undefined;
|
|
62
|
+
const outputMode = (input.output_mode as string) ?? "files_with_matches";
|
|
63
|
+
|
|
64
|
+
// Try ripgrep first (use bash -c to resolve shell functions/aliases)
|
|
65
|
+
try {
|
|
66
|
+
const result = await runRipgrep(pattern, searchPath, globFilter, outputMode, context);
|
|
67
|
+
if (result !== null) return result;
|
|
68
|
+
} catch {
|
|
69
|
+
// ripgrep not available, fall through to grep
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback to system grep
|
|
73
|
+
return await runGrep(pattern, searchPath, globFilter, outputMode, context);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
async function runRipgrep(
|
|
78
|
+
pattern: string,
|
|
79
|
+
searchPath: string,
|
|
80
|
+
globFilter: string | undefined,
|
|
81
|
+
outputMode: string,
|
|
82
|
+
context: ToolContext
|
|
83
|
+
): Promise<string | null> {
|
|
84
|
+
const rgArgs: string[] = [];
|
|
85
|
+
|
|
86
|
+
switch (outputMode) {
|
|
87
|
+
case "files_with_matches":
|
|
88
|
+
rgArgs.push("-l");
|
|
89
|
+
break;
|
|
90
|
+
case "count":
|
|
91
|
+
rgArgs.push("-c");
|
|
92
|
+
break;
|
|
93
|
+
case "content":
|
|
94
|
+
rgArgs.push("-n");
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (globFilter) {
|
|
99
|
+
rgArgs.push("--glob", globFilter);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
rgArgs.push("--max-count", "250", "--no-heading");
|
|
103
|
+
rgArgs.push(pattern, searchPath);
|
|
104
|
+
|
|
105
|
+
// Use bash -c to resolve shell functions/aliases for rg
|
|
106
|
+
const cmd = `rg ${rgArgs.map(shellEscape).join(" ")}`;
|
|
107
|
+
const proc = Bun.spawn(["bash", "-c", cmd], {
|
|
108
|
+
cwd: context.cwd,
|
|
109
|
+
stdout: "pipe",
|
|
110
|
+
stderr: "pipe",
|
|
111
|
+
env: { ...process.env },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const stdout = await new Response(proc.stdout).text();
|
|
115
|
+
const stderr = await new Response(proc.stderr).text();
|
|
116
|
+
const exitCode = await proc.exited;
|
|
117
|
+
|
|
118
|
+
if (exitCode === 1 && !stderr) {
|
|
119
|
+
return `No matches found for "${pattern}"`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
123
|
+
if (stderr.includes("not found") || stderr.includes("command not found")) {
|
|
124
|
+
return null; // rg not available
|
|
125
|
+
}
|
|
126
|
+
return `Search error: ${stderr}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return stdout.trim() || `No matches found for "${pattern}"`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runGrep(
|
|
133
|
+
pattern: string,
|
|
134
|
+
searchPath: string,
|
|
135
|
+
globFilter: string | undefined,
|
|
136
|
+
outputMode: string,
|
|
137
|
+
context: ToolContext
|
|
138
|
+
): Promise<string> {
|
|
139
|
+
const args = ["grep", "-r"];
|
|
140
|
+
|
|
141
|
+
switch (outputMode) {
|
|
142
|
+
case "files_with_matches":
|
|
143
|
+
args.push("-l");
|
|
144
|
+
break;
|
|
145
|
+
case "count":
|
|
146
|
+
args.push("-c");
|
|
147
|
+
break;
|
|
148
|
+
case "content":
|
|
149
|
+
args.push("-n");
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (globFilter) {
|
|
154
|
+
args.push(`--include=${globFilter}`);
|
|
155
|
+
} else {
|
|
156
|
+
args.push("--include=*.ts", "--include=*.js", "--include=*.tsx",
|
|
157
|
+
"--include=*.jsx", "--include=*.py", "--include=*.go",
|
|
158
|
+
"--include=*.rs", "--include=*.md", "--include=*.json");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
args.push(pattern, searchPath);
|
|
162
|
+
|
|
163
|
+
const proc = Bun.spawn(args, {
|
|
164
|
+
cwd: context.cwd,
|
|
165
|
+
stdout: "pipe",
|
|
166
|
+
stderr: "pipe",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const stdout = await new Response(proc.stdout).text();
|
|
170
|
+
const exitCode = await proc.exited;
|
|
171
|
+
|
|
172
|
+
if (exitCode === 1) {
|
|
173
|
+
return `No matches found for "${pattern}"`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Limit output
|
|
177
|
+
const lines = stdout.trim().split("\n");
|
|
178
|
+
if (lines.length > 250) {
|
|
179
|
+
return lines.slice(0, 250).join("\n") + `\n\n[... ${lines.length - 250} more matches]`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return stdout.trim() || `No matches found for "${pattern}"`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function shellEscape(s: string): string {
|
|
186
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
187
|
+
}
|