@heysalad/cheri-cli 0.6.0 → 0.8.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/package.json +1 -1
- package/src/commands/agent.js +103 -146
- package/src/commands/login.js +3 -5
- package/src/lib/tools/command-tools.js +34 -0
- package/src/lib/tools/file-tools.js +72 -0
- package/src/lib/tools/index.js +32 -0
- package/src/lib/tools/search-tools.js +95 -0
- package/src/repl.js +2 -1
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -1,151 +1,68 @@
|
|
|
1
1
|
import { apiClient } from "../lib/api-client.js";
|
|
2
2
|
import { log } from "../lib/logger.js";
|
|
3
|
+
import { getToolDefinitions, executeTool as executeLocalTool, requiresConfirmation } from "../lib/tools/index.js";
|
|
3
4
|
import chalk from "chalk";
|
|
5
|
+
import readline from "readline";
|
|
6
|
+
|
|
7
|
+
const SYSTEM_PROMPT = `You are Cheri, an AI coding assistant by HeySalad. You are a powerful agentic coding tool that can read, write, edit, and search code, execute shell commands, and manage cloud workspaces.
|
|
8
|
+
|
|
9
|
+
You have two categories of tools:
|
|
10
|
+
1. LOCAL CODING TOOLS — read_file, write_file, edit_file, run_command, search_files, search_content, list_directory. Use these to work directly with the user's local codebase.
|
|
11
|
+
2. CLOUD PLATFORM TOOLS — get_account_info, list_workspaces, create_workspace, stop_workspace, get_workspace_status, get_memory, add_memory, clear_memory, get_usage, get_config, set_config. Use these to manage the Cheri cloud platform.
|
|
12
|
+
|
|
13
|
+
Guidelines:
|
|
14
|
+
- Read files before editing them. Understand existing code before making changes.
|
|
15
|
+
- Use search_files and search_content to explore unfamiliar codebases.
|
|
16
|
+
- Use list_directory to understand project structure.
|
|
17
|
+
- When writing code, match existing style and patterns.
|
|
18
|
+
- For shell commands, prefer specific commands over broad ones.
|
|
19
|
+
- Be concise. Show what you did and the result.
|
|
20
|
+
- Never guess file contents — always read first.
|
|
21
|
+
- Current working directory: ${process.cwd()}`;
|
|
22
|
+
|
|
23
|
+
// Cloud platform tools (executed via API)
|
|
24
|
+
const CLOUD_TOOLS = [
|
|
25
|
+
{ name: "get_account_info", description: "Get the current user's account information", parameters: { type: "object", properties: {}, required: [] } },
|
|
26
|
+
{ name: "list_workspaces", description: "List all cloud workspaces for the current user", parameters: { type: "object", properties: {}, required: [] } },
|
|
27
|
+
{ name: "create_workspace", description: "Launch a new cloud workspace for a GitHub repository", parameters: { type: "object", properties: { repo: { type: "string", description: "GitHub repo in owner/name format" } }, required: ["repo"] } },
|
|
28
|
+
{ name: "stop_workspace", description: "Stop and delete a running workspace", parameters: { type: "object", properties: { id: { type: "string", description: "Workspace ID to stop" } }, required: ["id"] } },
|
|
29
|
+
{ name: "get_workspace_status", description: "Get the status of a specific workspace", parameters: { type: "object", properties: { id: { type: "string", description: "Workspace ID" } }, required: ["id"] } },
|
|
30
|
+
{ name: "get_memory", description: "Retrieve all stored memory entries", parameters: { type: "object", properties: {}, required: [] } },
|
|
31
|
+
{ name: "add_memory", description: "Add a new memory entry for the user", parameters: { type: "object", properties: { content: { type: "string", description: "Memory content to store" }, category: { type: "string", description: "Optional category (defaults to 'general')" } }, required: ["content"] } },
|
|
32
|
+
{ name: "clear_memory", description: "Clear all stored memory entries", parameters: { type: "object", properties: {}, required: [] } },
|
|
33
|
+
{ name: "get_usage", description: "Get the user's API usage and rate limit statistics", parameters: { type: "object", properties: {}, required: [] } },
|
|
34
|
+
{ name: "get_config", description: "Get a configuration value by key (dot notation supported)", parameters: { type: "object", properties: { key: { type: "string", description: "Config key, e.g. 'ai.provider'" } }, required: ["key"] } },
|
|
35
|
+
{ name: "set_config", description: "Set a configuration value", parameters: { type: "object", properties: { key: { type: "string", description: "Config key" }, value: { type: "string", description: "Value to set" } }, required: ["key", "value"] } },
|
|
36
|
+
];
|
|
4
37
|
|
|
5
|
-
const
|
|
38
|
+
const CLOUD_TOOL_NAMES = new Set(CLOUD_TOOLS.map((t) => t.name));
|
|
6
39
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function: {
|
|
11
|
-
name: "get_account_info",
|
|
12
|
-
description: "Get the current user's account information",
|
|
13
|
-
parameters: { type: "object", properties: {}, required: [] },
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
type: "function",
|
|
18
|
-
function: {
|
|
19
|
-
name: "list_workspaces",
|
|
20
|
-
description: "List all cloud workspaces for the current user",
|
|
21
|
-
parameters: { type: "object", properties: {}, required: [] },
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
type: "function",
|
|
26
|
-
function: {
|
|
27
|
-
name: "create_workspace",
|
|
28
|
-
description: "Launch a new cloud workspace for a GitHub repository",
|
|
29
|
-
parameters: {
|
|
30
|
-
type: "object",
|
|
31
|
-
properties: { repo: { type: "string", description: "GitHub repo in owner/name format" } },
|
|
32
|
-
required: ["repo"],
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
type: "function",
|
|
38
|
-
function: {
|
|
39
|
-
name: "stop_workspace",
|
|
40
|
-
description: "Stop and delete a running workspace",
|
|
41
|
-
parameters: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: { id: { type: "string", description: "Workspace ID to stop" } },
|
|
44
|
-
required: ["id"],
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
type: "function",
|
|
50
|
-
function: {
|
|
51
|
-
name: "get_workspace_status",
|
|
52
|
-
description: "Get the status of a specific workspace",
|
|
53
|
-
parameters: {
|
|
54
|
-
type: "object",
|
|
55
|
-
properties: { id: { type: "string", description: "Workspace ID" } },
|
|
56
|
-
required: ["id"],
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
type: "function",
|
|
62
|
-
function: {
|
|
63
|
-
name: "get_memory",
|
|
64
|
-
description: "Retrieve all stored memory entries",
|
|
65
|
-
parameters: { type: "object", properties: {}, required: [] },
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
40
|
+
// Build unified tool list in OpenAI function-calling format
|
|
41
|
+
function buildTools() {
|
|
42
|
+
const localDefs = getToolDefinitions().map((t) => ({
|
|
69
43
|
type: "function",
|
|
70
|
-
function: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
parameters: {
|
|
74
|
-
type: "object",
|
|
75
|
-
properties: {
|
|
76
|
-
content: { type: "string", description: "Memory content to store" },
|
|
77
|
-
category: { type: "string", description: "Optional category (defaults to 'general')" },
|
|
78
|
-
},
|
|
79
|
-
required: ["content"],
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
{
|
|
44
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
45
|
+
}));
|
|
46
|
+
const cloudDefs = CLOUD_TOOLS.map((t) => ({
|
|
84
47
|
type: "function",
|
|
85
|
-
function: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
type: "function",
|
|
93
|
-
function: {
|
|
94
|
-
name: "get_usage",
|
|
95
|
-
description: "Get the user's API usage and rate limit statistics",
|
|
96
|
-
parameters: { type: "object", properties: {}, required: [] },
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
type: "function",
|
|
101
|
-
function: {
|
|
102
|
-
name: "get_config",
|
|
103
|
-
description: "Get a configuration value by key (dot notation supported)",
|
|
104
|
-
parameters: {
|
|
105
|
-
type: "object",
|
|
106
|
-
properties: { key: { type: "string", description: "Config key, e.g. 'ai.provider'" } },
|
|
107
|
-
required: ["key"],
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
type: "function",
|
|
113
|
-
function: {
|
|
114
|
-
name: "set_config",
|
|
115
|
-
description: "Set a configuration value",
|
|
116
|
-
parameters: {
|
|
117
|
-
type: "object",
|
|
118
|
-
properties: {
|
|
119
|
-
key: { type: "string", description: "Config key" },
|
|
120
|
-
value: { type: "string", description: "Value to set" },
|
|
121
|
-
},
|
|
122
|
-
required: ["key", "value"],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
];
|
|
48
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
49
|
+
}));
|
|
50
|
+
return [...localDefs, ...cloudDefs];
|
|
51
|
+
}
|
|
127
52
|
|
|
128
|
-
|
|
53
|
+
// Execute a cloud platform tool via the API
|
|
54
|
+
async function executeCloudTool(name, args) {
|
|
129
55
|
try {
|
|
130
56
|
switch (name) {
|
|
131
|
-
case "get_account_info":
|
|
132
|
-
|
|
133
|
-
case "
|
|
134
|
-
|
|
135
|
-
case "
|
|
136
|
-
|
|
137
|
-
case "
|
|
138
|
-
|
|
139
|
-
case "
|
|
140
|
-
return await apiClient.getWorkspaceStatus(args.id);
|
|
141
|
-
case "get_memory":
|
|
142
|
-
return await apiClient.getMemory();
|
|
143
|
-
case "add_memory":
|
|
144
|
-
return await apiClient.addMemory(args.content, args.category);
|
|
145
|
-
case "clear_memory":
|
|
146
|
-
return await apiClient.clearMemory();
|
|
147
|
-
case "get_usage":
|
|
148
|
-
return await apiClient.getUsage();
|
|
57
|
+
case "get_account_info": return await apiClient.getMe();
|
|
58
|
+
case "list_workspaces": return await apiClient.listWorkspaces();
|
|
59
|
+
case "create_workspace": return await apiClient.createWorkspace(args.repo);
|
|
60
|
+
case "stop_workspace": return await apiClient.deleteWorkspace(args.id);
|
|
61
|
+
case "get_workspace_status": return await apiClient.getWorkspaceStatus(args.id);
|
|
62
|
+
case "get_memory": return await apiClient.getMemory();
|
|
63
|
+
case "add_memory": return await apiClient.addMemory(args.content, args.category);
|
|
64
|
+
case "clear_memory": return await apiClient.clearMemory();
|
|
65
|
+
case "get_usage": return await apiClient.getUsage();
|
|
149
66
|
case "get_config": {
|
|
150
67
|
const { getConfigValue } = await import("../lib/config-store.js");
|
|
151
68
|
return { key: args.key, value: getConfigValue(args.key) };
|
|
@@ -155,14 +72,42 @@ async function executeTool(name, args) {
|
|
|
155
72
|
setConfigValue(args.key, args.value);
|
|
156
73
|
return { key: args.key, value: args.value, status: "updated" };
|
|
157
74
|
}
|
|
158
|
-
default:
|
|
159
|
-
return { error: `Unknown tool: ${name}` };
|
|
75
|
+
default: return { error: `Unknown cloud tool: ${name}` };
|
|
160
76
|
}
|
|
161
77
|
} catch (err) {
|
|
162
78
|
return { error: err.message };
|
|
163
79
|
}
|
|
164
80
|
}
|
|
165
81
|
|
|
82
|
+
// Ask user for confirmation before destructive operations
|
|
83
|
+
async function confirmAction(toolName, input) {
|
|
84
|
+
const desc = toolName === "run_command" ? input.command : JSON.stringify(input);
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
87
|
+
rl.question(chalk.yellow(` Allow ${chalk.cyan(toolName)}: ${chalk.dim(desc)}? [Y/n] `), (answer) => {
|
|
88
|
+
rl.close();
|
|
89
|
+
resolve(answer.trim().toLowerCase() !== "n");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Unified tool executor
|
|
95
|
+
async function executeTool(name, args) {
|
|
96
|
+
if (CLOUD_TOOL_NAMES.has(name)) {
|
|
97
|
+
return executeCloudTool(name, args);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Local tool — check if it needs confirmation
|
|
101
|
+
if (requiresConfirmation(name)) {
|
|
102
|
+
const allowed = await confirmAction(name, args);
|
|
103
|
+
if (!allowed) {
|
|
104
|
+
return { error: "User denied execution" };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return executeLocalTool(name, args);
|
|
109
|
+
}
|
|
110
|
+
|
|
166
111
|
// Parse SSE stream from the cloud proxy
|
|
167
112
|
async function* parseSSEStream(response) {
|
|
168
113
|
const reader = response.body.getReader();
|
|
@@ -194,15 +139,17 @@ async function* parseSSEStream(response) {
|
|
|
194
139
|
}
|
|
195
140
|
|
|
196
141
|
export async function runAgent(userRequest) {
|
|
142
|
+
const ALL_TOOLS = buildTools();
|
|
143
|
+
|
|
197
144
|
const messages = [
|
|
198
145
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
199
146
|
{ role: "user", content: userRequest },
|
|
200
147
|
];
|
|
201
148
|
|
|
202
|
-
const MAX_ITERATIONS =
|
|
149
|
+
const MAX_ITERATIONS = 15;
|
|
203
150
|
|
|
204
151
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
205
|
-
const response = await apiClient.chatStream(messages,
|
|
152
|
+
const response = await apiClient.chatStream(messages, ALL_TOOLS);
|
|
206
153
|
|
|
207
154
|
let fullText = "";
|
|
208
155
|
const toolCalls = {};
|
|
@@ -255,7 +202,9 @@ export async function runAgent(userRequest) {
|
|
|
255
202
|
let input = {};
|
|
256
203
|
try { input = JSON.parse(tc.arguments); } catch {}
|
|
257
204
|
|
|
258
|
-
|
|
205
|
+
const isLocal = !CLOUD_TOOL_NAMES.has(tc.name);
|
|
206
|
+
const prefix = isLocal ? chalk.magenta("local") : chalk.blue("cloud");
|
|
207
|
+
log.info(`${prefix} ${chalk.cyan(tc.name)}${Object.keys(input).length ? chalk.dim(" " + truncate(JSON.stringify(input), 80)) : ""}`);
|
|
259
208
|
|
|
260
209
|
const result = await executeTool(tc.name, input);
|
|
261
210
|
|
|
@@ -265,22 +214,30 @@ export async function runAgent(userRequest) {
|
|
|
265
214
|
log.success(tc.name);
|
|
266
215
|
}
|
|
267
216
|
|
|
217
|
+
// Truncate large tool results to avoid blowing context
|
|
218
|
+
const resultStr = JSON.stringify(result);
|
|
219
|
+
const truncatedResult = resultStr.length > 8000 ? resultStr.slice(0, 8000) + "...(truncated)" : resultStr;
|
|
220
|
+
|
|
268
221
|
messages.push({
|
|
269
222
|
role: "tool",
|
|
270
223
|
tool_call_id: tc.id,
|
|
271
|
-
content:
|
|
224
|
+
content: truncatedResult,
|
|
272
225
|
});
|
|
273
226
|
}
|
|
274
227
|
}
|
|
275
228
|
|
|
276
|
-
log.warn("Agent reached maximum iterations (
|
|
229
|
+
log.warn("Agent reached maximum iterations (15). Stopping.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function truncate(str, max) {
|
|
233
|
+
return str.length > max ? str.slice(0, max) + "..." : str;
|
|
277
234
|
}
|
|
278
235
|
|
|
279
236
|
export function registerAgentCommand(program) {
|
|
280
237
|
program
|
|
281
238
|
.command("agent")
|
|
282
239
|
.argument("<request...>")
|
|
283
|
-
.description("AI agent — natural language command interface")
|
|
240
|
+
.description("AI coding agent — natural language command interface")
|
|
284
241
|
.action(async (requestParts) => {
|
|
285
242
|
const request = requestParts.join(" ");
|
|
286
243
|
try {
|
package/src/commands/login.js
CHANGED
|
@@ -13,12 +13,10 @@ export async function loginFlow() {
|
|
|
13
13
|
|
|
14
14
|
log.info("Step 1: Open this URL in your browser to authenticate:");
|
|
15
15
|
log.blank();
|
|
16
|
-
console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/github`)}`);
|
|
16
|
+
console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/github?source=cli`)}`);
|
|
17
17
|
log.blank();
|
|
18
|
-
log.info("Step 2: After login,
|
|
19
|
-
|
|
20
|
-
log.blank();
|
|
21
|
-
log.info("Step 3: Copy your API token and paste it below.");
|
|
18
|
+
log.info("Step 2: After GitHub login, your token will be shown automatically.");
|
|
19
|
+
log.info(" Copy it and paste it below.");
|
|
22
20
|
log.blank();
|
|
23
21
|
|
|
24
22
|
const { token } = await inquirer.prompt([
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
export const runCommand = {
|
|
4
|
+
name: "run_command",
|
|
5
|
+
description: "Execute a shell command and return its output. Use for running builds, tests, git commands, installing packages, etc.",
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
command: { type: "string", description: "The shell command to execute" },
|
|
10
|
+
cwd: { type: "string", description: "Working directory (optional, defaults to current directory)" },
|
|
11
|
+
},
|
|
12
|
+
required: ["command"],
|
|
13
|
+
},
|
|
14
|
+
requiresConfirmation: true,
|
|
15
|
+
handler: async ({ command, cwd }) => {
|
|
16
|
+
try {
|
|
17
|
+
const output = execSync(command, {
|
|
18
|
+
cwd: cwd || process.cwd(),
|
|
19
|
+
encoding: "utf-8",
|
|
20
|
+
timeout: 120_000,
|
|
21
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
});
|
|
24
|
+
return { command, exitCode: 0, stdout: output, stderr: "" };
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return {
|
|
27
|
+
command,
|
|
28
|
+
exitCode: err.status ?? 1,
|
|
29
|
+
stdout: err.stdout || "",
|
|
30
|
+
stderr: err.stderr || err.message,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { dirname, resolve } from "path";
|
|
3
|
+
|
|
4
|
+
export const readFile = {
|
|
5
|
+
name: "read_file",
|
|
6
|
+
description: "Read the contents of a file at the given path. Returns the file contents as a string.",
|
|
7
|
+
parameters: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
path: { type: "string", description: "Absolute or relative file path to read" },
|
|
11
|
+
},
|
|
12
|
+
required: ["path"],
|
|
13
|
+
},
|
|
14
|
+
handler: async ({ path }) => {
|
|
15
|
+
const resolved = resolve(path);
|
|
16
|
+
if (!existsSync(resolved)) {
|
|
17
|
+
return { error: `File not found: ${resolved}` };
|
|
18
|
+
}
|
|
19
|
+
const content = readFileSync(resolved, "utf-8");
|
|
20
|
+
return { path: resolved, content, lines: content.split("\n").length };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const writeFile = {
|
|
25
|
+
name: "write_file",
|
|
26
|
+
description: "Create or overwrite a file with the given content. Creates parent directories if needed.",
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
path: { type: "string", description: "Absolute or relative file path to write" },
|
|
31
|
+
content: { type: "string", description: "The content to write to the file" },
|
|
32
|
+
},
|
|
33
|
+
required: ["path", "content"],
|
|
34
|
+
},
|
|
35
|
+
handler: async ({ path, content }) => {
|
|
36
|
+
const resolved = resolve(path);
|
|
37
|
+
const dir = dirname(resolved);
|
|
38
|
+
if (!existsSync(dir)) {
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
writeFileSync(resolved, content, "utf-8");
|
|
42
|
+
return { path: resolved, bytesWritten: Buffer.byteLength(content, "utf-8") };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const editFile = {
|
|
47
|
+
name: "edit_file",
|
|
48
|
+
description: "Edit a file by replacing an exact string match with new content. The old_string must match exactly (including whitespace and indentation).",
|
|
49
|
+
parameters: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
path: { type: "string", description: "Absolute or relative file path to edit" },
|
|
53
|
+
old_string: { type: "string", description: "The exact string to find and replace" },
|
|
54
|
+
new_string: { type: "string", description: "The replacement string" },
|
|
55
|
+
},
|
|
56
|
+
required: ["path", "old_string", "new_string"],
|
|
57
|
+
},
|
|
58
|
+
handler: async ({ path, old_string, new_string }) => {
|
|
59
|
+
const resolved = resolve(path);
|
|
60
|
+
if (!existsSync(resolved)) {
|
|
61
|
+
return { error: `File not found: ${resolved}` };
|
|
62
|
+
}
|
|
63
|
+
const content = readFileSync(resolved, "utf-8");
|
|
64
|
+
if (!content.includes(old_string)) {
|
|
65
|
+
return { error: "old_string not found in file. Make sure it matches exactly, including whitespace." };
|
|
66
|
+
}
|
|
67
|
+
const count = content.split(old_string).length - 1;
|
|
68
|
+
const newContent = content.replace(old_string, new_string);
|
|
69
|
+
writeFileSync(resolved, newContent, "utf-8");
|
|
70
|
+
return { path: resolved, replacements: 1, totalOccurrences: count };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile, writeFile, editFile } from "./file-tools.js";
|
|
2
|
+
import { runCommand } from "./command-tools.js";
|
|
3
|
+
import { searchFiles, searchContent, listDirectory } from "./search-tools.js";
|
|
4
|
+
|
|
5
|
+
const tools = [readFile, writeFile, editFile, runCommand, searchFiles, searchContent, listDirectory];
|
|
6
|
+
|
|
7
|
+
const toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
8
|
+
|
|
9
|
+
export function getToolDefinitions() {
|
|
10
|
+
return tools.map(({ name, description, parameters }) => ({ name, description, parameters }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getTool(name) {
|
|
14
|
+
return toolMap.get(name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function executeTool(name, input) {
|
|
18
|
+
const tool = toolMap.get(name);
|
|
19
|
+
if (!tool) {
|
|
20
|
+
return { error: `Unknown tool: ${name}` };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return await tool.handler(input);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return { error: err.message };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function requiresConfirmation(name) {
|
|
30
|
+
const tool = toolMap.get(name);
|
|
31
|
+
return tool?.requiresConfirmation === true;
|
|
32
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readdirSync, statSync, existsSync } from "fs";
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
export const searchFiles = {
|
|
6
|
+
name: "search_files",
|
|
7
|
+
description: "Search for files matching a glob/name pattern. Returns matching file paths.",
|
|
8
|
+
parameters: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
pattern: { type: "string", description: "File name pattern to search for (e.g., '*.js', 'config*', 'test')" },
|
|
12
|
+
path: { type: "string", description: "Directory to search in (defaults to current directory)" },
|
|
13
|
+
},
|
|
14
|
+
required: ["pattern"],
|
|
15
|
+
},
|
|
16
|
+
handler: async ({ pattern, path }) => {
|
|
17
|
+
const dir = resolve(path || ".");
|
|
18
|
+
try {
|
|
19
|
+
const result = execSync(`find ${JSON.stringify(dir)} -name ${JSON.stringify(pattern)} -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -50`, {
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
timeout: 10_000,
|
|
22
|
+
});
|
|
23
|
+
const files = result.trim().split("\n").filter(Boolean);
|
|
24
|
+
return { pattern, searchPath: dir, matches: files, count: files.length };
|
|
25
|
+
} catch {
|
|
26
|
+
return { pattern, searchPath: dir, matches: [], count: 0 };
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const searchContent = {
|
|
32
|
+
name: "search_content",
|
|
33
|
+
description: "Search for a text pattern inside files (like grep). Returns matching lines with file paths and line numbers.",
|
|
34
|
+
parameters: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
pattern: { type: "string", description: "Text or regex pattern to search for" },
|
|
38
|
+
path: { type: "string", description: "Directory to search in (defaults to current directory)" },
|
|
39
|
+
include: { type: "string", description: "File glob to filter (e.g., '*.js')" },
|
|
40
|
+
},
|
|
41
|
+
required: ["pattern"],
|
|
42
|
+
},
|
|
43
|
+
handler: async ({ pattern, path, include }) => {
|
|
44
|
+
const dir = resolve(path || ".");
|
|
45
|
+
try {
|
|
46
|
+
let cmd = `grep -rn --include='${include || "*"}' ${JSON.stringify(pattern)} ${JSON.stringify(dir)} 2>/dev/null | head -50`;
|
|
47
|
+
const result = execSync(cmd, { encoding: "utf-8", timeout: 10_000 });
|
|
48
|
+
const lines = result.trim().split("\n").filter(Boolean);
|
|
49
|
+
const matches = lines.map((line) => {
|
|
50
|
+
const match = line.match(/^(.+?):(\d+):(.*)$/);
|
|
51
|
+
if (match) return { file: match[1], line: parseInt(match[2]), content: match[3].trim() };
|
|
52
|
+
return { raw: line };
|
|
53
|
+
});
|
|
54
|
+
return { pattern, searchPath: dir, matches, count: matches.length };
|
|
55
|
+
} catch {
|
|
56
|
+
return { pattern, searchPath: dir, matches: [], count: 0 };
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const listDirectory = {
|
|
62
|
+
name: "list_directory",
|
|
63
|
+
description: "List files and directories at the given path. Shows names, types, and sizes.",
|
|
64
|
+
parameters: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
path: { type: "string", description: "Directory path to list (defaults to current directory)" },
|
|
68
|
+
},
|
|
69
|
+
required: [],
|
|
70
|
+
},
|
|
71
|
+
handler: async ({ path }) => {
|
|
72
|
+
const dir = resolve(path || ".");
|
|
73
|
+
if (!existsSync(dir)) {
|
|
74
|
+
return { error: `Directory not found: ${dir}` };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const entries = readdirSync(dir).map((name) => {
|
|
78
|
+
try {
|
|
79
|
+
const fullPath = join(dir, name);
|
|
80
|
+
const stat = statSync(fullPath);
|
|
81
|
+
return {
|
|
82
|
+
name,
|
|
83
|
+
type: stat.isDirectory() ? "directory" : "file",
|
|
84
|
+
size: stat.isDirectory() ? undefined : stat.size,
|
|
85
|
+
};
|
|
86
|
+
} catch {
|
|
87
|
+
return { name, type: "unknown" };
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return { path: dir, entries, count: entries.length };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { error: err.message };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
package/src/repl.js
CHANGED