noumen 0.1.0 → 0.2.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/README.md +767 -51
- package/dist/a2a/index.d.ts +148 -0
- package/dist/a2a/index.js +579 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/acp/index.d.ts +129 -0
- package/dist/acp/index.js +498 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/agent-BrkbZyOT.d.ts +1028 -0
- package/dist/cache-DVqaCX8v.d.ts +38 -0
- package/dist/chunk-2ZTGQLYK.js +356 -0
- package/dist/chunk-2ZTGQLYK.js.map +1 -0
- package/dist/chunk-42PHHZUA.js +132 -0
- package/dist/chunk-42PHHZUA.js.map +1 -0
- package/dist/chunk-4SQA2UCV.js +26 -0
- package/dist/chunk-4SQA2UCV.js.map +1 -0
- package/dist/chunk-5GEX6ZSB.js +179 -0
- package/dist/chunk-5GEX6ZSB.js.map +1 -0
- package/dist/chunk-7ZMN7XJE.js +94 -0
- package/dist/chunk-7ZMN7XJE.js.map +1 -0
- package/dist/chunk-AMYIJSAZ.js +57 -0
- package/dist/chunk-AMYIJSAZ.js.map +1 -0
- package/dist/chunk-BGG2E6JD.js +10 -0
- package/dist/chunk-BGG2E6JD.js.map +1 -0
- package/dist/chunk-BZSFUEWM.js +43 -0
- package/dist/chunk-BZSFUEWM.js.map +1 -0
- package/dist/chunk-CPFHEPW4.js +139 -0
- package/dist/chunk-CPFHEPW4.js.map +1 -0
- package/dist/chunk-D43BWEZA.js +346 -0
- package/dist/chunk-D43BWEZA.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-JACGEMTF.js +43 -0
- package/dist/chunk-JACGEMTF.js.map +1 -0
- package/dist/chunk-JX7CLUCV.js +21 -0
- package/dist/chunk-JX7CLUCV.js.map +1 -0
- package/dist/chunk-KXDB56YW.js +39 -0
- package/dist/chunk-KXDB56YW.js.map +1 -0
- package/dist/chunk-KY6ZPWHO.js +112 -0
- package/dist/chunk-KY6ZPWHO.js.map +1 -0
- package/dist/chunk-NBDFQYUZ.js +7992 -0
- package/dist/chunk-NBDFQYUZ.js.map +1 -0
- package/dist/chunk-OGXNFXFA.js +196 -0
- package/dist/chunk-OGXNFXFA.js.map +1 -0
- package/dist/chunk-QTJ7VTJY.js +1994 -0
- package/dist/chunk-QTJ7VTJY.js.map +1 -0
- package/dist/chunk-UVSSQBDY.js +192 -0
- package/dist/chunk-UVSSQBDY.js.map +1 -0
- package/dist/chunk-Y45R3PQL.js +684 -0
- package/dist/chunk-Y45R3PQL.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +868 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.d.ts +64 -0
- package/dist/client/index.js +409 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client-CRRO2376.js +10 -0
- package/dist/client-CRRO2376.js.map +1 -0
- package/dist/headless-Q7XHHZIW.js +143 -0
- package/dist/headless-Q7XHHZIW.js.map +1 -0
- package/dist/history-snip-64GYP4ZL.js +12 -0
- package/dist/history-snip-64GYP4ZL.js.map +1 -0
- package/dist/index.d.ts +1305 -418
- package/dist/index.js +384 -1757
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc/index.d.ts +54 -0
- package/dist/jsonrpc/index.js +34 -0
- package/dist/jsonrpc/index.js.map +1 -0
- package/dist/lsp/index.d.ts +36 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/index.js.map +1 -0
- package/dist/lsp-PS3BWIHC.js +8 -0
- package/dist/lsp-PS3BWIHC.js.map +1 -0
- package/dist/manager-DLXK63XC.js +8 -0
- package/dist/manager-DLXK63XC.js.map +1 -0
- package/dist/mcp/index.d.ts +111 -0
- package/dist/mcp/index.js +104 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp-auth-AEI2R4ZC.js +9 -0
- package/dist/mcp-auth-AEI2R4ZC.js.map +1 -0
- package/dist/ollama-YNXAYP3R.js +18 -0
- package/dist/ollama-YNXAYP3R.js.map +1 -0
- package/dist/provider-factory-34MSWJZ3.js +20 -0
- package/dist/provider-factory-34MSWJZ3.js.map +1 -0
- package/dist/providers/anthropic.d.ts +19 -0
- package/dist/providers/anthropic.js +33 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/bedrock.d.ts +39 -0
- package/dist/providers/bedrock.js +54 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/gemini.d.ts +16 -0
- package/dist/providers/gemini.js +224 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/openai.d.ts +18 -0
- package/dist/providers/openai.js +8 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +16 -0
- package/dist/providers/openrouter.js +23 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/vertex.d.ts +40 -0
- package/dist/providers/vertex.js +64 -0
- package/dist/providers/vertex.js.map +1 -0
- package/dist/render-GRN4ZSSW.js +14 -0
- package/dist/render-GRN4ZSSW.js.map +1 -0
- package/dist/resolve-XM52G7YE.js +14 -0
- package/dist/resolve-XM52G7YE.js.map +1 -0
- package/dist/server/index.d.ts +128 -0
- package/dist/server/index.js +626 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server-Cg1yWGaV.d.ts +96 -0
- package/dist/spinner-OJNR6NFO.js +8 -0
- package/dist/spinner-OJNR6NFO.js.map +1 -0
- package/dist/types-2kTLUCnD.d.ts +107 -0
- package/dist/types-3c88cRKH.d.ts +547 -0
- package/dist/types-CwKKucOF.d.ts +620 -0
- package/dist/types-DwdzmXfs.d.ts +107 -0
- package/dist/types-NIyVwQ4h.d.ts +109 -0
- package/dist/types-QwfylltH.d.ts +71 -0
- package/package.json +134 -6
|
@@ -0,0 +1,1994 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMAGE_EXTENSIONS,
|
|
3
|
+
compressImageBufferWithTokenLimit,
|
|
4
|
+
createImageMetadataText,
|
|
5
|
+
maybeResizeAndDownsampleImageBuffer
|
|
6
|
+
} from "./chunk-5GEX6ZSB.js";
|
|
7
|
+
|
|
8
|
+
// src/tools/tool-search.ts
|
|
9
|
+
var TOOL_SEARCH_NAME = "ToolSearch";
|
|
10
|
+
function isDeferredTool(tool) {
|
|
11
|
+
if (tool.alwaysLoad === true) return false;
|
|
12
|
+
if (tool.mcpInfo !== void 0) return true;
|
|
13
|
+
if (tool.name === TOOL_SEARCH_NAME) return false;
|
|
14
|
+
return tool.shouldDefer === true;
|
|
15
|
+
}
|
|
16
|
+
function formatDeferredToolLine(tool) {
|
|
17
|
+
const desc = tool.description.split(".")[0];
|
|
18
|
+
return `- ${tool.name}: ${desc}`;
|
|
19
|
+
}
|
|
20
|
+
function parseToolName(name) {
|
|
21
|
+
if (name.startsWith("mcp__") || name.includes("__")) {
|
|
22
|
+
const withoutPrefix = name.replace(/^mcp__/, "").toLowerCase();
|
|
23
|
+
const parts2 = withoutPrefix.split("__").flatMap((p) => p.split("_"));
|
|
24
|
+
return { parts: parts2.filter(Boolean), full: withoutPrefix.replace(/__/g, " ").replace(/_/g, " "), isMcp: true };
|
|
25
|
+
}
|
|
26
|
+
const parts = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").toLowerCase().split(/\s+/).filter(Boolean);
|
|
27
|
+
return { parts, full: parts.join(" "), isMcp: false };
|
|
28
|
+
}
|
|
29
|
+
function escapeRegExp(s) {
|
|
30
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
}
|
|
32
|
+
function searchToolsWithKeywords(query, deferredTools, allTools, maxResults) {
|
|
33
|
+
const queryLower = query.toLowerCase().trim();
|
|
34
|
+
const exactMatch = deferredTools.find((t) => t.name.toLowerCase() === queryLower) ?? allTools.find((t) => t.name.toLowerCase() === queryLower);
|
|
35
|
+
if (exactMatch) return [exactMatch.name];
|
|
36
|
+
if (queryLower.startsWith("mcp__") && queryLower.length > 5) {
|
|
37
|
+
const prefixMatches = deferredTools.filter((t) => t.name.toLowerCase().startsWith(queryLower)).slice(0, maxResults).map((t) => t.name);
|
|
38
|
+
if (prefixMatches.length > 0) return prefixMatches;
|
|
39
|
+
}
|
|
40
|
+
const queryTerms = queryLower.split(/\s+/).filter((t) => t.length > 0);
|
|
41
|
+
const requiredTerms = [];
|
|
42
|
+
const optionalTerms = [];
|
|
43
|
+
for (const term of queryTerms) {
|
|
44
|
+
if (term.startsWith("+") && term.length > 1) {
|
|
45
|
+
requiredTerms.push(term.slice(1));
|
|
46
|
+
} else {
|
|
47
|
+
optionalTerms.push(term);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const allScoringTerms = requiredTerms.length > 0 ? [...requiredTerms, ...optionalTerms] : queryTerms;
|
|
51
|
+
const termPatterns = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const term of allScoringTerms) {
|
|
53
|
+
if (!termPatterns.has(term)) {
|
|
54
|
+
termPatterns.set(term, new RegExp(`\\b${escapeRegExp(term)}\\b`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let candidates = deferredTools;
|
|
58
|
+
if (requiredTerms.length > 0) {
|
|
59
|
+
candidates = deferredTools.filter((tool) => {
|
|
60
|
+
const parsed = parseToolName(tool.name);
|
|
61
|
+
const descLower = tool.description.toLowerCase();
|
|
62
|
+
return requiredTerms.every((term) => {
|
|
63
|
+
const pattern = termPatterns.get(term);
|
|
64
|
+
return parsed.parts.includes(term) || parsed.parts.some((part) => part.includes(term)) || pattern.test(descLower);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const scored = candidates.map((tool) => {
|
|
69
|
+
const parsed = parseToolName(tool.name);
|
|
70
|
+
const descLower = tool.description.toLowerCase();
|
|
71
|
+
let score = 0;
|
|
72
|
+
for (const term of allScoringTerms) {
|
|
73
|
+
const pattern = termPatterns.get(term);
|
|
74
|
+
if (parsed.parts.includes(term)) {
|
|
75
|
+
score += parsed.isMcp ? 12 : 10;
|
|
76
|
+
} else if (parsed.parts.some((part) => part.includes(term))) {
|
|
77
|
+
score += parsed.isMcp ? 6 : 5;
|
|
78
|
+
}
|
|
79
|
+
if (parsed.full.includes(term) && score === 0) {
|
|
80
|
+
score += 3;
|
|
81
|
+
}
|
|
82
|
+
if (pattern.test(descLower)) {
|
|
83
|
+
score += 2;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { name: tool.name, score };
|
|
87
|
+
});
|
|
88
|
+
return scored.filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults).map((item) => item.name);
|
|
89
|
+
}
|
|
90
|
+
function formatToolSchemas(tools) {
|
|
91
|
+
if (tools.length === 0) return "No matching deferred tools found.";
|
|
92
|
+
const lines = tools.map((t) => {
|
|
93
|
+
const schema = {
|
|
94
|
+
description: t.description,
|
|
95
|
+
name: t.name,
|
|
96
|
+
parameters: t.parameters
|
|
97
|
+
};
|
|
98
|
+
return `<function>${JSON.stringify(schema)}</function>`;
|
|
99
|
+
});
|
|
100
|
+
return `<functions>
|
|
101
|
+
${lines.join("\n")}
|
|
102
|
+
</functions>`;
|
|
103
|
+
}
|
|
104
|
+
function createToolSearchTool(getDeferredTools, getAllTools, getToolsByNames, onDiscovered) {
|
|
105
|
+
return {
|
|
106
|
+
name: TOOL_SEARCH_NAME,
|
|
107
|
+
description: 'Fetches full schema definitions for deferred tools so they can be called. Deferred tools appear by name in <available-deferred-tools> sections. Until fetched, only the name is known \u2014 there is no parameter schema, so the tool cannot be invoked. Use this tool to load tool schemas.\n\nQuery forms:\n- "select:Read,Edit,Grep" \u2014 fetch these exact tools by name\n- "notebook jupyter" \u2014 keyword search, up to max_results best matches\n- "+slack send" \u2014 require "slack" in the name, rank by remaining terms',
|
|
108
|
+
isReadOnly: true,
|
|
109
|
+
isConcurrencySafe: true,
|
|
110
|
+
parameters: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
query: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: 'Query to find deferred tools. Use "select:<tool_name>" for direct selection, or keywords to search.'
|
|
116
|
+
},
|
|
117
|
+
max_results: {
|
|
118
|
+
type: "number",
|
|
119
|
+
description: "Maximum number of results to return (default: 5)"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
required: ["query"]
|
|
123
|
+
},
|
|
124
|
+
async call(args) {
|
|
125
|
+
const query = args.query;
|
|
126
|
+
const maxResults = args.max_results ?? 5;
|
|
127
|
+
const deferredTools = getDeferredTools();
|
|
128
|
+
const allTools = getAllTools();
|
|
129
|
+
const selectMatch = query.match(/^select:(.+)$/i);
|
|
130
|
+
if (selectMatch) {
|
|
131
|
+
const requested = selectMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
132
|
+
const found = [];
|
|
133
|
+
for (const toolName of requested) {
|
|
134
|
+
const match = deferredTools.find((t) => t.name.toLowerCase() === toolName.toLowerCase()) ?? allTools.find((t) => t.name.toLowerCase() === toolName.toLowerCase());
|
|
135
|
+
if (match && !found.includes(match.name)) {
|
|
136
|
+
found.push(match.name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (found.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
content: JSON.stringify({
|
|
142
|
+
matches: [],
|
|
143
|
+
query,
|
|
144
|
+
total_deferred_tools: deferredTools.length
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
onDiscovered(found);
|
|
149
|
+
const matchedTools2 = getToolsByNames(found);
|
|
150
|
+
return { content: formatToolSchemas(matchedTools2) };
|
|
151
|
+
}
|
|
152
|
+
const matches = searchToolsWithKeywords(query, deferredTools, allTools, maxResults);
|
|
153
|
+
if (matches.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
content: JSON.stringify({
|
|
156
|
+
matches: [],
|
|
157
|
+
query,
|
|
158
|
+
total_deferred_tools: deferredTools.length
|
|
159
|
+
})
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
onDiscovered(matches);
|
|
163
|
+
const matchedTools = getToolsByNames(matches);
|
|
164
|
+
return { content: formatToolSchemas(matchedTools) };
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/utils/zod.ts
|
|
170
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
171
|
+
function zodToJsonSchema(schema) {
|
|
172
|
+
const hit = cache.get(schema);
|
|
173
|
+
if (hit) return hit;
|
|
174
|
+
const zod = schema._zod ? schema : void 0;
|
|
175
|
+
if (!zod) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"zodToJsonSchema requires a Zod v4 schema. Install zod and pass a z.object(\u2026) schema."
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
let toJSONSchema;
|
|
181
|
+
try {
|
|
182
|
+
const sAny = schema;
|
|
183
|
+
if (typeof sAny._toJSONSchema === "function") {
|
|
184
|
+
const result = sAny._toJSONSchema();
|
|
185
|
+
cache.set(schema, result);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
toJSONSchema = globalThis.__noumen_toJSONSchema;
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
if (toJSONSchema) {
|
|
192
|
+
const result = toJSONSchema(schema);
|
|
193
|
+
cache.set(schema, result);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
throw new Error(
|
|
197
|
+
"Could not convert Zod schema to JSON Schema. Call `registerZodToJsonSchema(toJSONSchema)` from zod/v4 or upgrade to Zod v4."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
function registerZodToJsonSchema(fn) {
|
|
201
|
+
globalThis.__noumen_toJSONSchema = fn;
|
|
202
|
+
}
|
|
203
|
+
function formatZodValidationError(toolName, issues) {
|
|
204
|
+
if (!issues || !issues.issues.length) {
|
|
205
|
+
return `${toolName}: validation failed with unknown error`;
|
|
206
|
+
}
|
|
207
|
+
const parts = [];
|
|
208
|
+
const missing = issues.issues.filter(
|
|
209
|
+
(i) => i.code === "invalid_type" && i.message.includes("required")
|
|
210
|
+
);
|
|
211
|
+
const unrecognized = issues.issues.filter(
|
|
212
|
+
(i) => i.code === "unrecognized_keys"
|
|
213
|
+
);
|
|
214
|
+
const other = issues.issues.filter(
|
|
215
|
+
(i) => !missing.includes(i) && !unrecognized.includes(i)
|
|
216
|
+
);
|
|
217
|
+
if (missing.length) {
|
|
218
|
+
parts.push(
|
|
219
|
+
`Missing required parameter${missing.length > 1 ? "s" : ""}: ${missing.map((m) => formatPath(m.path)).join(", ")}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (unrecognized.length) {
|
|
223
|
+
parts.push(
|
|
224
|
+
`Unrecognized parameter${unrecognized.length > 1 ? "s" : ""}: ${unrecognized.map((u) => u.message).join(", ")}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
for (const issue of other) {
|
|
228
|
+
const path2 = formatPath(issue.path);
|
|
229
|
+
parts.push(`${path2 ? path2 + ": " : ""}${issue.message}`);
|
|
230
|
+
}
|
|
231
|
+
return `${toolName} failed due to the following ${parts.length > 1 ? "issues" : "issue"}:
|
|
232
|
+
${parts.join("\n")}`;
|
|
233
|
+
}
|
|
234
|
+
function formatPath(path2) {
|
|
235
|
+
return path2.map((p, i) => typeof p === "number" ? `[${p}]` : i > 0 ? `.${p}` : p).join("");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/tools/prompts/read.ts
|
|
239
|
+
var READ_PROMPT = `Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
240
|
+
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
241
|
+
|
|
242
|
+
Usage:
|
|
243
|
+
- The file_path parameter must be an absolute path, not a relative path.
|
|
244
|
+
- By default, it reads the entire file. Use offset and limit to read specific portions of large files.
|
|
245
|
+
- Lines in the output are numbered with the format: LINE_NUMBER|LINE_CONTENT
|
|
246
|
+
- If you read a file that exists but has empty contents you will receive a notice in place of file contents.
|
|
247
|
+
- This tool can read image files (e.g. PNG, JPG) when the provider supports multimodal input.
|
|
248
|
+
- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs.
|
|
249
|
+
- This tool can only read files, not directories. To list a directory, use an ls command via the Bash tool.
|
|
250
|
+
- If the file has not changed since the last read, a "file_unchanged" result is returned to save context tokens.
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
// src/tools/read.ts
|
|
254
|
+
import * as path from "path";
|
|
255
|
+
var DEFAULT_MAX_IMAGE_TOKENS = 1600;
|
|
256
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
257
|
+
var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
|
|
258
|
+
"/dev/zero",
|
|
259
|
+
"/dev/random",
|
|
260
|
+
"/dev/urandom",
|
|
261
|
+
"/dev/full",
|
|
262
|
+
"/dev/stdin",
|
|
263
|
+
"/dev/tty",
|
|
264
|
+
"/dev/console",
|
|
265
|
+
"/dev/stdout",
|
|
266
|
+
"/dev/stderr",
|
|
267
|
+
"/dev/fd/0",
|
|
268
|
+
"/dev/fd/1",
|
|
269
|
+
"/dev/fd/2"
|
|
270
|
+
]);
|
|
271
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
272
|
+
".exe",
|
|
273
|
+
".dll",
|
|
274
|
+
".so",
|
|
275
|
+
".dylib",
|
|
276
|
+
".bin",
|
|
277
|
+
".zip",
|
|
278
|
+
".tar",
|
|
279
|
+
".gz",
|
|
280
|
+
".bz2",
|
|
281
|
+
".xz",
|
|
282
|
+
".7z",
|
|
283
|
+
".rar",
|
|
284
|
+
".wasm",
|
|
285
|
+
".o",
|
|
286
|
+
".a",
|
|
287
|
+
".obj",
|
|
288
|
+
".lib",
|
|
289
|
+
".class",
|
|
290
|
+
".pyc",
|
|
291
|
+
".pyo",
|
|
292
|
+
".jar",
|
|
293
|
+
".war",
|
|
294
|
+
".ear",
|
|
295
|
+
".iso",
|
|
296
|
+
".img",
|
|
297
|
+
".dmg",
|
|
298
|
+
".msi",
|
|
299
|
+
".deb",
|
|
300
|
+
".rpm",
|
|
301
|
+
".apk",
|
|
302
|
+
".ipa"
|
|
303
|
+
]);
|
|
304
|
+
var readFileTool = {
|
|
305
|
+
name: "ReadFile",
|
|
306
|
+
description: "Read a file from the filesystem. Returns the file content with line numbers. For image files (.png, .jpg, .jpeg, .gif, .webp), returns the image data directly. Use offset and limit to read specific portions of large text files.",
|
|
307
|
+
prompt: READ_PROMPT,
|
|
308
|
+
isReadOnly: true,
|
|
309
|
+
isConcurrencySafe: true,
|
|
310
|
+
parameters: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
file_path: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "The path of the file to read (absolute or relative to cwd)"
|
|
316
|
+
},
|
|
317
|
+
offset: {
|
|
318
|
+
type: "number",
|
|
319
|
+
description: "Line number to start reading from (1-indexed). Defaults to 1.",
|
|
320
|
+
minimum: 1
|
|
321
|
+
},
|
|
322
|
+
limit: {
|
|
323
|
+
type: "number",
|
|
324
|
+
description: "Maximum number of lines to read. If omitted, reads entire file.",
|
|
325
|
+
minimum: 1
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
required: ["file_path"]
|
|
329
|
+
},
|
|
330
|
+
async call(args, ctx) {
|
|
331
|
+
const filePath = args.file_path;
|
|
332
|
+
const offset = args.offset ?? 1;
|
|
333
|
+
const limit = args.limit;
|
|
334
|
+
try {
|
|
335
|
+
const resolved = path.resolve(ctx.cwd, filePath);
|
|
336
|
+
if (BLOCKED_DEVICE_PATHS.has(resolved)) {
|
|
337
|
+
return {
|
|
338
|
+
content: `Error: Cannot read device file ${filePath}.`,
|
|
339
|
+
isError: true
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
343
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
344
|
+
return {
|
|
345
|
+
content: `Error: Cannot read binary ${ext} file. This tool only reads text files.`,
|
|
346
|
+
isError: true
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (IMAGE_EXTENSIONS.has(ext) && ctx.fs.readFileBytes) {
|
|
350
|
+
return readImageFile(filePath, ext, ctx);
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const stat = await ctx.fs.stat(filePath);
|
|
354
|
+
if (stat.size !== void 0 && stat.size > MAX_FILE_SIZE) {
|
|
355
|
+
return {
|
|
356
|
+
content: `Error: File is too large (${Math.round(stat.size / 1024 / 1024)}MB). Use offset/limit to read specific portions.`,
|
|
357
|
+
isError: true
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
if (ctx.fileStateCache) {
|
|
363
|
+
const cached = ctx.fileStateCache.get(filePath);
|
|
364
|
+
if (cached && !cached.isPartialView && cached.offset !== void 0 && cached.offset === offset && cached.limit === limit) {
|
|
365
|
+
try {
|
|
366
|
+
const stat = await ctx.fs.stat(filePath);
|
|
367
|
+
const mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
|
|
368
|
+
if (mtime === cached.timestamp) {
|
|
369
|
+
return { content: "file_unchanged" };
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const content = await ctx.fs.readFile(filePath);
|
|
376
|
+
const lines = content.split("\n");
|
|
377
|
+
const startIdx = Math.max(0, offset - 1);
|
|
378
|
+
const endIdx = limit ? Math.min(lines.length, startIdx + limit) : lines.length;
|
|
379
|
+
const selectedLines = lines.slice(startIdx, endIdx);
|
|
380
|
+
const numbered = selectedLines.map(
|
|
381
|
+
(line, i) => `${String(startIdx + i + 1).padStart(6)}|${line}`
|
|
382
|
+
);
|
|
383
|
+
let result = numbered.join("\n");
|
|
384
|
+
if (endIdx < lines.length) {
|
|
385
|
+
result += `
|
|
386
|
+
... ${lines.length - endIdx} lines not shown ...`;
|
|
387
|
+
}
|
|
388
|
+
if (ctx.fileStateCache) {
|
|
389
|
+
let mtime = 0;
|
|
390
|
+
try {
|
|
391
|
+
const stat = await ctx.fs.stat(filePath);
|
|
392
|
+
mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
ctx.fileStateCache.set(filePath, {
|
|
396
|
+
content: selectedLines.join("\n"),
|
|
397
|
+
timestamp: mtime,
|
|
398
|
+
offset,
|
|
399
|
+
limit
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return { content: result || "File is empty." };
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return {
|
|
405
|
+
content: `Error reading file: ${err instanceof Error ? err.message : String(err)}`,
|
|
406
|
+
isError: true
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
async function readImageFile(filePath, ext, ctx) {
|
|
412
|
+
const imageBuffer = await ctx.fs.readFileBytes(filePath);
|
|
413
|
+
const originalSize = imageBuffer.length;
|
|
414
|
+
const formatExt = ext.replace(/^\./, "");
|
|
415
|
+
const resized = await maybeResizeAndDownsampleImageBuffer(
|
|
416
|
+
imageBuffer,
|
|
417
|
+
originalSize,
|
|
418
|
+
formatExt
|
|
419
|
+
);
|
|
420
|
+
let base64 = resized.buffer.toString("base64");
|
|
421
|
+
let mediaType = resized.mediaType;
|
|
422
|
+
const estimatedTokens = Math.ceil(base64.length * 0.125);
|
|
423
|
+
if (estimatedTokens > DEFAULT_MAX_IMAGE_TOKENS) {
|
|
424
|
+
try {
|
|
425
|
+
const compressed = await compressImageBufferWithTokenLimit(
|
|
426
|
+
imageBuffer,
|
|
427
|
+
DEFAULT_MAX_IMAGE_TOKENS,
|
|
428
|
+
`image/${formatExt}`
|
|
429
|
+
);
|
|
430
|
+
base64 = compressed.base64;
|
|
431
|
+
mediaType = compressed.mediaType;
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const parts = [
|
|
436
|
+
{
|
|
437
|
+
type: "image",
|
|
438
|
+
data: base64,
|
|
439
|
+
media_type: `image/${mediaType}`
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
if (resized.dimensions) {
|
|
443
|
+
parts.push({
|
|
444
|
+
type: "text",
|
|
445
|
+
text: createImageMetadataText(resized.dimensions)
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return { content: parts };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/tools/prompts/write.ts
|
|
452
|
+
var WRITE_PROMPT = `Writes a file to the local filesystem. Parent directories are created automatically if they don't exist.
|
|
453
|
+
|
|
454
|
+
Usage:
|
|
455
|
+
- This tool will overwrite the existing file if there is one at the provided path.
|
|
456
|
+
- If this is an existing file, you MUST use the ReadFile tool first to read the file's contents. This tool will fail if you did not read the file first.
|
|
457
|
+
- Prefer the EditFile tool for modifying existing files \u2014 it only sends the diff. Only use this tool to create new files or for complete rewrites.
|
|
458
|
+
- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
|
|
459
|
+
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
|
|
460
|
+
`;
|
|
461
|
+
|
|
462
|
+
// src/tools/write.ts
|
|
463
|
+
var writeFileTool = {
|
|
464
|
+
name: "WriteFile",
|
|
465
|
+
description: "Create or overwrite a file with the given content. Parent directories are created automatically if they don't exist.",
|
|
466
|
+
prompt: WRITE_PROMPT,
|
|
467
|
+
isReadOnly: false,
|
|
468
|
+
checkPermissions(args) {
|
|
469
|
+
const filePath = args.file_path;
|
|
470
|
+
return {
|
|
471
|
+
behavior: "passthrough",
|
|
472
|
+
message: `Write to ${filePath}`
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
parameters: {
|
|
476
|
+
type: "object",
|
|
477
|
+
properties: {
|
|
478
|
+
file_path: {
|
|
479
|
+
type: "string",
|
|
480
|
+
description: "The path of the file to write (absolute or relative to cwd)"
|
|
481
|
+
},
|
|
482
|
+
content: {
|
|
483
|
+
type: "string",
|
|
484
|
+
description: "The content to write to the file"
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
required: ["file_path", "content"]
|
|
488
|
+
},
|
|
489
|
+
async call(args, ctx) {
|
|
490
|
+
const filePath = args.file_path;
|
|
491
|
+
const content = args.content;
|
|
492
|
+
try {
|
|
493
|
+
if (ctx.checkpointManager && ctx.currentMessageId) {
|
|
494
|
+
await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
|
|
495
|
+
}
|
|
496
|
+
const existed = await ctx.fs.exists(filePath);
|
|
497
|
+
if (existed && ctx.fileStateCache) {
|
|
498
|
+
const cached = ctx.fileStateCache.get(filePath);
|
|
499
|
+
if (!cached) {
|
|
500
|
+
return {
|
|
501
|
+
content: `Error: File ${filePath} exists but has not been read yet. Read it first before overwriting.`,
|
|
502
|
+
isError: true
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
await ctx.fs.writeFile(filePath, content);
|
|
507
|
+
ctx.notifyHook?.("FileWrite", {
|
|
508
|
+
event: "FileWrite",
|
|
509
|
+
sessionId: ctx.sessionId ?? "",
|
|
510
|
+
toolName: "WriteFile",
|
|
511
|
+
filePath,
|
|
512
|
+
isNew: !existed
|
|
513
|
+
}).catch(() => {
|
|
514
|
+
});
|
|
515
|
+
if (ctx.fileStateCache) {
|
|
516
|
+
let mtime = 0;
|
|
517
|
+
try {
|
|
518
|
+
const stat = await ctx.fs.stat(filePath);
|
|
519
|
+
mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
ctx.fileStateCache.set(filePath, {
|
|
523
|
+
content,
|
|
524
|
+
timestamp: mtime
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
content: existed ? `File updated successfully at: ${filePath}` : `File created successfully at: ${filePath}`
|
|
529
|
+
};
|
|
530
|
+
} catch (err) {
|
|
531
|
+
return {
|
|
532
|
+
content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
|
|
533
|
+
isError: true
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/tools/edit-utils.ts
|
|
540
|
+
var LEFT_SINGLE_CURLY = "\u2018";
|
|
541
|
+
var RIGHT_SINGLE_CURLY = "\u2019";
|
|
542
|
+
var LEFT_DOUBLE_CURLY = "\u201C";
|
|
543
|
+
var RIGHT_DOUBLE_CURLY = "\u201D";
|
|
544
|
+
function normalizeQuotes(str) {
|
|
545
|
+
return str.replaceAll(LEFT_SINGLE_CURLY, "'").replaceAll(RIGHT_SINGLE_CURLY, "'").replaceAll(LEFT_DOUBLE_CURLY, '"').replaceAll(RIGHT_DOUBLE_CURLY, '"');
|
|
546
|
+
}
|
|
547
|
+
function findActualString(fileContent, searchString) {
|
|
548
|
+
if (fileContent.includes(searchString)) {
|
|
549
|
+
return searchString;
|
|
550
|
+
}
|
|
551
|
+
const normalizedSearch = normalizeQuotes(searchString);
|
|
552
|
+
const normalizedFile = normalizeQuotes(fileContent);
|
|
553
|
+
const searchIndex = normalizedFile.indexOf(normalizedSearch);
|
|
554
|
+
if (searchIndex !== -1) {
|
|
555
|
+
return fileContent.substring(searchIndex, searchIndex + searchString.length);
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
function countOccurrences(haystack, needle) {
|
|
560
|
+
const normalizedNeedle = normalizeQuotes(needle);
|
|
561
|
+
const normalizedHaystack = normalizeQuotes(haystack);
|
|
562
|
+
let count = 0;
|
|
563
|
+
let pos = 0;
|
|
564
|
+
while (true) {
|
|
565
|
+
const idx = normalizedHaystack.indexOf(normalizedNeedle, pos);
|
|
566
|
+
if (idx === -1) break;
|
|
567
|
+
count++;
|
|
568
|
+
pos = idx + 1;
|
|
569
|
+
}
|
|
570
|
+
return count;
|
|
571
|
+
}
|
|
572
|
+
function usesCurlyQuotes(str) {
|
|
573
|
+
return {
|
|
574
|
+
singleCurly: str.includes(LEFT_SINGLE_CURLY) || str.includes(RIGHT_SINGLE_CURLY),
|
|
575
|
+
doubleCurly: str.includes(LEFT_DOUBLE_CURLY) || str.includes(RIGHT_DOUBLE_CURLY)
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function preserveQuoteStyle(oldString, actualOldString, newString) {
|
|
579
|
+
if (oldString === actualOldString) {
|
|
580
|
+
return newString;
|
|
581
|
+
}
|
|
582
|
+
const fileStyle = usesCurlyQuotes(actualOldString);
|
|
583
|
+
let result = newString;
|
|
584
|
+
if (fileStyle.singleCurly) {
|
|
585
|
+
result = convertStraightToCurlySingle(result);
|
|
586
|
+
}
|
|
587
|
+
if (fileStyle.doubleCurly) {
|
|
588
|
+
result = convertStraightToCurlyDouble(result);
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
function convertStraightToCurlySingle(str) {
|
|
593
|
+
let result = "";
|
|
594
|
+
let inWord = false;
|
|
595
|
+
for (let i = 0; i < str.length; i++) {
|
|
596
|
+
const ch = str[i];
|
|
597
|
+
if (ch === "'") {
|
|
598
|
+
const prev = i > 0 ? str[i - 1] : " ";
|
|
599
|
+
if (/\s/.test(prev) || prev === "(" || prev === "[" || prev === "{") {
|
|
600
|
+
result += LEFT_SINGLE_CURLY;
|
|
601
|
+
inWord = true;
|
|
602
|
+
} else {
|
|
603
|
+
result += RIGHT_SINGLE_CURLY;
|
|
604
|
+
inWord = false;
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
result += ch;
|
|
608
|
+
inWord = /\w/.test(ch);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
function convertStraightToCurlyDouble(str) {
|
|
614
|
+
let result = "";
|
|
615
|
+
let open = true;
|
|
616
|
+
for (let i = 0; i < str.length; i++) {
|
|
617
|
+
const ch = str[i];
|
|
618
|
+
if (ch === '"') {
|
|
619
|
+
result += open ? LEFT_DOUBLE_CURLY : RIGHT_DOUBLE_CURLY;
|
|
620
|
+
open = !open;
|
|
621
|
+
} else {
|
|
622
|
+
result += ch;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
function stripTrailingWhitespace(str) {
|
|
628
|
+
const parts = str.split(/(\r\n|\n|\r)/);
|
|
629
|
+
const result = [];
|
|
630
|
+
for (let i = 0; i < parts.length; i++) {
|
|
631
|
+
const part = parts[i];
|
|
632
|
+
if (i % 2 === 0) {
|
|
633
|
+
result.push(part.replace(/[\t ]+$/, ""));
|
|
634
|
+
} else {
|
|
635
|
+
result.push(part);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return result.join("");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/tools/prompts/edit.ts
|
|
642
|
+
var EDIT_PROMPT = `Performs exact string replacements in files.
|
|
643
|
+
|
|
644
|
+
Usage:
|
|
645
|
+
- You must use the ReadFile tool at least once before editing a file. This tool will error if you attempt an edit without reading the file first.
|
|
646
|
+
- When editing text from ReadFile output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + pipe. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
647
|
+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
648
|
+
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
649
|
+
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
|
|
650
|
+
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
|
|
651
|
+
`;
|
|
652
|
+
|
|
653
|
+
// src/tools/edit.ts
|
|
654
|
+
var editFileTool = {
|
|
655
|
+
name: "EditFile",
|
|
656
|
+
description: "Edit a file by replacing an exact string match with new content. The old_string must match exactly (including whitespace and indentation). Set replace_all to true to replace all occurrences.",
|
|
657
|
+
prompt: EDIT_PROMPT,
|
|
658
|
+
isReadOnly: false,
|
|
659
|
+
checkPermissions(args) {
|
|
660
|
+
const filePath = args.file_path;
|
|
661
|
+
return {
|
|
662
|
+
behavior: "passthrough",
|
|
663
|
+
message: `Edit ${filePath}`
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
parameters: {
|
|
667
|
+
type: "object",
|
|
668
|
+
properties: {
|
|
669
|
+
file_path: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "The path of the file to edit"
|
|
672
|
+
},
|
|
673
|
+
old_string: {
|
|
674
|
+
type: "string",
|
|
675
|
+
description: "The exact string to find and replace"
|
|
676
|
+
},
|
|
677
|
+
new_string: {
|
|
678
|
+
type: "string",
|
|
679
|
+
description: "The replacement string"
|
|
680
|
+
},
|
|
681
|
+
replace_all: {
|
|
682
|
+
type: "boolean",
|
|
683
|
+
description: "If true, replace all occurrences of old_string. Defaults to false."
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
required: ["file_path", "old_string", "new_string"]
|
|
687
|
+
},
|
|
688
|
+
async call(args, ctx) {
|
|
689
|
+
const filePath = args.file_path;
|
|
690
|
+
const oldString = args.old_string;
|
|
691
|
+
const newString = args.new_string;
|
|
692
|
+
const replaceAll = args.replace_all ?? false;
|
|
693
|
+
if (filePath.endsWith(".ipynb")) {
|
|
694
|
+
return {
|
|
695
|
+
content: `Error: ${filePath} is a Jupyter Notebook. Use the NotebookEdit tool to edit notebook files.`,
|
|
696
|
+
isError: true
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (oldString === newString) {
|
|
700
|
+
return {
|
|
701
|
+
content: "No changes to make: old_string and new_string are exactly the same.",
|
|
702
|
+
isError: true
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
if (ctx.fileStateCache) {
|
|
707
|
+
const cached = ctx.fileStateCache.get(filePath);
|
|
708
|
+
if (!cached || cached.isPartialView) {
|
|
709
|
+
return {
|
|
710
|
+
content: `Error: File has not been read yet. Use ReadFile on ${filePath} before editing.`,
|
|
711
|
+
isError: true
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const stat = await ctx.fs.stat(filePath);
|
|
716
|
+
const mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
|
|
717
|
+
if (mtime > cached.timestamp) {
|
|
718
|
+
const currentContent = await ctx.fs.readFile(filePath);
|
|
719
|
+
if (currentContent !== cached.content) {
|
|
720
|
+
return {
|
|
721
|
+
content: `Error: ${filePath} has been modified since last read. Re-read the file before editing.`,
|
|
722
|
+
isError: true
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (ctx.checkpointManager && ctx.currentMessageId) {
|
|
730
|
+
await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
|
|
731
|
+
}
|
|
732
|
+
const content = await ctx.fs.readFile(filePath);
|
|
733
|
+
const actualOldString = findActualString(content, oldString);
|
|
734
|
+
if (!actualOldString) {
|
|
735
|
+
return {
|
|
736
|
+
content: `Error: old_string not found in ${filePath}. Make sure the string matches exactly, including whitespace and indentation.`,
|
|
737
|
+
isError: true
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (!replaceAll) {
|
|
741
|
+
const count = content.split(actualOldString).length - 1;
|
|
742
|
+
if (count > 1) {
|
|
743
|
+
return {
|
|
744
|
+
content: `Error: old_string appears ${count} times in ${filePath}. Provide more context to make it unique, or set replace_all to true.`,
|
|
745
|
+
isError: true
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const actualNewString = preserveQuoteStyle(oldString, actualOldString, newString);
|
|
750
|
+
let updated;
|
|
751
|
+
if (replaceAll) {
|
|
752
|
+
updated = content.split(actualOldString).join(actualNewString);
|
|
753
|
+
} else if (actualNewString === "") {
|
|
754
|
+
const hasTrailingNewline = !actualOldString.endsWith("\n") && content.includes(actualOldString + "\n");
|
|
755
|
+
const deleteTarget = hasTrailingNewline ? actualOldString + "\n" : actualOldString;
|
|
756
|
+
updated = content.replace(deleteTarget, () => actualNewString);
|
|
757
|
+
} else {
|
|
758
|
+
updated = content.replace(actualOldString, () => actualNewString);
|
|
759
|
+
}
|
|
760
|
+
await ctx.fs.writeFile(filePath, updated);
|
|
761
|
+
ctx.notifyHook?.("FileWrite", {
|
|
762
|
+
event: "FileWrite",
|
|
763
|
+
sessionId: ctx.sessionId ?? "",
|
|
764
|
+
toolName: "EditFile",
|
|
765
|
+
filePath,
|
|
766
|
+
isNew: false
|
|
767
|
+
}).catch(() => {
|
|
768
|
+
});
|
|
769
|
+
if (ctx.fileStateCache) {
|
|
770
|
+
let mtime = 0;
|
|
771
|
+
try {
|
|
772
|
+
const stat = await ctx.fs.stat(filePath);
|
|
773
|
+
mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
ctx.fileStateCache.set(filePath, {
|
|
777
|
+
content: updated,
|
|
778
|
+
timestamp: mtime
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
content: `File ${filePath} has been updated successfully.`
|
|
783
|
+
};
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return {
|
|
786
|
+
content: `Error editing file: ${err instanceof Error ? err.message : String(err)}`,
|
|
787
|
+
isError: true
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/tools/shell-safety/git-safety.ts
|
|
794
|
+
var GIT_INTERNAL_PATTERNS = [
|
|
795
|
+
/\.git\/hooks\//,
|
|
796
|
+
/\.git\/config$/,
|
|
797
|
+
/\.git\/info\//,
|
|
798
|
+
/\.git\/objects\//,
|
|
799
|
+
/\.git\/refs\//,
|
|
800
|
+
/\.git\/HEAD$/,
|
|
801
|
+
/\.git\/index$/,
|
|
802
|
+
/\.git\/packed-refs$/,
|
|
803
|
+
/\.git\/shallow$/,
|
|
804
|
+
/\.git\/modules\//
|
|
805
|
+
];
|
|
806
|
+
function isGitInternalPath(path2) {
|
|
807
|
+
const normalized = path2.replace(/\\/g, "/");
|
|
808
|
+
return GIT_INTERNAL_PATTERNS.some((p) => p.test(normalized));
|
|
809
|
+
}
|
|
810
|
+
var BARE_REPO_MARKERS = ["HEAD", "objects", "refs"];
|
|
811
|
+
function looksLikeBareRepo(dirEntries) {
|
|
812
|
+
const entrySet = new Set(dirEntries.map((e) => e.replace(/\/$/, "")));
|
|
813
|
+
if (entrySet.has(".git")) return false;
|
|
814
|
+
return BARE_REPO_MARKERS.every((m) => entrySet.has(m));
|
|
815
|
+
}
|
|
816
|
+
function commandWritesGitInternals(command) {
|
|
817
|
+
const redirectPattern = /(?:>{1,2}|tee\s+)\s*(\S+)/g;
|
|
818
|
+
let match;
|
|
819
|
+
while ((match = redirectPattern.exec(command)) !== null) {
|
|
820
|
+
if (isGitInternalPath(match[1])) return true;
|
|
821
|
+
}
|
|
822
|
+
const copyPattern = /\b(?:cp|mv|ln)\b.*\s(\S*\.git\/\S+)/;
|
|
823
|
+
const copyMatch = command.match(copyPattern);
|
|
824
|
+
if (copyMatch && isGitInternalPath(copyMatch[1])) return true;
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/tools/shell-safety/command-classification.ts
|
|
829
|
+
var READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
830
|
+
"cat",
|
|
831
|
+
"head",
|
|
832
|
+
"tail",
|
|
833
|
+
"less",
|
|
834
|
+
"more",
|
|
835
|
+
"wc",
|
|
836
|
+
"file",
|
|
837
|
+
"which",
|
|
838
|
+
"whence",
|
|
839
|
+
"where",
|
|
840
|
+
"whereis",
|
|
841
|
+
"type",
|
|
842
|
+
"pwd",
|
|
843
|
+
"date",
|
|
844
|
+
"uname",
|
|
845
|
+
"hostname",
|
|
846
|
+
"whoami",
|
|
847
|
+
"id",
|
|
848
|
+
"groups",
|
|
849
|
+
"ls",
|
|
850
|
+
"ll",
|
|
851
|
+
"la",
|
|
852
|
+
"dir",
|
|
853
|
+
"tree",
|
|
854
|
+
"stat",
|
|
855
|
+
"du",
|
|
856
|
+
"df",
|
|
857
|
+
"free",
|
|
858
|
+
"uptime",
|
|
859
|
+
"ps",
|
|
860
|
+
"top",
|
|
861
|
+
"htop",
|
|
862
|
+
"lsof",
|
|
863
|
+
"ss",
|
|
864
|
+
"netstat",
|
|
865
|
+
"ifconfig",
|
|
866
|
+
"ip",
|
|
867
|
+
"ping",
|
|
868
|
+
"dig",
|
|
869
|
+
"nslookup",
|
|
870
|
+
"host",
|
|
871
|
+
"traceroute",
|
|
872
|
+
"grep",
|
|
873
|
+
"egrep",
|
|
874
|
+
"fgrep",
|
|
875
|
+
"rg",
|
|
876
|
+
"ag",
|
|
877
|
+
"ack",
|
|
878
|
+
"find",
|
|
879
|
+
"fd",
|
|
880
|
+
"fdfind",
|
|
881
|
+
"locate",
|
|
882
|
+
"readlink",
|
|
883
|
+
"realpath",
|
|
884
|
+
"basename",
|
|
885
|
+
"dirname",
|
|
886
|
+
"diff",
|
|
887
|
+
"comm",
|
|
888
|
+
"sort",
|
|
889
|
+
"uniq",
|
|
890
|
+
"cut",
|
|
891
|
+
"tr",
|
|
892
|
+
"awk",
|
|
893
|
+
"sed",
|
|
894
|
+
// sed -i is destructive but caught by destructive patterns
|
|
895
|
+
"jq",
|
|
896
|
+
"yq",
|
|
897
|
+
"xxd",
|
|
898
|
+
"hexdump",
|
|
899
|
+
"od",
|
|
900
|
+
"md5sum",
|
|
901
|
+
"sha256sum",
|
|
902
|
+
"shasum",
|
|
903
|
+
"base64",
|
|
904
|
+
"true",
|
|
905
|
+
"false",
|
|
906
|
+
"test",
|
|
907
|
+
"[",
|
|
908
|
+
"[[",
|
|
909
|
+
"man",
|
|
910
|
+
"help",
|
|
911
|
+
"info",
|
|
912
|
+
"nproc",
|
|
913
|
+
"arch",
|
|
914
|
+
"lscpu",
|
|
915
|
+
"lsb_release",
|
|
916
|
+
"sw_vers",
|
|
917
|
+
"sysctl",
|
|
918
|
+
"getconf",
|
|
919
|
+
"dotnet"
|
|
920
|
+
// dotnet --info, dotnet --list-sdks
|
|
921
|
+
]);
|
|
922
|
+
var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
923
|
+
"status",
|
|
924
|
+
"log",
|
|
925
|
+
"diff",
|
|
926
|
+
"show",
|
|
927
|
+
"blame",
|
|
928
|
+
"shortlog",
|
|
929
|
+
"describe",
|
|
930
|
+
"rev-parse",
|
|
931
|
+
"rev-list",
|
|
932
|
+
"cat-file",
|
|
933
|
+
"ls-files",
|
|
934
|
+
"ls-tree",
|
|
935
|
+
"ls-remote",
|
|
936
|
+
"name-rev",
|
|
937
|
+
"for-each-ref",
|
|
938
|
+
"count-objects",
|
|
939
|
+
"fsck",
|
|
940
|
+
"verify-pack",
|
|
941
|
+
"reflog",
|
|
942
|
+
"stash",
|
|
943
|
+
// "stash list" / "stash show" — stash apply/pop are not here
|
|
944
|
+
"tag",
|
|
945
|
+
// "tag -l" is safe; "tag <name>" creates — caught below
|
|
946
|
+
"branch",
|
|
947
|
+
// "branch --list" is safe; "branch <name>" creates — caught below
|
|
948
|
+
"remote",
|
|
949
|
+
// "remote -v" safe; "remote add/remove" — caught below
|
|
950
|
+
"config",
|
|
951
|
+
// "config --list/--get" safe
|
|
952
|
+
"help",
|
|
953
|
+
"version",
|
|
954
|
+
"--version",
|
|
955
|
+
"--help"
|
|
956
|
+
]);
|
|
957
|
+
var GIT_MUTATING_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
958
|
+
"push",
|
|
959
|
+
"pull",
|
|
960
|
+
"fetch",
|
|
961
|
+
"merge",
|
|
962
|
+
"rebase",
|
|
963
|
+
"cherry-pick",
|
|
964
|
+
"revert",
|
|
965
|
+
"commit",
|
|
966
|
+
"add",
|
|
967
|
+
"rm",
|
|
968
|
+
"mv",
|
|
969
|
+
"init",
|
|
970
|
+
"clone",
|
|
971
|
+
"checkout",
|
|
972
|
+
"switch",
|
|
973
|
+
"restore",
|
|
974
|
+
"reset",
|
|
975
|
+
"clean",
|
|
976
|
+
"bisect",
|
|
977
|
+
"am",
|
|
978
|
+
"apply",
|
|
979
|
+
"format-patch",
|
|
980
|
+
"submodule",
|
|
981
|
+
"worktree"
|
|
982
|
+
]);
|
|
983
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
984
|
+
// rm -rf / rm -r / rm --recursive (but not plain rm single-file)
|
|
985
|
+
/\brm\s+(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/,
|
|
986
|
+
// rm on root-like paths
|
|
987
|
+
/\brm\s+.*\s+\/($|\s)/,
|
|
988
|
+
// git force operations
|
|
989
|
+
/\bgit\s+push\s+.*--force\b/,
|
|
990
|
+
/\bgit\s+push\s+-f\b/,
|
|
991
|
+
/\bgit\s+reset\s+--hard\b/,
|
|
992
|
+
/\bgit\s+clean\s+.*-[a-zA-Z]*f/,
|
|
993
|
+
/\bgit\s+checkout\s+--\s+\./,
|
|
994
|
+
// Filesystem destruction
|
|
995
|
+
/\bchmod\s+(-[a-zA-Z]*R[a-zA-Z]*|--recursive)\s+777\b/,
|
|
996
|
+
/\bchown\s+(-[a-zA-Z]*R[a-zA-Z]*|--recursive)\b/,
|
|
997
|
+
/\bdd\s+/,
|
|
998
|
+
/\bmkfs\b/,
|
|
999
|
+
/\bformat\b/,
|
|
1000
|
+
/\bfdisk\b/,
|
|
1001
|
+
// Dangerous redirects
|
|
1002
|
+
/>\s*\/dev\/sd[a-z]/,
|
|
1003
|
+
// Database destructive operations
|
|
1004
|
+
/\bDROP\s+(TABLE|DATABASE|SCHEMA)\b/i,
|
|
1005
|
+
/\bTRUNCATE\s+TABLE\b/i,
|
|
1006
|
+
/\bDELETE\s+FROM\b/i,
|
|
1007
|
+
// sed in-place
|
|
1008
|
+
/\bsed\s+(-[a-zA-Z]*i[a-zA-Z]*|--in-place)\b/,
|
|
1009
|
+
// Container/system destruction
|
|
1010
|
+
/\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/,
|
|
1011
|
+
/\bkubectl\s+delete\b/,
|
|
1012
|
+
// Kill processes
|
|
1013
|
+
/\bkill\s+-9\b/,
|
|
1014
|
+
/\bkillall\b/,
|
|
1015
|
+
/\bpkill\b/,
|
|
1016
|
+
// Recursive operations on root
|
|
1017
|
+
/\bfind\s+\/\s+.*-delete\b/,
|
|
1018
|
+
/\bfind\s+\/\s+.*-exec\s+rm\b/
|
|
1019
|
+
];
|
|
1020
|
+
var SAFE_ECHO_RE = /^(?:echo|printf)(?:\s+(?:'[^']*'|[^|;&`$(){}><#\\!"'\s]+))*(?:\s+2>&1)?\s*$/;
|
|
1021
|
+
function hasTokenFlag(tokens, ...flags) {
|
|
1022
|
+
return tokens.some((t) => flags.includes(t));
|
|
1023
|
+
}
|
|
1024
|
+
function splitCompoundCommand(command) {
|
|
1025
|
+
return command.split(/\s*(?:;|&&|\|\||(?<!\|)\|(?!\|))\s*/).map((s) => s.trim()).filter(Boolean);
|
|
1026
|
+
}
|
|
1027
|
+
function stripPrefixes(command) {
|
|
1028
|
+
let cmd = command.trim();
|
|
1029
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(cmd)) {
|
|
1030
|
+
cmd = cmd.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
|
|
1031
|
+
}
|
|
1032
|
+
for (const prefix of ["sudo", "env", "nohup", "time", "nice", "ionice", "strace", "ltrace"]) {
|
|
1033
|
+
if (cmd.startsWith(prefix + " ")) {
|
|
1034
|
+
cmd = cmd.slice(prefix.length).trim();
|
|
1035
|
+
while (cmd.startsWith("-")) {
|
|
1036
|
+
const spaceIdx = cmd.indexOf(" ");
|
|
1037
|
+
if (spaceIdx === -1) break;
|
|
1038
|
+
cmd = cmd.slice(spaceIdx).trim();
|
|
1039
|
+
}
|
|
1040
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(cmd)) {
|
|
1041
|
+
cmd = cmd.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return cmd;
|
|
1046
|
+
}
|
|
1047
|
+
function extractCommandName(command) {
|
|
1048
|
+
const cmd = stripPrefixes(command);
|
|
1049
|
+
const firstToken = cmd.split(/\s/)[0] ?? "";
|
|
1050
|
+
const base = firstToken.includes("/") ? firstToken.split("/").pop() : firstToken;
|
|
1051
|
+
return base;
|
|
1052
|
+
}
|
|
1053
|
+
function classifyGitCommand(command) {
|
|
1054
|
+
if (/\bgit\s+--version\b/.test(command)) {
|
|
1055
|
+
return { isReadOnly: true, isDestructive: false, reason: "git --version is read-only" };
|
|
1056
|
+
}
|
|
1057
|
+
if (/\bgit\s+--help\b/.test(command)) {
|
|
1058
|
+
return { isReadOnly: true, isDestructive: false, reason: "git --help is read-only" };
|
|
1059
|
+
}
|
|
1060
|
+
if (/\bgit\s+(-c\s|--exec-path=|--config-env=)/.test(command)) {
|
|
1061
|
+
return { isReadOnly: false, isDestructive: true, reason: "git config injection vector (-c/--exec-path/--config-env)" };
|
|
1062
|
+
}
|
|
1063
|
+
const match = command.match(/\bgit\s+(?:--[a-z-]+=?\S*\s+)*([a-z][a-z-]*)/);
|
|
1064
|
+
if (!match) {
|
|
1065
|
+
return { isReadOnly: false, isDestructive: false, reason: "Cannot parse git subcommand" };
|
|
1066
|
+
}
|
|
1067
|
+
const subcommand = match[1];
|
|
1068
|
+
if (GIT_READ_ONLY_SUBCOMMANDS.has(subcommand)) {
|
|
1069
|
+
const afterSubcmd = command.slice(command.indexOf(subcommand) + subcommand.length).trim();
|
|
1070
|
+
const tokens = afterSubcmd.split(/\s+/).filter(Boolean);
|
|
1071
|
+
const positional = tokens.filter((t) => !t.startsWith("-"));
|
|
1072
|
+
const flags = tokens.filter((t) => t.startsWith("-"));
|
|
1073
|
+
if (subcommand === "branch") {
|
|
1074
|
+
if (hasTokenFlag(flags, "--list", "-l")) {
|
|
1075
|
+
return { isReadOnly: true, isDestructive: false, reason: "git branch --list is read-only" };
|
|
1076
|
+
}
|
|
1077
|
+
if (hasTokenFlag(flags, "-d", "-D", "--delete")) {
|
|
1078
|
+
return { isReadOnly: false, isDestructive: true, reason: "git branch delete" };
|
|
1079
|
+
}
|
|
1080
|
+
if (positional.length > 0) {
|
|
1081
|
+
return { isReadOnly: false, isDestructive: false, reason: "git branch create" };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (subcommand === "tag") {
|
|
1085
|
+
if (hasTokenFlag(flags, "-l", "--list")) {
|
|
1086
|
+
return { isReadOnly: true, isDestructive: false, reason: "git tag --list is read-only" };
|
|
1087
|
+
}
|
|
1088
|
+
if (hasTokenFlag(flags, "-d", "-D", "--delete")) {
|
|
1089
|
+
return { isReadOnly: false, isDestructive: true, reason: "git tag delete" };
|
|
1090
|
+
}
|
|
1091
|
+
if (positional.length > 0) {
|
|
1092
|
+
return { isReadOnly: false, isDestructive: false, reason: "git tag create" };
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (subcommand === "stash") {
|
|
1096
|
+
const stashSubcmd = positional[0];
|
|
1097
|
+
if (stashSubcmd === "list" || stashSubcmd === "show") {
|
|
1098
|
+
return { isReadOnly: true, isDestructive: false, reason: `git stash ${stashSubcmd} is read-only` };
|
|
1099
|
+
}
|
|
1100
|
+
if (stashSubcmd === "drop" || stashSubcmd === "clear") {
|
|
1101
|
+
return { isReadOnly: false, isDestructive: true, reason: "git stash destructive operation" };
|
|
1102
|
+
}
|
|
1103
|
+
return { isReadOnly: false, isDestructive: false, reason: "git stash mutating operation" };
|
|
1104
|
+
}
|
|
1105
|
+
if (subcommand === "config") {
|
|
1106
|
+
if (hasTokenFlag(flags, "--set", "--add", "--unset", "--unset-all", "--replace-all", "--rename-section", "--remove-section")) {
|
|
1107
|
+
return { isReadOnly: false, isDestructive: false, reason: "git config write operation" };
|
|
1108
|
+
}
|
|
1109
|
+
if (positional.length >= 2) {
|
|
1110
|
+
return { isReadOnly: false, isDestructive: false, reason: "git config set key value" };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (subcommand === "remote") {
|
|
1114
|
+
const remoteSubcmd = positional[0];
|
|
1115
|
+
if (remoteSubcmd && ["add", "remove", "rename", "set-url", "set-branches", "prune"].includes(remoteSubcmd)) {
|
|
1116
|
+
return { isReadOnly: false, isDestructive: false, reason: "git remote mutating operation" };
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return { isReadOnly: true, isDestructive: false, reason: `git ${subcommand} is read-only` };
|
|
1120
|
+
}
|
|
1121
|
+
if (GIT_MUTATING_SUBCOMMANDS.has(subcommand)) {
|
|
1122
|
+
for (const pattern of DESTRUCTIVE_PATTERNS) {
|
|
1123
|
+
if (pattern.test(command)) {
|
|
1124
|
+
return { isReadOnly: false, isDestructive: true, reason: `Destructive: ${pattern.source}` };
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return { isReadOnly: false, isDestructive: false, reason: `git ${subcommand} is mutating` };
|
|
1128
|
+
}
|
|
1129
|
+
return { isReadOnly: false, isDestructive: false, reason: `Unknown git subcommand: ${subcommand}` };
|
|
1130
|
+
}
|
|
1131
|
+
function classifySingleCommand(command, config) {
|
|
1132
|
+
const name = extractCommandName(command);
|
|
1133
|
+
if (!name) {
|
|
1134
|
+
return { isReadOnly: false, isDestructive: false, reason: "Empty command" };
|
|
1135
|
+
}
|
|
1136
|
+
const allDestructive = [
|
|
1137
|
+
...DESTRUCTIVE_PATTERNS,
|
|
1138
|
+
...config?.extraDestructivePatterns ?? []
|
|
1139
|
+
];
|
|
1140
|
+
for (const pattern of allDestructive) {
|
|
1141
|
+
if (pattern.test(command)) {
|
|
1142
|
+
return {
|
|
1143
|
+
isReadOnly: false,
|
|
1144
|
+
isDestructive: true,
|
|
1145
|
+
reason: `Matches destructive pattern: ${pattern.source}`
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (name === "git") {
|
|
1150
|
+
return classifyGitCommand(command);
|
|
1151
|
+
}
|
|
1152
|
+
if (name === "xargs" && /\bgit\b/.test(command)) {
|
|
1153
|
+
return classifyGitCommand(command);
|
|
1154
|
+
}
|
|
1155
|
+
if ((name === "echo" || name === "printf") && SAFE_ECHO_RE.test(stripPrefixes(command).trim())) {
|
|
1156
|
+
return { isReadOnly: true, isDestructive: false, reason: `${name} with safe arguments is read-only` };
|
|
1157
|
+
}
|
|
1158
|
+
const extraReadOnly = new Set(config?.extraReadOnlyCommands ?? []);
|
|
1159
|
+
if (READ_ONLY_COMMANDS.has(name) || extraReadOnly.has(name)) {
|
|
1160
|
+
return { isReadOnly: true, isDestructive: false, reason: `${name} is read-only` };
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
isReadOnly: false,
|
|
1164
|
+
isDestructive: false,
|
|
1165
|
+
reason: `${name} is not in the read-only allowlist`
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function classifyCommand(command, config) {
|
|
1169
|
+
if (!command.trim()) {
|
|
1170
|
+
return { isReadOnly: true, isDestructive: false, reason: "Empty command" };
|
|
1171
|
+
}
|
|
1172
|
+
const subCommands = splitCompoundCommand(command);
|
|
1173
|
+
if (subCommands.length === 0) {
|
|
1174
|
+
return { isReadOnly: true, isDestructive: false, reason: "Empty command" };
|
|
1175
|
+
}
|
|
1176
|
+
if (subCommands.length > 1) {
|
|
1177
|
+
const hasCd = subCommands.some((s) => /^(cd|pushd)\s/.test(s.trim()));
|
|
1178
|
+
const hasGit = subCommands.some((s) => {
|
|
1179
|
+
const n = extractCommandName(s);
|
|
1180
|
+
return n === "git" || n === "xargs" && /\bgit\b/.test(s);
|
|
1181
|
+
});
|
|
1182
|
+
if (hasCd && hasGit) {
|
|
1183
|
+
return {
|
|
1184
|
+
isReadOnly: false,
|
|
1185
|
+
isDestructive: false,
|
|
1186
|
+
reason: "cd + git compound may escape working directory (bare-repo risk)"
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
if (hasGit && commandWritesGitInternals(command)) {
|
|
1190
|
+
return {
|
|
1191
|
+
isReadOnly: false,
|
|
1192
|
+
isDestructive: true,
|
|
1193
|
+
reason: "Compound command writes to git internal paths before running git"
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
let allReadOnly = true;
|
|
1198
|
+
let anyDestructive = false;
|
|
1199
|
+
const reasons = [];
|
|
1200
|
+
for (const sub of subCommands) {
|
|
1201
|
+
const result = classifySingleCommand(sub, config);
|
|
1202
|
+
if (!result.isReadOnly) allReadOnly = false;
|
|
1203
|
+
if (result.isDestructive) anyDestructive = true;
|
|
1204
|
+
if (result.reason) reasons.push(result.reason);
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
isReadOnly: allReadOnly,
|
|
1208
|
+
isDestructive: anyDestructive,
|
|
1209
|
+
reason: reasons.join("; ")
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/tools/shell-safety/git-tracking.ts
|
|
1214
|
+
function detectGitOperations(command, stdout) {
|
|
1215
|
+
const events = [];
|
|
1216
|
+
const cmd = command.trim();
|
|
1217
|
+
if (/\bgit\s+commit\b/.test(cmd)) {
|
|
1218
|
+
const shaMatch = stdout.match(/\[[\w/.-]+\s+([0-9a-f]{7,40})\]/);
|
|
1219
|
+
const sha = shaMatch ? shaMatch[1] : "unknown";
|
|
1220
|
+
events.push({ type: "commit", details: `commit ${sha}` });
|
|
1221
|
+
}
|
|
1222
|
+
if (/\bgit\s+merge\b/.test(cmd)) {
|
|
1223
|
+
const branchMatch = cmd.match(/\bgit\s+merge\s+(?:--\S+\s+)*(\S+)/);
|
|
1224
|
+
const branch = branchMatch ? branchMatch[1] : "unknown";
|
|
1225
|
+
events.push({ type: "merge", details: `merge ${branch}` });
|
|
1226
|
+
}
|
|
1227
|
+
if (/\bgit\s+rebase\b/.test(cmd)) {
|
|
1228
|
+
const branchMatch = cmd.match(/\bgit\s+rebase\s+(?:--\S+\s+)*(\S+)/);
|
|
1229
|
+
const branch = branchMatch ? branchMatch[1] : "unknown";
|
|
1230
|
+
events.push({ type: "rebase", details: `rebase onto ${branch}` });
|
|
1231
|
+
}
|
|
1232
|
+
if (/\bgit\s+push\b/.test(cmd)) {
|
|
1233
|
+
const remoteMatch = cmd.match(/\bgit\s+push\s+(?:--\S+\s+)*(\S+)/);
|
|
1234
|
+
const remote = remoteMatch ? remoteMatch[1] : "origin";
|
|
1235
|
+
const branchMatch = stdout.match(/\S+\s+->\s+(\S+)/);
|
|
1236
|
+
const branch = branchMatch ? branchMatch[1] : "";
|
|
1237
|
+
events.push({
|
|
1238
|
+
type: "push",
|
|
1239
|
+
details: `push to ${remote}${branch ? ` (${branch})` : ""}`
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
if (/\b(gh\s+pr\s+create|glab\s+mr\s+create)\b/.test(cmd)) {
|
|
1243
|
+
const urlMatch = stdout.match(/(https?:\/\/\S+(?:pull|merge_requests)\/\d+)/);
|
|
1244
|
+
const url = urlMatch ? urlMatch[1] : "";
|
|
1245
|
+
events.push({
|
|
1246
|
+
type: "pr_create",
|
|
1247
|
+
details: url ? `PR created: ${url}` : "PR created"
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
return events;
|
|
1251
|
+
}
|
|
1252
|
+
function hasGitIndexLockError(output) {
|
|
1253
|
+
return /\.git\/index\.lock/.test(output);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/tools/prompts/bash.ts
|
|
1257
|
+
var BASH_PROMPT = `Executes a given bash command and returns its output.
|
|
1258
|
+
|
|
1259
|
+
The working directory persists between commands, but shell state does not.
|
|
1260
|
+
|
|
1261
|
+
IMPORTANT: Avoid using this tool to run \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool:
|
|
1262
|
+
|
|
1263
|
+
- File search: Use Glob (NOT find or ls)
|
|
1264
|
+
- Content search: Use Grep (NOT grep or rg)
|
|
1265
|
+
- Read files: Use ReadFile (NOT cat/head/tail)
|
|
1266
|
+
- Edit files: Use EditFile (NOT sed/awk)
|
|
1267
|
+
- Write files: Use WriteFile (NOT echo >/cat <<EOF)
|
|
1268
|
+
- Communication: Output text directly (NOT echo/printf)
|
|
1269
|
+
|
|
1270
|
+
While the Bash tool can do similar things, the built-in tools are preferred as they provide better structured output and integrate with the permission system.
|
|
1271
|
+
|
|
1272
|
+
# Instructions
|
|
1273
|
+
|
|
1274
|
+
- If your command will create new directories or files, first use this tool to run \`ls\` to verify the parent directory exists and is the correct location.
|
|
1275
|
+
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt").
|
|
1276
|
+
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
|
|
1277
|
+
- You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 30000ms (0.5 minutes).
|
|
1278
|
+
- When issuing multiple commands:
|
|
1279
|
+
- If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
|
|
1280
|
+
- If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
|
|
1281
|
+
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
|
|
1282
|
+
- DO NOT use newlines to separate commands (newlines are ok in quoted strings).
|
|
1283
|
+
- For git commands:
|
|
1284
|
+
- Prefer to create a new commit rather than amending an existing commit.
|
|
1285
|
+
- Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative. Only use destructive operations when they are truly the best approach.
|
|
1286
|
+
- Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
|
|
1287
|
+
- Avoid unnecessary \`sleep\` commands:
|
|
1288
|
+
- Do not sleep between commands that can run immediately \u2014 just run them.
|
|
1289
|
+
- Do not retry failing commands in a sleep loop \u2014 diagnose the root cause.
|
|
1290
|
+
- If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.
|
|
1291
|
+
`;
|
|
1292
|
+
|
|
1293
|
+
// src/tools/bash.ts
|
|
1294
|
+
var MAX_OUTPUT_CHARS = 1e5;
|
|
1295
|
+
var bashTool = {
|
|
1296
|
+
name: "Bash",
|
|
1297
|
+
description: "Execute a bash shell command. Use this for running scripts, installing packages, git operations, and other system commands.",
|
|
1298
|
+
prompt: BASH_PROMPT,
|
|
1299
|
+
isReadOnly(args) {
|
|
1300
|
+
const command = args.command;
|
|
1301
|
+
return classifyCommand(command).isReadOnly;
|
|
1302
|
+
},
|
|
1303
|
+
isDestructive(args) {
|
|
1304
|
+
const command = args.command;
|
|
1305
|
+
return classifyCommand(command).isDestructive;
|
|
1306
|
+
},
|
|
1307
|
+
checkPermissions(args) {
|
|
1308
|
+
const command = args.command;
|
|
1309
|
+
const classification = classifyCommand(command);
|
|
1310
|
+
if (classification.isDestructive) {
|
|
1311
|
+
return {
|
|
1312
|
+
behavior: "ask",
|
|
1313
|
+
message: `Destructive command: ${command}${classification.reason ? ` (${classification.reason})` : ""}`
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
if (commandWritesGitInternals(command)) {
|
|
1317
|
+
return {
|
|
1318
|
+
behavior: "ask",
|
|
1319
|
+
message: `Command writes to .git/ internals: ${command}`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
behavior: "passthrough",
|
|
1324
|
+
message: `Execute: ${command}`
|
|
1325
|
+
};
|
|
1326
|
+
},
|
|
1327
|
+
parameters: {
|
|
1328
|
+
type: "object",
|
|
1329
|
+
properties: {
|
|
1330
|
+
command: {
|
|
1331
|
+
type: "string",
|
|
1332
|
+
description: "The bash command to execute"
|
|
1333
|
+
},
|
|
1334
|
+
timeout: {
|
|
1335
|
+
type: "number",
|
|
1336
|
+
description: "Timeout in milliseconds (default: 30000)"
|
|
1337
|
+
},
|
|
1338
|
+
description: {
|
|
1339
|
+
type: "string",
|
|
1340
|
+
description: "Short description of what this command does (5-10 words)"
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
required: ["command"]
|
|
1344
|
+
},
|
|
1345
|
+
async call(args, ctx) {
|
|
1346
|
+
const command = args.command;
|
|
1347
|
+
const timeout = args.timeout;
|
|
1348
|
+
try {
|
|
1349
|
+
const result = await ctx.computer.executeCommand(command, {
|
|
1350
|
+
timeout,
|
|
1351
|
+
cwd: ctx.cwd
|
|
1352
|
+
});
|
|
1353
|
+
let output = "";
|
|
1354
|
+
if (result.stdout) {
|
|
1355
|
+
output += result.stdout;
|
|
1356
|
+
}
|
|
1357
|
+
if (result.stderr) {
|
|
1358
|
+
if (output) output += "\n";
|
|
1359
|
+
output += `STDERR:
|
|
1360
|
+
${result.stderr}`;
|
|
1361
|
+
}
|
|
1362
|
+
if (!output.trim()) {
|
|
1363
|
+
output = "(no output)";
|
|
1364
|
+
}
|
|
1365
|
+
if (output.length > MAX_OUTPUT_CHARS) {
|
|
1366
|
+
const totalChars = output.length;
|
|
1367
|
+
output = output.slice(0, MAX_OUTPUT_CHARS) + `
|
|
1368
|
+
... output truncated (${totalChars} total chars)`;
|
|
1369
|
+
}
|
|
1370
|
+
if (result.exitCode !== 0) {
|
|
1371
|
+
output = `Exit code: ${result.exitCode}
|
|
1372
|
+
${output}`;
|
|
1373
|
+
}
|
|
1374
|
+
const toolResult = {
|
|
1375
|
+
content: output,
|
|
1376
|
+
isError: result.exitCode !== 0
|
|
1377
|
+
};
|
|
1378
|
+
if (result.exitCode === 0) {
|
|
1379
|
+
const gitOps = detectGitOperations(command, result.stdout ?? "");
|
|
1380
|
+
if (gitOps.length > 0) {
|
|
1381
|
+
toolResult.metadata = { gitOperations: gitOps };
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return toolResult;
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
return {
|
|
1387
|
+
content: `Error executing command: ${err instanceof Error ? err.message : String(err)}`,
|
|
1388
|
+
isError: true
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// src/tools/prompts/glob.ts
|
|
1395
|
+
var GLOB_PROMPT = `Fast file pattern matching tool that works with any codebase size.
|
|
1396
|
+
|
|
1397
|
+
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
1398
|
+
- Returns matching file paths sorted by modification time
|
|
1399
|
+
- Use this tool when you need to find files by name patterns
|
|
1400
|
+
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
|
|
1401
|
+
`;
|
|
1402
|
+
|
|
1403
|
+
// src/utils/shell-escape.ts
|
|
1404
|
+
function shellEscape(s) {
|
|
1405
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/tools/glob.ts
|
|
1409
|
+
var MAX_RESULTS = 200;
|
|
1410
|
+
var globTool = {
|
|
1411
|
+
name: "Glob",
|
|
1412
|
+
description: "Find files matching a glob pattern. Uses ripgrep (rg --files --glob) for fast, gitignore-aware file discovery. Returns matching file paths sorted by modification time.",
|
|
1413
|
+
prompt: GLOB_PROMPT,
|
|
1414
|
+
isReadOnly: true,
|
|
1415
|
+
isConcurrencySafe: true,
|
|
1416
|
+
parameters: {
|
|
1417
|
+
type: "object",
|
|
1418
|
+
properties: {
|
|
1419
|
+
pattern: {
|
|
1420
|
+
type: "string",
|
|
1421
|
+
description: 'Glob pattern to match files (e.g. "*.ts", "src/**/*.tsx")'
|
|
1422
|
+
},
|
|
1423
|
+
path: {
|
|
1424
|
+
type: "string",
|
|
1425
|
+
description: "Directory to search in (defaults to cwd)"
|
|
1426
|
+
}
|
|
1427
|
+
},
|
|
1428
|
+
required: ["pattern"]
|
|
1429
|
+
},
|
|
1430
|
+
async call(args, ctx) {
|
|
1431
|
+
const pattern = args.pattern;
|
|
1432
|
+
const searchPath = args.path ?? ctx.cwd;
|
|
1433
|
+
const fullPattern = pattern.startsWith("**/") ? pattern : `**/${pattern}`;
|
|
1434
|
+
const command = `rg --files --glob ${shellEscape(fullPattern)} --sort=modified | head -n ${String(MAX_RESULTS + 1)}`;
|
|
1435
|
+
try {
|
|
1436
|
+
const result = await ctx.computer.executeCommand(command, {
|
|
1437
|
+
cwd: searchPath
|
|
1438
|
+
});
|
|
1439
|
+
if (result.exitCode > 1) {
|
|
1440
|
+
return {
|
|
1441
|
+
content: `Glob error: ${result.stderr || result.stdout}`,
|
|
1442
|
+
isError: true
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
const lines = result.stdout.split("\n").filter((l) => l.trim() !== "");
|
|
1446
|
+
if (lines.length === 0) {
|
|
1447
|
+
return { content: "No files found matching the pattern." };
|
|
1448
|
+
}
|
|
1449
|
+
const truncated = lines.length > MAX_RESULTS;
|
|
1450
|
+
const files = truncated ? lines.slice(0, MAX_RESULTS) : lines;
|
|
1451
|
+
let output = files.join("\n");
|
|
1452
|
+
if (truncated) {
|
|
1453
|
+
output += `
|
|
1454
|
+
|
|
1455
|
+
(Results truncated. More than ${MAX_RESULTS} files match.)`;
|
|
1456
|
+
}
|
|
1457
|
+
return { content: output };
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
return {
|
|
1460
|
+
content: `Error searching files: ${err instanceof Error ? err.message : String(err)}`,
|
|
1461
|
+
isError: true
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// src/tools/prompts/grep.ts
|
|
1468
|
+
var GREP_PROMPT = `A powerful search tool built on ripgrep.
|
|
1469
|
+
|
|
1470
|
+
Usage:
|
|
1471
|
+
- ALWAYS use Grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a Bash command. The Grep tool has been optimized for correct permissions and access.
|
|
1472
|
+
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
1473
|
+
- Filter files with the glob parameter (e.g., "*.js", "**/*.tsx")
|
|
1474
|
+
- Returns matching lines with file paths and line numbers
|
|
1475
|
+
- Use the Agent tool for open-ended searches requiring multiple rounds
|
|
1476
|
+
- Pattern syntax: Uses ripgrep (not grep) \u2014 literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
|
|
1477
|
+
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, pass a context_lines parameter.
|
|
1478
|
+
`;
|
|
1479
|
+
|
|
1480
|
+
// src/tools/grep.ts
|
|
1481
|
+
var MAX_MATCHES = 250;
|
|
1482
|
+
var grepTool = {
|
|
1483
|
+
name: "Grep",
|
|
1484
|
+
description: "Search file contents using ripgrep (rg). Supports regex patterns. Returns matching lines with file paths and line numbers.",
|
|
1485
|
+
prompt: GREP_PROMPT,
|
|
1486
|
+
isReadOnly: true,
|
|
1487
|
+
isConcurrencySafe: true,
|
|
1488
|
+
parameters: {
|
|
1489
|
+
type: "object",
|
|
1490
|
+
properties: {
|
|
1491
|
+
pattern: {
|
|
1492
|
+
type: "string",
|
|
1493
|
+
description: "Regular expression pattern to search for"
|
|
1494
|
+
},
|
|
1495
|
+
path: {
|
|
1496
|
+
type: "string",
|
|
1497
|
+
description: "File or directory to search in (defaults to cwd)"
|
|
1498
|
+
},
|
|
1499
|
+
glob: {
|
|
1500
|
+
type: "string",
|
|
1501
|
+
description: 'Glob pattern to filter files (e.g. "*.ts", "*.{js,jsx}")'
|
|
1502
|
+
},
|
|
1503
|
+
case_insensitive: {
|
|
1504
|
+
type: "boolean",
|
|
1505
|
+
description: "Case insensitive search (default: false)"
|
|
1506
|
+
},
|
|
1507
|
+
context_lines: {
|
|
1508
|
+
type: "number",
|
|
1509
|
+
description: "Number of context lines to show before and after each match"
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
required: ["pattern"]
|
|
1513
|
+
},
|
|
1514
|
+
async call(args, ctx) {
|
|
1515
|
+
const pattern = args.pattern;
|
|
1516
|
+
const searchPath = args.path ?? ctx.cwd;
|
|
1517
|
+
const glob = args.glob;
|
|
1518
|
+
const caseInsensitive = args.case_insensitive;
|
|
1519
|
+
const contextLines = args.context_lines;
|
|
1520
|
+
const rgArgs = [
|
|
1521
|
+
"rg",
|
|
1522
|
+
"--line-number",
|
|
1523
|
+
"--no-heading",
|
|
1524
|
+
"--color=never",
|
|
1525
|
+
`--max-count=${MAX_MATCHES}`
|
|
1526
|
+
];
|
|
1527
|
+
if (caseInsensitive) rgArgs.push("-i");
|
|
1528
|
+
if (contextLines !== void 0) rgArgs.push(`-C${contextLines}`);
|
|
1529
|
+
if (glob) rgArgs.push(`--glob`, shellEscape(glob));
|
|
1530
|
+
rgArgs.push("--", shellEscape(pattern), ".");
|
|
1531
|
+
const command = rgArgs.join(" ");
|
|
1532
|
+
try {
|
|
1533
|
+
const result = await ctx.computer.executeCommand(command, {
|
|
1534
|
+
cwd: searchPath
|
|
1535
|
+
});
|
|
1536
|
+
if (result.exitCode === 1 && !result.stdout.trim()) {
|
|
1537
|
+
return { content: "No matches found." };
|
|
1538
|
+
}
|
|
1539
|
+
if (result.exitCode > 1) {
|
|
1540
|
+
return {
|
|
1541
|
+
content: `Grep error: ${result.stderr || result.stdout}`,
|
|
1542
|
+
isError: true
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
const lines = result.stdout.split("\n");
|
|
1546
|
+
let output = result.stdout;
|
|
1547
|
+
if (lines.length > MAX_MATCHES) {
|
|
1548
|
+
output = lines.slice(0, MAX_MATCHES).join("\n") + `
|
|
1549
|
+
|
|
1550
|
+
(Results truncated at ${MAX_MATCHES} matches.)`;
|
|
1551
|
+
}
|
|
1552
|
+
return { content: output || "No matches found." };
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
return {
|
|
1555
|
+
content: `Error searching: ${err instanceof Error ? err.message : String(err)}`,
|
|
1556
|
+
isError: true
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
// src/tools/prompts/web-fetch.ts
|
|
1563
|
+
var WEB_FETCH_PROMPT = `Fetches content from a specified URL and returns it in a readable format.
|
|
1564
|
+
|
|
1565
|
+
- Takes a URL and fetches the page content, converting HTML to markdown
|
|
1566
|
+
- Returns the processed content for analysis
|
|
1567
|
+
- Use this tool when you need to retrieve and analyze web content
|
|
1568
|
+
|
|
1569
|
+
Usage notes:
|
|
1570
|
+
- The URL must be a fully-formed valid URL
|
|
1571
|
+
- HTTP URLs will be automatically upgraded to HTTPS
|
|
1572
|
+
- This tool is read-only and does not modify any files
|
|
1573
|
+
- Results may be summarized if the content is very large
|
|
1574
|
+
- When a URL redirects to a different host, the tool will inform you and provide the redirect URL. You should then make a new WebFetch request with the redirect URL.
|
|
1575
|
+
- For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).
|
|
1576
|
+
`;
|
|
1577
|
+
|
|
1578
|
+
// src/tools/web-fetch.ts
|
|
1579
|
+
var MAX_CONTENT_LENGTH = 5 * 1024 * 1024;
|
|
1580
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
1581
|
+
var MAX_OUTPUT_CHARS2 = 1e5;
|
|
1582
|
+
var webFetchTool = {
|
|
1583
|
+
name: "WebFetch",
|
|
1584
|
+
description: "Fetch a URL and return its contents as markdown. Useful for reading web pages, documentation, API responses, and other online content. Provide an optional prompt to extract specific information.",
|
|
1585
|
+
prompt: WEB_FETCH_PROMPT,
|
|
1586
|
+
isReadOnly: true,
|
|
1587
|
+
isConcurrencySafe: true,
|
|
1588
|
+
parameters: {
|
|
1589
|
+
type: "object",
|
|
1590
|
+
properties: {
|
|
1591
|
+
url: {
|
|
1592
|
+
type: "string",
|
|
1593
|
+
description: "The URL to fetch (must be a valid http/https URL)"
|
|
1594
|
+
},
|
|
1595
|
+
prompt: {
|
|
1596
|
+
type: "string",
|
|
1597
|
+
description: "Optional instruction for what to extract from the page content"
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
required: ["url"]
|
|
1601
|
+
},
|
|
1602
|
+
async call(args, _ctx) {
|
|
1603
|
+
const url = args.url;
|
|
1604
|
+
const prompt = args.prompt;
|
|
1605
|
+
try {
|
|
1606
|
+
new URL(url);
|
|
1607
|
+
} catch {
|
|
1608
|
+
return { content: `Invalid URL: ${url}`, isError: true };
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
const controller = new AbortController();
|
|
1612
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
1613
|
+
const response = await fetch(url, {
|
|
1614
|
+
signal: controller.signal,
|
|
1615
|
+
headers: {
|
|
1616
|
+
"User-Agent": "noumen-agent/1.0",
|
|
1617
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7"
|
|
1618
|
+
},
|
|
1619
|
+
redirect: "follow"
|
|
1620
|
+
});
|
|
1621
|
+
clearTimeout(timeoutId);
|
|
1622
|
+
if (!response.ok) {
|
|
1623
|
+
return {
|
|
1624
|
+
content: `HTTP ${response.status}: ${response.statusText}`,
|
|
1625
|
+
isError: true
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1629
|
+
const contentLength = parseInt(
|
|
1630
|
+
response.headers.get("content-length") ?? "0",
|
|
1631
|
+
10
|
|
1632
|
+
);
|
|
1633
|
+
if (contentLength > MAX_CONTENT_LENGTH) {
|
|
1634
|
+
return {
|
|
1635
|
+
content: `Response too large (${contentLength} bytes, limit ${MAX_CONTENT_LENGTH})`,
|
|
1636
|
+
isError: true
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
let text = "";
|
|
1640
|
+
let bytesRead = 0;
|
|
1641
|
+
const reader = response.body?.getReader();
|
|
1642
|
+
const decoder = new TextDecoder();
|
|
1643
|
+
if (reader) {
|
|
1644
|
+
while (true) {
|
|
1645
|
+
const { done, value } = await reader.read();
|
|
1646
|
+
if (done) break;
|
|
1647
|
+
bytesRead += value.byteLength;
|
|
1648
|
+
if (bytesRead > MAX_CONTENT_LENGTH) {
|
|
1649
|
+
reader.cancel();
|
|
1650
|
+
return {
|
|
1651
|
+
content: `Response too large (>${MAX_CONTENT_LENGTH} bytes streamed, limit ${MAX_CONTENT_LENGTH})`,
|
|
1652
|
+
isError: true
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
text += decoder.decode(value, { stream: true });
|
|
1656
|
+
}
|
|
1657
|
+
text += decoder.decode();
|
|
1658
|
+
} else {
|
|
1659
|
+
text = await response.text();
|
|
1660
|
+
}
|
|
1661
|
+
let markdown;
|
|
1662
|
+
if (contentType.includes("text/html") || contentType.includes("xhtml")) {
|
|
1663
|
+
const { NodeHtmlMarkdown } = await import("node-html-markdown");
|
|
1664
|
+
markdown = NodeHtmlMarkdown.translate(text);
|
|
1665
|
+
} else {
|
|
1666
|
+
markdown = text;
|
|
1667
|
+
}
|
|
1668
|
+
if (markdown.length > MAX_OUTPUT_CHARS2) {
|
|
1669
|
+
const totalChars = markdown.length;
|
|
1670
|
+
markdown = markdown.slice(0, MAX_OUTPUT_CHARS2) + `
|
|
1671
|
+
|
|
1672
|
+
... content truncated (${totalChars} total chars)`;
|
|
1673
|
+
}
|
|
1674
|
+
let result = `# Content from ${url}
|
|
1675
|
+
|
|
1676
|
+
${markdown}`;
|
|
1677
|
+
if (prompt) {
|
|
1678
|
+
result = `## Extraction prompt: ${prompt}
|
|
1679
|
+
|
|
1680
|
+
${result}`;
|
|
1681
|
+
}
|
|
1682
|
+
return { content: result };
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1685
|
+
return { content: `Fetch timed out after ${FETCH_TIMEOUT_MS}ms`, isError: true };
|
|
1686
|
+
}
|
|
1687
|
+
return {
|
|
1688
|
+
content: `Fetch error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1689
|
+
isError: true
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
// src/tools/prompts/notebook.ts
|
|
1696
|
+
var NOTEBOOK_PROMPT = `Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.`;
|
|
1697
|
+
|
|
1698
|
+
// src/tools/notebook.ts
|
|
1699
|
+
var notebookEditTool = {
|
|
1700
|
+
name: "NotebookEdit",
|
|
1701
|
+
description: "Edit a Jupyter notebook (.ipynb) file. Can replace, insert, or delete cells. The notebook is pure JSON \u2014 no kernel execution.",
|
|
1702
|
+
prompt: NOTEBOOK_PROMPT,
|
|
1703
|
+
isReadOnly: false,
|
|
1704
|
+
isConcurrencySafe: false,
|
|
1705
|
+
parameters: {
|
|
1706
|
+
type: "object",
|
|
1707
|
+
properties: {
|
|
1708
|
+
notebook_path: {
|
|
1709
|
+
type: "string",
|
|
1710
|
+
description: "Path to the .ipynb file"
|
|
1711
|
+
},
|
|
1712
|
+
cell_index: {
|
|
1713
|
+
type: "number",
|
|
1714
|
+
description: "0-based index of the cell to edit. For insert, the new cell is placed at this index."
|
|
1715
|
+
},
|
|
1716
|
+
new_source: {
|
|
1717
|
+
type: "string",
|
|
1718
|
+
description: "The new cell source content. Each line becomes an element in the source array."
|
|
1719
|
+
},
|
|
1720
|
+
cell_type: {
|
|
1721
|
+
type: "string",
|
|
1722
|
+
description: 'Cell type: "code" or "markdown" (default: "code")'
|
|
1723
|
+
},
|
|
1724
|
+
edit_mode: {
|
|
1725
|
+
type: "string",
|
|
1726
|
+
description: '"replace" (default) \u2014 replace existing cell source; "insert" \u2014 insert a new cell at cell_index; "delete" \u2014 delete the cell at cell_index (new_source is ignored)'
|
|
1727
|
+
}
|
|
1728
|
+
},
|
|
1729
|
+
required: ["notebook_path", "cell_index"]
|
|
1730
|
+
},
|
|
1731
|
+
async call(args, ctx) {
|
|
1732
|
+
const path2 = args.notebook_path;
|
|
1733
|
+
const cellIndex = args.cell_index;
|
|
1734
|
+
const newSource = args.new_source ?? "";
|
|
1735
|
+
const cellType = args.cell_type ?? "code";
|
|
1736
|
+
const editMode = args.edit_mode ?? "replace";
|
|
1737
|
+
try {
|
|
1738
|
+
const raw = await ctx.fs.readFile(path2);
|
|
1739
|
+
let notebook;
|
|
1740
|
+
try {
|
|
1741
|
+
notebook = JSON.parse(raw);
|
|
1742
|
+
} catch {
|
|
1743
|
+
return { content: `Not a valid JSON notebook: ${path2}`, isError: true };
|
|
1744
|
+
}
|
|
1745
|
+
if (!Array.isArray(notebook.cells)) {
|
|
1746
|
+
return { content: "Notebook has no cells array.", isError: true };
|
|
1747
|
+
}
|
|
1748
|
+
const sourceLines = newSource.split("\n").map(
|
|
1749
|
+
(line, i, arr) => i < arr.length - 1 ? line + "\n" : line
|
|
1750
|
+
);
|
|
1751
|
+
switch (editMode) {
|
|
1752
|
+
case "replace": {
|
|
1753
|
+
if (cellIndex < 0 || cellIndex >= notebook.cells.length) {
|
|
1754
|
+
return {
|
|
1755
|
+
content: `Cell index ${cellIndex} out of range (0-${notebook.cells.length - 1}).`,
|
|
1756
|
+
isError: true
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
notebook.cells[cellIndex].source = sourceLines;
|
|
1760
|
+
notebook.cells[cellIndex].cell_type = cellType;
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
case "insert": {
|
|
1764
|
+
if (cellIndex < 0 || cellIndex > notebook.cells.length) {
|
|
1765
|
+
return {
|
|
1766
|
+
content: `Insert index ${cellIndex} out of range (0-${notebook.cells.length}).`,
|
|
1767
|
+
isError: true
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
const newCell = {
|
|
1771
|
+
cell_type: cellType,
|
|
1772
|
+
source: sourceLines,
|
|
1773
|
+
metadata: {},
|
|
1774
|
+
...cellType === "code" ? { outputs: [], execution_count: null } : {}
|
|
1775
|
+
};
|
|
1776
|
+
notebook.cells.splice(cellIndex, 0, newCell);
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
case "delete": {
|
|
1780
|
+
if (cellIndex < 0 || cellIndex >= notebook.cells.length) {
|
|
1781
|
+
return {
|
|
1782
|
+
content: `Cell index ${cellIndex} out of range (0-${notebook.cells.length - 1}).`,
|
|
1783
|
+
isError: true
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
notebook.cells.splice(cellIndex, 1);
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
default:
|
|
1790
|
+
return {
|
|
1791
|
+
content: `Unknown edit_mode: ${editMode}. Use "replace", "insert", or "delete".`,
|
|
1792
|
+
isError: true
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
await ctx.fs.writeFile(path2, JSON.stringify(notebook, null, 1) + "\n");
|
|
1796
|
+
const action = editMode === "delete" ? `Deleted cell ${cellIndex}` : editMode === "insert" ? `Inserted new ${cellType} cell at index ${cellIndex}` : `Replaced cell ${cellIndex} content`;
|
|
1797
|
+
return { content: `${action} in ${path2}. Notebook now has ${notebook.cells.length} cells.` };
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
return {
|
|
1800
|
+
content: `Error editing notebook: ${err instanceof Error ? err.message : String(err)}`,
|
|
1801
|
+
isError: true
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
// src/tools/ask-user.ts
|
|
1808
|
+
var askUserTool = {
|
|
1809
|
+
name: "AskUser",
|
|
1810
|
+
description: "Ask the user a question and wait for their response. Use when you need clarification, confirmation, or additional information before proceeding.",
|
|
1811
|
+
isReadOnly: true,
|
|
1812
|
+
isConcurrencySafe: false,
|
|
1813
|
+
requiresUserInteraction: true,
|
|
1814
|
+
parameters: {
|
|
1815
|
+
type: "object",
|
|
1816
|
+
properties: {
|
|
1817
|
+
question: {
|
|
1818
|
+
type: "string",
|
|
1819
|
+
description: "The question to ask the user"
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
required: ["question"]
|
|
1823
|
+
},
|
|
1824
|
+
async call(args, ctx) {
|
|
1825
|
+
const question = args.question;
|
|
1826
|
+
if (!ctx.userInputHandler) {
|
|
1827
|
+
return {
|
|
1828
|
+
content: "Cannot ask user: no userInputHandler configured. Set userInputHandler in AgentOptions or ThreadConfig.",
|
|
1829
|
+
isError: true
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
const answer = await ctx.userInputHandler(question);
|
|
1834
|
+
return { content: answer };
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
return {
|
|
1837
|
+
content: `Error getting user input: ${err instanceof Error ? err.message : String(err)}`,
|
|
1838
|
+
isError: true
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
|
|
1844
|
+
// src/tools/registry.ts
|
|
1845
|
+
function resolveToolPrompt(tool) {
|
|
1846
|
+
if (tool.prompt === void 0) return tool.description;
|
|
1847
|
+
return typeof tool.prompt === "function" ? tool.prompt() : tool.prompt;
|
|
1848
|
+
}
|
|
1849
|
+
function resolveToolFlag(flag, args, defaultValue = false) {
|
|
1850
|
+
if (flag === void 0) return defaultValue;
|
|
1851
|
+
if (typeof flag === "function") {
|
|
1852
|
+
try {
|
|
1853
|
+
return flag(args);
|
|
1854
|
+
} catch {
|
|
1855
|
+
return defaultValue;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return flag;
|
|
1859
|
+
}
|
|
1860
|
+
var ToolRegistry = class {
|
|
1861
|
+
tools = /* @__PURE__ */ new Map();
|
|
1862
|
+
_discoveredTools = /* @__PURE__ */ new Set();
|
|
1863
|
+
_toolSearchEnabled = false;
|
|
1864
|
+
constructor(additionalTools) {
|
|
1865
|
+
const builtIn = [
|
|
1866
|
+
readFileTool,
|
|
1867
|
+
writeFileTool,
|
|
1868
|
+
editFileTool,
|
|
1869
|
+
bashTool,
|
|
1870
|
+
globTool,
|
|
1871
|
+
grepTool,
|
|
1872
|
+
webFetchTool,
|
|
1873
|
+
notebookEditTool,
|
|
1874
|
+
askUserTool
|
|
1875
|
+
];
|
|
1876
|
+
for (const tool of builtIn) {
|
|
1877
|
+
this.tools.set(tool.name, tool);
|
|
1878
|
+
}
|
|
1879
|
+
if (additionalTools) {
|
|
1880
|
+
for (const tool of additionalTools) {
|
|
1881
|
+
this.tools.set(tool.name, tool);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
enableToolSearch() {
|
|
1886
|
+
this._toolSearchEnabled = true;
|
|
1887
|
+
}
|
|
1888
|
+
register(tool) {
|
|
1889
|
+
this.tools.set(tool.name, tool);
|
|
1890
|
+
}
|
|
1891
|
+
get(name) {
|
|
1892
|
+
return this.tools.get(name);
|
|
1893
|
+
}
|
|
1894
|
+
async execute(name, args, ctx) {
|
|
1895
|
+
const tool = this.tools.get(name);
|
|
1896
|
+
if (!tool) {
|
|
1897
|
+
return {
|
|
1898
|
+
content: `Unknown tool: ${name}`,
|
|
1899
|
+
isError: true
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
if (tool.inputSchema) {
|
|
1903
|
+
const parsed = tool.inputSchema.safeParse(args);
|
|
1904
|
+
if (!parsed.success) {
|
|
1905
|
+
return {
|
|
1906
|
+
content: formatZodValidationError(name, parsed.error),
|
|
1907
|
+
isError: true
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return tool.call(args, ctx);
|
|
1912
|
+
}
|
|
1913
|
+
toToolDefinitions() {
|
|
1914
|
+
return Array.from(this.tools.values()).map((tool) => ({
|
|
1915
|
+
type: "function",
|
|
1916
|
+
function: {
|
|
1917
|
+
name: tool.name,
|
|
1918
|
+
description: resolveToolPrompt(tool),
|
|
1919
|
+
parameters: tool.parameters
|
|
1920
|
+
}
|
|
1921
|
+
}));
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Get tool definitions filtered by tool search. Eager tools (always sent)
|
|
1925
|
+
* plus any deferred tools the model has discovered via ToolSearch.
|
|
1926
|
+
* Falls back to all tools when tool search is not enabled.
|
|
1927
|
+
*/
|
|
1928
|
+
getActiveToolDefinitions() {
|
|
1929
|
+
if (!this._toolSearchEnabled) return this.toToolDefinitions();
|
|
1930
|
+
return Array.from(this.tools.values()).filter((tool) => !isDeferredTool(tool) || this._discoveredTools.has(tool.name)).map((tool) => ({
|
|
1931
|
+
type: "function",
|
|
1932
|
+
function: {
|
|
1933
|
+
name: tool.name,
|
|
1934
|
+
description: resolveToolPrompt(tool),
|
|
1935
|
+
parameters: tool.parameters
|
|
1936
|
+
}
|
|
1937
|
+
}));
|
|
1938
|
+
}
|
|
1939
|
+
getEagerTools() {
|
|
1940
|
+
return Array.from(this.tools.values()).filter((tool) => !isDeferredTool(tool));
|
|
1941
|
+
}
|
|
1942
|
+
getDeferredTools() {
|
|
1943
|
+
return Array.from(this.tools.values()).filter(isDeferredTool);
|
|
1944
|
+
}
|
|
1945
|
+
getToolsByNames(names) {
|
|
1946
|
+
return names.map((name) => this.tools.get(name)).filter((t) => t !== void 0);
|
|
1947
|
+
}
|
|
1948
|
+
markDiscovered(names) {
|
|
1949
|
+
for (const name of names) {
|
|
1950
|
+
this._discoveredTools.add(name);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
get discoveredTools() {
|
|
1954
|
+
return this._discoveredTools;
|
|
1955
|
+
}
|
|
1956
|
+
listTools() {
|
|
1957
|
+
return Array.from(this.tools.values());
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
export {
|
|
1962
|
+
TOOL_SEARCH_NAME,
|
|
1963
|
+
isDeferredTool,
|
|
1964
|
+
formatDeferredToolLine,
|
|
1965
|
+
searchToolsWithKeywords,
|
|
1966
|
+
createToolSearchTool,
|
|
1967
|
+
zodToJsonSchema,
|
|
1968
|
+
registerZodToJsonSchema,
|
|
1969
|
+
formatZodValidationError,
|
|
1970
|
+
readFileTool,
|
|
1971
|
+
writeFileTool,
|
|
1972
|
+
normalizeQuotes,
|
|
1973
|
+
findActualString,
|
|
1974
|
+
countOccurrences,
|
|
1975
|
+
preserveQuoteStyle,
|
|
1976
|
+
stripTrailingWhitespace,
|
|
1977
|
+
editFileTool,
|
|
1978
|
+
isGitInternalPath,
|
|
1979
|
+
looksLikeBareRepo,
|
|
1980
|
+
commandWritesGitInternals,
|
|
1981
|
+
extractCommandName,
|
|
1982
|
+
classifyCommand,
|
|
1983
|
+
detectGitOperations,
|
|
1984
|
+
hasGitIndexLockError,
|
|
1985
|
+
bashTool,
|
|
1986
|
+
globTool,
|
|
1987
|
+
grepTool,
|
|
1988
|
+
webFetchTool,
|
|
1989
|
+
notebookEditTool,
|
|
1990
|
+
askUserTool,
|
|
1991
|
+
resolveToolFlag,
|
|
1992
|
+
ToolRegistry
|
|
1993
|
+
};
|
|
1994
|
+
//# sourceMappingURL=chunk-QTJ7VTJY.js.map
|