aemeathcli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +607 -0
- package/dist/App-P4MYD4QY.js +2719 -0
- package/dist/App-P4MYD4QY.js.map +1 -0
- package/dist/api-key-fallback-YQQBOQIL.js +11 -0
- package/dist/api-key-fallback-YQQBOQIL.js.map +1 -0
- package/dist/chunk-4IJD72YB.js +184 -0
- package/dist/chunk-4IJD72YB.js.map +1 -0
- package/dist/chunk-6PDJ45T4.js +325 -0
- package/dist/chunk-6PDJ45T4.js.map +1 -0
- package/dist/chunk-CARHU3DO.js +562 -0
- package/dist/chunk-CARHU3DO.js.map +1 -0
- package/dist/chunk-CGEV3ARR.js +80 -0
- package/dist/chunk-CGEV3ARR.js.map +1 -0
- package/dist/chunk-CS5X3BWX.js +27 -0
- package/dist/chunk-CS5X3BWX.js.map +1 -0
- package/dist/chunk-CYQNBB25.js +44 -0
- package/dist/chunk-CYQNBB25.js.map +1 -0
- package/dist/chunk-DAHGLHNR.js +657 -0
- package/dist/chunk-DAHGLHNR.js.map +1 -0
- package/dist/chunk-H66O5Z2V.js +305 -0
- package/dist/chunk-H66O5Z2V.js.map +1 -0
- package/dist/chunk-HCIHOHLX.js +322 -0
- package/dist/chunk-HCIHOHLX.js.map +1 -0
- package/dist/chunk-HMJRPNPZ.js +1031 -0
- package/dist/chunk-HMJRPNPZ.js.map +1 -0
- package/dist/chunk-I5PZ4JTS.js +119 -0
- package/dist/chunk-I5PZ4JTS.js.map +1 -0
- package/dist/chunk-IYW62KKR.js +255 -0
- package/dist/chunk-IYW62KKR.js.map +1 -0
- package/dist/chunk-JAXXTYID.js +51 -0
- package/dist/chunk-JAXXTYID.js.map +1 -0
- package/dist/chunk-LSOYPSAT.js +183 -0
- package/dist/chunk-LSOYPSAT.js.map +1 -0
- package/dist/chunk-MFBHNWGV.js +416 -0
- package/dist/chunk-MFBHNWGV.js.map +1 -0
- package/dist/chunk-MXZSI3AY.js +311 -0
- package/dist/chunk-MXZSI3AY.js.map +1 -0
- package/dist/chunk-NBR3GHMT.js +72 -0
- package/dist/chunk-NBR3GHMT.js.map +1 -0
- package/dist/chunk-O3ZF22SW.js +246 -0
- package/dist/chunk-O3ZF22SW.js.map +1 -0
- package/dist/chunk-SUSJPZU2.js +181 -0
- package/dist/chunk-SUSJPZU2.js.map +1 -0
- package/dist/chunk-TEVZS4FA.js +310 -0
- package/dist/chunk-TEVZS4FA.js.map +1 -0
- package/dist/chunk-UY2SYSEZ.js +211 -0
- package/dist/chunk-UY2SYSEZ.js.map +1 -0
- package/dist/chunk-WAHVZH7V.js +260 -0
- package/dist/chunk-WAHVZH7V.js.map +1 -0
- package/dist/chunk-WPP3PEDE.js +234 -0
- package/dist/chunk-WPP3PEDE.js.map +1 -0
- package/dist/chunk-Y5XVD2CD.js +1610 -0
- package/dist/chunk-Y5XVD2CD.js.map +1 -0
- package/dist/chunk-YL5XFHR3.js +56 -0
- package/dist/chunk-YL5XFHR3.js.map +1 -0
- package/dist/chunk-ZGOHARPV.js +122 -0
- package/dist/chunk-ZGOHARPV.js.map +1 -0
- package/dist/claude-adapter-QMLFMSP3.js +6 -0
- package/dist/claude-adapter-QMLFMSP3.js.map +1 -0
- package/dist/claude-login-5WELXPKT.js +324 -0
- package/dist/claude-login-5WELXPKT.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +703 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-login-7HHLJHBF.js +164 -0
- package/dist/codex-login-7HHLJHBF.js.map +1 -0
- package/dist/config-store-W6FBCQAQ.js +6 -0
- package/dist/config-store-W6FBCQAQ.js.map +1 -0
- package/dist/executor-6RIKIGXK.js +4 -0
- package/dist/executor-6RIKIGXK.js.map +1 -0
- package/dist/gemini-adapter-6JIHZ7WI.js +6 -0
- package/dist/gemini-adapter-6JIHZ7WI.js.map +1 -0
- package/dist/gemini-login-ZZLYC3J6.js +346 -0
- package/dist/gemini-login-ZZLYC3J6.js.map +1 -0
- package/dist/index.d.ts +2210 -0
- package/dist/index.js +1419 -0
- package/dist/index.js.map +1 -0
- package/dist/kimi-adapter-JN4HFFHU.js +6 -0
- package/dist/kimi-adapter-JN4HFFHU.js.map +1 -0
- package/dist/kimi-login-CZPS63NK.js +149 -0
- package/dist/kimi-login-CZPS63NK.js.map +1 -0
- package/dist/native-cli-adapters-OLW3XX57.js +6 -0
- package/dist/native-cli-adapters-OLW3XX57.js.map +1 -0
- package/dist/ollama-adapter-OJQ3FKWK.js +6 -0
- package/dist/ollama-adapter-OJQ3FKWK.js.map +1 -0
- package/dist/openai-adapter-XU46EN7B.js +6 -0
- package/dist/openai-adapter-XU46EN7B.js.map +1 -0
- package/dist/registry-4KD24ZC3.js +6 -0
- package/dist/registry-4KD24ZC3.js.map +1 -0
- package/dist/registry-H7B3AHPQ.js +5 -0
- package/dist/registry-H7B3AHPQ.js.map +1 -0
- package/dist/server-manager-PTGBHCLS.js +5 -0
- package/dist/server-manager-PTGBHCLS.js.map +1 -0
- package/dist/session-manager-ECEEACGY.js +12 -0
- package/dist/session-manager-ECEEACGY.js.map +1 -0
- package/dist/team-manager-HC4XGCFY.js +11 -0
- package/dist/team-manager-HC4XGCFY.js.map +1 -0
- package/dist/tmux-manager-GPYZ3WQH.js +6 -0
- package/dist/tmux-manager-GPYZ3WQH.js.map +1 -0
- package/dist/tools-TSMXMHIF.js +6 -0
- package/dist/tools-TSMXMHIF.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1610 @@
|
|
|
1
|
+
import { validatePath, isCommandBlocked, redactSecrets } from './chunk-CS5X3BWX.js';
|
|
2
|
+
import { FileNotFoundError, ExecutionTimeoutError } from './chunk-ZGOHARPV.js';
|
|
3
|
+
import { logger } from './chunk-JAXXTYID.js';
|
|
4
|
+
import { stat, readFile, mkdir, writeFile } from 'fs/promises';
|
|
5
|
+
import { dirname, resolve, extname } from 'path';
|
|
6
|
+
import fg from 'fast-glob';
|
|
7
|
+
import { execFile } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import { execaCommand } from 'execa';
|
|
10
|
+
|
|
11
|
+
// src/tools/registry.ts
|
|
12
|
+
function redactToolArgs(args) {
|
|
13
|
+
const redacted = {};
|
|
14
|
+
for (const [key, value] of Object.entries(args)) {
|
|
15
|
+
if (typeof value === "string" && (key === "content" || key === "command" || key === "new_source")) {
|
|
16
|
+
redacted[key] = redactSecrets(value.length > 200 ? value.slice(0, 200) + "..." : value);
|
|
17
|
+
} else if (typeof value === "string") {
|
|
18
|
+
redacted[key] = redactSecrets(value);
|
|
19
|
+
} else {
|
|
20
|
+
redacted[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return redacted;
|
|
24
|
+
}
|
|
25
|
+
var ToolRegistry = class {
|
|
26
|
+
tools = /* @__PURE__ */ new Map();
|
|
27
|
+
categoryIndex = /* @__PURE__ */ new Map();
|
|
28
|
+
register(tool) {
|
|
29
|
+
const name = tool.definition.name;
|
|
30
|
+
if (this.tools.has(name)) {
|
|
31
|
+
logger.warn({ toolName: name }, "Overwriting existing tool registration");
|
|
32
|
+
}
|
|
33
|
+
this.tools.set(name, tool);
|
|
34
|
+
let categorySet = this.categoryIndex.get(tool.category);
|
|
35
|
+
if (!categorySet) {
|
|
36
|
+
categorySet = /* @__PURE__ */ new Set();
|
|
37
|
+
this.categoryIndex.set(tool.category, categorySet);
|
|
38
|
+
}
|
|
39
|
+
categorySet.add(name);
|
|
40
|
+
logger.debug({ toolName: name, category: tool.category }, "Tool registered");
|
|
41
|
+
}
|
|
42
|
+
get(name) {
|
|
43
|
+
return this.tools.get(name);
|
|
44
|
+
}
|
|
45
|
+
getAll() {
|
|
46
|
+
return [...this.tools.values()];
|
|
47
|
+
}
|
|
48
|
+
getDefinitions() {
|
|
49
|
+
return [...this.tools.values()].map((t) => t.definition);
|
|
50
|
+
}
|
|
51
|
+
getByCategory(category) {
|
|
52
|
+
const names = this.categoryIndex.get(category);
|
|
53
|
+
if (!names) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const results = [];
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const tool = this.tools.get(name);
|
|
59
|
+
if (tool) {
|
|
60
|
+
results.push(tool);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
async execute(call, context) {
|
|
66
|
+
const tool = this.tools.get(call.name);
|
|
67
|
+
if (!tool) {
|
|
68
|
+
return {
|
|
69
|
+
toolCallId: call.id,
|
|
70
|
+
name: call.name,
|
|
71
|
+
content: `Unknown tool: ${call.name}`,
|
|
72
|
+
isError: true
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (tool.requiresApproval(context.permissionMode, call.arguments)) {
|
|
76
|
+
return {
|
|
77
|
+
toolCallId: call.id,
|
|
78
|
+
name: call.name,
|
|
79
|
+
content: `Tool "${call.name}" requires user approval in ${context.permissionMode} mode.`,
|
|
80
|
+
isError: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
logger.debug({ toolName: call.name, args: redactToolArgs(call.arguments) }, "Executing tool");
|
|
85
|
+
const result = await tool.execute(call.arguments);
|
|
86
|
+
return {
|
|
87
|
+
...result,
|
|
88
|
+
toolCallId: call.id,
|
|
89
|
+
name: call.name
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : "Unknown execution error";
|
|
93
|
+
logger.error({ toolName: call.name, error: message }, "Tool execution failed");
|
|
94
|
+
return {
|
|
95
|
+
toolCallId: call.id,
|
|
96
|
+
name: call.name,
|
|
97
|
+
content: message,
|
|
98
|
+
isError: true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
104
|
+
var DEFAULT_LINE_LIMIT = 2e3;
|
|
105
|
+
var MAX_LINE_LENGTH = 2e3;
|
|
106
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
107
|
+
".png",
|
|
108
|
+
".jpg",
|
|
109
|
+
".jpeg",
|
|
110
|
+
".gif",
|
|
111
|
+
".bmp",
|
|
112
|
+
".ico",
|
|
113
|
+
".webp",
|
|
114
|
+
".svg",
|
|
115
|
+
".mp3",
|
|
116
|
+
".mp4",
|
|
117
|
+
".avi",
|
|
118
|
+
".mov",
|
|
119
|
+
".wav",
|
|
120
|
+
".flac",
|
|
121
|
+
".zip",
|
|
122
|
+
".gz",
|
|
123
|
+
".tar",
|
|
124
|
+
".bz2",
|
|
125
|
+
".7z",
|
|
126
|
+
".rar",
|
|
127
|
+
".exe",
|
|
128
|
+
".dll",
|
|
129
|
+
".so",
|
|
130
|
+
".dylib",
|
|
131
|
+
".o",
|
|
132
|
+
".a",
|
|
133
|
+
".pdf",
|
|
134
|
+
".doc",
|
|
135
|
+
".docx",
|
|
136
|
+
".xls",
|
|
137
|
+
".xlsx",
|
|
138
|
+
".woff",
|
|
139
|
+
".woff2",
|
|
140
|
+
".ttf",
|
|
141
|
+
".eot",
|
|
142
|
+
".otf",
|
|
143
|
+
".sqlite",
|
|
144
|
+
".db"
|
|
145
|
+
]);
|
|
146
|
+
function isBinaryFile(filePath, buffer) {
|
|
147
|
+
if (BINARY_EXTENSIONS.has(extname(filePath).toLowerCase())) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
const sample = buffer.subarray(0, 8192);
|
|
151
|
+
for (let i = 0; i < sample.length; i++) {
|
|
152
|
+
if (sample[i] === 0) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
function formatWithLineNumbers(content, offset, limit) {
|
|
159
|
+
const allLines = content.split("\n");
|
|
160
|
+
const startLine = Math.max(0, offset);
|
|
161
|
+
const endLine = Math.min(allLines.length, startLine + limit);
|
|
162
|
+
const sliced = allLines.slice(startLine, endLine);
|
|
163
|
+
const maxLineNum = endLine;
|
|
164
|
+
const padWidth = String(maxLineNum).length;
|
|
165
|
+
return sliced.map((line, idx) => {
|
|
166
|
+
const lineNum = String(startLine + idx + 1).padStart(padWidth, " ");
|
|
167
|
+
const truncated = line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line;
|
|
168
|
+
return `${lineNum} ${truncated}`;
|
|
169
|
+
}).join("\n");
|
|
170
|
+
}
|
|
171
|
+
var projectRoot = process.cwd();
|
|
172
|
+
function setReadProjectRoot(root) {
|
|
173
|
+
projectRoot = root;
|
|
174
|
+
}
|
|
175
|
+
function createReadTool() {
|
|
176
|
+
return {
|
|
177
|
+
definition: {
|
|
178
|
+
name: "read",
|
|
179
|
+
description: "Read a file from the filesystem with line numbers. Supports offset and limit for large files.",
|
|
180
|
+
parameters: [
|
|
181
|
+
{
|
|
182
|
+
name: "file_path",
|
|
183
|
+
type: "string",
|
|
184
|
+
description: "Absolute path to the file to read",
|
|
185
|
+
required: true
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "offset",
|
|
189
|
+
type: "number",
|
|
190
|
+
description: "Line number to start reading from (0-indexed)",
|
|
191
|
+
required: false,
|
|
192
|
+
default: 0
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "limit",
|
|
196
|
+
type: "number",
|
|
197
|
+
description: "Maximum number of lines to read",
|
|
198
|
+
required: false,
|
|
199
|
+
default: DEFAULT_LINE_LIMIT
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
category: "file",
|
|
204
|
+
requiresApproval: (_mode, _args) => {
|
|
205
|
+
return false;
|
|
206
|
+
},
|
|
207
|
+
execute: async (args) => {
|
|
208
|
+
const filePath = args["file_path"];
|
|
209
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
210
|
+
return {
|
|
211
|
+
toolCallId: "",
|
|
212
|
+
name: "read",
|
|
213
|
+
content: "file_path parameter is required and must be a non-empty string.",
|
|
214
|
+
isError: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
let resolvedPath;
|
|
218
|
+
try {
|
|
219
|
+
resolvedPath = validatePath(filePath, projectRoot);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const msg = err instanceof Error ? err.message : "Path validation failed";
|
|
222
|
+
return { toolCallId: "", name: "read", content: msg, isError: true };
|
|
223
|
+
}
|
|
224
|
+
let fileStat;
|
|
225
|
+
try {
|
|
226
|
+
fileStat = await stat(resolvedPath);
|
|
227
|
+
} catch {
|
|
228
|
+
throw new FileNotFoundError(resolvedPath);
|
|
229
|
+
}
|
|
230
|
+
if (!fileStat.isFile()) {
|
|
231
|
+
return {
|
|
232
|
+
toolCallId: "",
|
|
233
|
+
name: "read",
|
|
234
|
+
content: `"${resolvedPath}" is not a regular file. Use Bash ls to list directories.`,
|
|
235
|
+
isError: true
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
239
|
+
return {
|
|
240
|
+
toolCallId: "",
|
|
241
|
+
name: "read",
|
|
242
|
+
content: `File is too large (${(fileStat.size / 1024 / 1024).toFixed(1)} MB). Maximum is ${MAX_FILE_SIZE / 1024 / 1024} MB.`,
|
|
243
|
+
isError: true
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const rawBuffer = await readFile(resolvedPath);
|
|
247
|
+
if (isBinaryFile(resolvedPath, rawBuffer)) {
|
|
248
|
+
return {
|
|
249
|
+
toolCallId: "",
|
|
250
|
+
name: "read",
|
|
251
|
+
content: `Binary file detected: ${resolvedPath} (${fileStat.size} bytes). Cannot display binary content.`,
|
|
252
|
+
isError: false
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const content = rawBuffer.toString("utf-8");
|
|
256
|
+
if (content.length === 0) {
|
|
257
|
+
return {
|
|
258
|
+
toolCallId: "",
|
|
259
|
+
name: "read",
|
|
260
|
+
content: `File "${resolvedPath}" exists but is empty.`,
|
|
261
|
+
isError: false
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const offset = typeof args["offset"] === "number" ? args["offset"] : 0;
|
|
265
|
+
const limit = typeof args["limit"] === "number" ? args["limit"] : DEFAULT_LINE_LIMIT;
|
|
266
|
+
const formatted = formatWithLineNumbers(content, offset, limit);
|
|
267
|
+
logger.debug({ file: resolvedPath, offset, limit }, "File read");
|
|
268
|
+
return {
|
|
269
|
+
toolCallId: "",
|
|
270
|
+
name: "read",
|
|
271
|
+
content: formatted,
|
|
272
|
+
isError: false
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
var CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
278
|
+
".env",
|
|
279
|
+
".pem",
|
|
280
|
+
".key",
|
|
281
|
+
".crt",
|
|
282
|
+
".p12",
|
|
283
|
+
".pfx",
|
|
284
|
+
".jks"
|
|
285
|
+
]);
|
|
286
|
+
var SENSITIVE_FILENAMES = /* @__PURE__ */ new Set([
|
|
287
|
+
".env",
|
|
288
|
+
".env.local",
|
|
289
|
+
".env.production",
|
|
290
|
+
".env.development",
|
|
291
|
+
"credentials.json",
|
|
292
|
+
"credentials.enc",
|
|
293
|
+
"secrets.json",
|
|
294
|
+
"id_rsa",
|
|
295
|
+
"id_ed25519",
|
|
296
|
+
"config.json"
|
|
297
|
+
]);
|
|
298
|
+
function isConfigFile(filePath) {
|
|
299
|
+
const ext = extname(filePath).toLowerCase();
|
|
300
|
+
const base = filePath.split("/").pop() ?? "";
|
|
301
|
+
return CONFIG_EXTENSIONS.has(ext) || SENSITIVE_FILENAMES.has(base);
|
|
302
|
+
}
|
|
303
|
+
var projectRoot2 = process.cwd();
|
|
304
|
+
function setWriteProjectRoot(root) {
|
|
305
|
+
projectRoot2 = root;
|
|
306
|
+
}
|
|
307
|
+
function createWriteTool() {
|
|
308
|
+
return {
|
|
309
|
+
definition: {
|
|
310
|
+
name: "write",
|
|
311
|
+
description: "Write content to a file. Creates parent directories if needed. Overwrites existing files.",
|
|
312
|
+
parameters: [
|
|
313
|
+
{
|
|
314
|
+
name: "file_path",
|
|
315
|
+
type: "string",
|
|
316
|
+
description: "Absolute path to the file to write",
|
|
317
|
+
required: true
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "content",
|
|
321
|
+
type: "string",
|
|
322
|
+
description: "The content to write to the file",
|
|
323
|
+
required: true
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
category: "file",
|
|
328
|
+
requiresApproval: (mode, _args) => {
|
|
329
|
+
return mode === "strict" || mode === "standard";
|
|
330
|
+
},
|
|
331
|
+
execute: async (args) => {
|
|
332
|
+
const filePath = args["file_path"];
|
|
333
|
+
const content = args["content"];
|
|
334
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
335
|
+
return {
|
|
336
|
+
toolCallId: "",
|
|
337
|
+
name: "write",
|
|
338
|
+
content: "file_path parameter is required and must be a non-empty string.",
|
|
339
|
+
isError: true
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (typeof content !== "string") {
|
|
343
|
+
return {
|
|
344
|
+
toolCallId: "",
|
|
345
|
+
name: "write",
|
|
346
|
+
content: "content parameter is required and must be a string.",
|
|
347
|
+
isError: true
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
let resolvedPath;
|
|
351
|
+
try {
|
|
352
|
+
resolvedPath = validatePath(filePath, projectRoot2);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
const msg = err instanceof Error ? err.message : "Path validation failed";
|
|
355
|
+
return { toolCallId: "", name: "write", content: msg, isError: true };
|
|
356
|
+
}
|
|
357
|
+
const parentDir = dirname(resolvedPath);
|
|
358
|
+
try {
|
|
359
|
+
await mkdir(parentDir, { recursive: true });
|
|
360
|
+
} catch (err) {
|
|
361
|
+
const msg = err instanceof Error ? err.message : "Failed to create directory";
|
|
362
|
+
return {
|
|
363
|
+
toolCallId: "",
|
|
364
|
+
name: "write",
|
|
365
|
+
content: `Failed to create parent directory: ${msg}`,
|
|
366
|
+
isError: true
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const fileMode = isConfigFile(resolvedPath) ? 384 : 420;
|
|
370
|
+
let existed = false;
|
|
371
|
+
try {
|
|
372
|
+
const fileStat = await stat(resolvedPath);
|
|
373
|
+
existed = fileStat.isFile();
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
await writeFile(resolvedPath, content, { encoding: "utf-8", mode: fileMode });
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const msg = err instanceof Error ? err.message : "Write failed";
|
|
380
|
+
return {
|
|
381
|
+
toolCallId: "",
|
|
382
|
+
name: "write",
|
|
383
|
+
content: `Failed to write file: ${msg}`,
|
|
384
|
+
isError: true
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const lineCount = content.split("\n").length;
|
|
388
|
+
const action = existed ? "Updated" : "Created";
|
|
389
|
+
logger.debug(
|
|
390
|
+
{ file: resolvedPath, lines: lineCount, mode: fileMode.toString(8) },
|
|
391
|
+
`File ${action.toLowerCase()}`
|
|
392
|
+
);
|
|
393
|
+
return {
|
|
394
|
+
toolCallId: "",
|
|
395
|
+
name: "write",
|
|
396
|
+
content: `${action} ${resolvedPath} (${lineCount} lines)`,
|
|
397
|
+
isError: false
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
var projectRoot3 = process.cwd();
|
|
403
|
+
function setEditProjectRoot(root) {
|
|
404
|
+
projectRoot3 = root;
|
|
405
|
+
}
|
|
406
|
+
function createEditTool() {
|
|
407
|
+
return {
|
|
408
|
+
definition: {
|
|
409
|
+
name: "edit",
|
|
410
|
+
description: "Perform exact string replacement in a file. The old_string must be unique unless replace_all is true.",
|
|
411
|
+
parameters: [
|
|
412
|
+
{
|
|
413
|
+
name: "file_path",
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Absolute path to the file to edit",
|
|
416
|
+
required: true
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "old_string",
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "The exact text to find and replace",
|
|
422
|
+
required: true
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "new_string",
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "The replacement text",
|
|
428
|
+
required: true
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "replace_all",
|
|
432
|
+
type: "boolean",
|
|
433
|
+
description: "Replace all occurrences instead of requiring uniqueness",
|
|
434
|
+
required: false,
|
|
435
|
+
default: false
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
},
|
|
439
|
+
category: "file",
|
|
440
|
+
requiresApproval: (mode, _args) => {
|
|
441
|
+
return mode === "strict" || mode === "standard";
|
|
442
|
+
},
|
|
443
|
+
execute: async (args) => {
|
|
444
|
+
const filePath = args["file_path"];
|
|
445
|
+
const oldString = args["old_string"];
|
|
446
|
+
const newString = args["new_string"];
|
|
447
|
+
const replaceAll = args["replace_all"] === true;
|
|
448
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
449
|
+
return {
|
|
450
|
+
toolCallId: "",
|
|
451
|
+
name: "edit",
|
|
452
|
+
content: "file_path parameter is required and must be a non-empty string.",
|
|
453
|
+
isError: true
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (typeof oldString !== "string") {
|
|
457
|
+
return {
|
|
458
|
+
toolCallId: "",
|
|
459
|
+
name: "edit",
|
|
460
|
+
content: "old_string parameter is required and must be a string.",
|
|
461
|
+
isError: true
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (typeof newString !== "string") {
|
|
465
|
+
return {
|
|
466
|
+
toolCallId: "",
|
|
467
|
+
name: "edit",
|
|
468
|
+
content: "new_string parameter is required and must be a string.",
|
|
469
|
+
isError: true
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (oldString === newString) {
|
|
473
|
+
return {
|
|
474
|
+
toolCallId: "",
|
|
475
|
+
name: "edit",
|
|
476
|
+
content: "old_string and new_string are identical \u2014 no edit needed.",
|
|
477
|
+
isError: true
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
let resolvedPath;
|
|
481
|
+
try {
|
|
482
|
+
resolvedPath = validatePath(filePath, projectRoot3);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
const msg = err instanceof Error ? err.message : "Path validation failed";
|
|
485
|
+
return { toolCallId: "", name: "edit", content: msg, isError: true };
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const fileStat = await stat(resolvedPath);
|
|
489
|
+
if (!fileStat.isFile()) {
|
|
490
|
+
return {
|
|
491
|
+
toolCallId: "",
|
|
492
|
+
name: "edit",
|
|
493
|
+
content: `"${resolvedPath}" is not a regular file.`,
|
|
494
|
+
isError: true
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
throw new FileNotFoundError(resolvedPath);
|
|
499
|
+
}
|
|
500
|
+
const rawBuffer = await readFile(resolvedPath);
|
|
501
|
+
const originalContent = rawBuffer.toString("utf-8");
|
|
502
|
+
if (!originalContent.includes(oldString)) {
|
|
503
|
+
return {
|
|
504
|
+
toolCallId: "",
|
|
505
|
+
name: "edit",
|
|
506
|
+
content: `old_string not found in ${resolvedPath}. Ensure it matches the file content exactly, including whitespace and indentation.`,
|
|
507
|
+
isError: true
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if (!replaceAll) {
|
|
511
|
+
const firstIdx = originalContent.indexOf(oldString);
|
|
512
|
+
const secondIdx = originalContent.indexOf(oldString, firstIdx + 1);
|
|
513
|
+
if (secondIdx !== -1) {
|
|
514
|
+
const occurrences = originalContent.split(oldString).length - 1;
|
|
515
|
+
return {
|
|
516
|
+
toolCallId: "",
|
|
517
|
+
name: "edit",
|
|
518
|
+
content: `old_string is not unique \u2014 found ${occurrences} occurrences in ${resolvedPath}. Provide more surrounding context to make it unique, or set replace_all to true.`,
|
|
519
|
+
isError: true
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
let newContent;
|
|
524
|
+
let replacementCount;
|
|
525
|
+
if (replaceAll) {
|
|
526
|
+
replacementCount = originalContent.split(oldString).length - 1;
|
|
527
|
+
newContent = originalContent.split(oldString).join(newString);
|
|
528
|
+
} else {
|
|
529
|
+
replacementCount = 1;
|
|
530
|
+
const idx = originalContent.indexOf(oldString);
|
|
531
|
+
newContent = originalContent.substring(0, idx) + newString + originalContent.substring(idx + oldString.length);
|
|
532
|
+
}
|
|
533
|
+
await writeFile(resolvedPath, newContent, "utf-8");
|
|
534
|
+
logger.debug(
|
|
535
|
+
{ file: resolvedPath, replacements: replacementCount },
|
|
536
|
+
"File edited"
|
|
537
|
+
);
|
|
538
|
+
return {
|
|
539
|
+
toolCallId: "",
|
|
540
|
+
name: "edit",
|
|
541
|
+
content: `Edited ${resolvedPath}: ${replacementCount} replacement(s) made.`,
|
|
542
|
+
isError: false
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
var MAX_RESULTS = 1e3;
|
|
548
|
+
var projectRoot4 = process.cwd();
|
|
549
|
+
function setGlobProjectRoot(root) {
|
|
550
|
+
projectRoot4 = root;
|
|
551
|
+
}
|
|
552
|
+
function createGlobTool() {
|
|
553
|
+
return {
|
|
554
|
+
definition: {
|
|
555
|
+
name: "glob",
|
|
556
|
+
description: "Find files matching a glob pattern. Results are sorted by modification time (newest first).",
|
|
557
|
+
parameters: [
|
|
558
|
+
{
|
|
559
|
+
name: "pattern",
|
|
560
|
+
type: "string",
|
|
561
|
+
description: 'Glob pattern to match (e.g. "**/*.ts", "src/**/*.tsx")',
|
|
562
|
+
required: true
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: "path",
|
|
566
|
+
type: "string",
|
|
567
|
+
description: "Directory to search in. Defaults to project root.",
|
|
568
|
+
required: false
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
},
|
|
572
|
+
category: "search",
|
|
573
|
+
requiresApproval: (_mode, _args) => {
|
|
574
|
+
return false;
|
|
575
|
+
},
|
|
576
|
+
execute: async (args) => {
|
|
577
|
+
const pattern = args["pattern"];
|
|
578
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
579
|
+
return {
|
|
580
|
+
toolCallId: "",
|
|
581
|
+
name: "glob",
|
|
582
|
+
content: "pattern parameter is required and must be a non-empty string.",
|
|
583
|
+
isError: true
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
let searchPath;
|
|
587
|
+
if (typeof args["path"] === "string" && args["path"].length > 0) {
|
|
588
|
+
const resolved = resolve(projectRoot4, args["path"]);
|
|
589
|
+
searchPath = validatePath(resolved, projectRoot4);
|
|
590
|
+
} else {
|
|
591
|
+
searchPath = projectRoot4;
|
|
592
|
+
}
|
|
593
|
+
let matchedPaths;
|
|
594
|
+
try {
|
|
595
|
+
matchedPaths = await fg(pattern, {
|
|
596
|
+
cwd: searchPath,
|
|
597
|
+
absolute: true,
|
|
598
|
+
dot: false,
|
|
599
|
+
onlyFiles: true,
|
|
600
|
+
ignore: [
|
|
601
|
+
"**/node_modules/**",
|
|
602
|
+
"**/.git/**",
|
|
603
|
+
"**/dist/**",
|
|
604
|
+
"**/build/**",
|
|
605
|
+
"**/.next/**",
|
|
606
|
+
"**/coverage/**"
|
|
607
|
+
]
|
|
608
|
+
});
|
|
609
|
+
} catch (err) {
|
|
610
|
+
const msg = err instanceof Error ? err.message : "Glob search failed";
|
|
611
|
+
return { toolCallId: "", name: "glob", content: msg, isError: true };
|
|
612
|
+
}
|
|
613
|
+
if (matchedPaths.length === 0) {
|
|
614
|
+
return {
|
|
615
|
+
toolCallId: "",
|
|
616
|
+
name: "glob",
|
|
617
|
+
content: "No files found",
|
|
618
|
+
isError: false
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const entries = [];
|
|
622
|
+
for (const filePath of matchedPaths) {
|
|
623
|
+
try {
|
|
624
|
+
const fileStat = await stat(filePath);
|
|
625
|
+
entries.push({ path: filePath, mtimeMs: fileStat.mtimeMs });
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
630
|
+
const truncated = entries.length > MAX_RESULTS;
|
|
631
|
+
const resultEntries = truncated ? entries.slice(0, MAX_RESULTS) : entries;
|
|
632
|
+
const output = resultEntries.map((e) => e.path).join("\n");
|
|
633
|
+
logger.debug(
|
|
634
|
+
{ pattern, searchPath, total: entries.length, returned: resultEntries.length },
|
|
635
|
+
"Glob search complete"
|
|
636
|
+
);
|
|
637
|
+
const suffix = truncated ? `
|
|
638
|
+
|
|
639
|
+
(Showing ${MAX_RESULTS} of ${entries.length} matches)` : "";
|
|
640
|
+
return {
|
|
641
|
+
toolCallId: "",
|
|
642
|
+
name: "glob",
|
|
643
|
+
content: output + suffix,
|
|
644
|
+
isError: false
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
var execFileAsync = promisify(execFile);
|
|
650
|
+
var MAX_OUTPUT_LENGTH = 3e4;
|
|
651
|
+
var DEFAULT_HEAD_LIMIT = 0;
|
|
652
|
+
async function findSearchBinary() {
|
|
653
|
+
try {
|
|
654
|
+
await execFileAsync("which", ["rg"]);
|
|
655
|
+
return "rg";
|
|
656
|
+
} catch {
|
|
657
|
+
return "grep";
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function buildRipgrepArgs(pattern, searchPath, outputMode, opts) {
|
|
661
|
+
const args = [];
|
|
662
|
+
if (outputMode === "files_with_matches") {
|
|
663
|
+
args.push("--files-with-matches");
|
|
664
|
+
} else if (outputMode === "count") {
|
|
665
|
+
args.push("--count");
|
|
666
|
+
}
|
|
667
|
+
if (opts.caseInsensitive === true) {
|
|
668
|
+
args.push("-i");
|
|
669
|
+
}
|
|
670
|
+
if (opts.multiline === true) {
|
|
671
|
+
args.push("-U", "--multiline-dotall");
|
|
672
|
+
}
|
|
673
|
+
if (outputMode === "content") {
|
|
674
|
+
{
|
|
675
|
+
args.push("-n");
|
|
676
|
+
}
|
|
677
|
+
if (typeof opts.contextLines === "number" && opts.contextLines > 0) {
|
|
678
|
+
args.push("-C", String(opts.contextLines));
|
|
679
|
+
} else {
|
|
680
|
+
if (typeof opts.beforeContext === "number" && opts.beforeContext > 0) {
|
|
681
|
+
args.push("-B", String(opts.beforeContext));
|
|
682
|
+
}
|
|
683
|
+
if (typeof opts.afterContext === "number" && opts.afterContext > 0) {
|
|
684
|
+
args.push("-A", String(opts.afterContext));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (typeof opts.glob === "string" && opts.glob.length > 0) {
|
|
689
|
+
args.push("--glob", opts.glob);
|
|
690
|
+
}
|
|
691
|
+
if (typeof opts.fileType === "string" && opts.fileType.length > 0) {
|
|
692
|
+
args.push("--type", opts.fileType);
|
|
693
|
+
}
|
|
694
|
+
args.push(
|
|
695
|
+
"--glob",
|
|
696
|
+
"!node_modules",
|
|
697
|
+
"--glob",
|
|
698
|
+
"!.git",
|
|
699
|
+
"--glob",
|
|
700
|
+
"!dist",
|
|
701
|
+
"--glob",
|
|
702
|
+
"!build",
|
|
703
|
+
"--glob",
|
|
704
|
+
"!coverage"
|
|
705
|
+
);
|
|
706
|
+
args.push("--", pattern, searchPath);
|
|
707
|
+
return args;
|
|
708
|
+
}
|
|
709
|
+
function buildGrepArgs(pattern, searchPath, outputMode, opts) {
|
|
710
|
+
const args = ["-r", "--extended-regexp"];
|
|
711
|
+
if (outputMode === "files_with_matches") {
|
|
712
|
+
args.push("-l");
|
|
713
|
+
} else if (outputMode === "count") {
|
|
714
|
+
args.push("-c");
|
|
715
|
+
}
|
|
716
|
+
if (opts.caseInsensitive === true) {
|
|
717
|
+
args.push("-i");
|
|
718
|
+
}
|
|
719
|
+
if (outputMode === "content" && typeof opts.contextLines === "number" && opts.contextLines > 0) {
|
|
720
|
+
args.push("-C", String(opts.contextLines));
|
|
721
|
+
}
|
|
722
|
+
args.push(
|
|
723
|
+
"--exclude-dir=node_modules",
|
|
724
|
+
"--exclude-dir=.git",
|
|
725
|
+
"--exclude-dir=dist",
|
|
726
|
+
"--exclude-dir=build"
|
|
727
|
+
);
|
|
728
|
+
args.push("--", pattern, searchPath);
|
|
729
|
+
return args;
|
|
730
|
+
}
|
|
731
|
+
function applyLimits(output, headLimit, offset) {
|
|
732
|
+
let lines = output.split("\n");
|
|
733
|
+
if (offset > 0) {
|
|
734
|
+
lines = lines.slice(offset);
|
|
735
|
+
}
|
|
736
|
+
if (headLimit > 0) {
|
|
737
|
+
lines = lines.slice(0, headLimit);
|
|
738
|
+
}
|
|
739
|
+
let result = lines.join("\n");
|
|
740
|
+
if (result.length > MAX_OUTPUT_LENGTH) {
|
|
741
|
+
result = result.substring(0, MAX_OUTPUT_LENGTH) + "\n...(truncated)";
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
var projectRoot5 = process.cwd();
|
|
746
|
+
function setGrepProjectRoot(root) {
|
|
747
|
+
projectRoot5 = root;
|
|
748
|
+
}
|
|
749
|
+
function createGrepTool() {
|
|
750
|
+
return {
|
|
751
|
+
definition: {
|
|
752
|
+
name: "grep",
|
|
753
|
+
description: "Search file contents using regex patterns. Uses ripgrep (rg) with grep fallback.",
|
|
754
|
+
parameters: [
|
|
755
|
+
{
|
|
756
|
+
name: "pattern",
|
|
757
|
+
type: "string",
|
|
758
|
+
description: "Regular expression pattern to search for",
|
|
759
|
+
required: true
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
name: "path",
|
|
763
|
+
type: "string",
|
|
764
|
+
description: "File or directory to search in. Defaults to project root.",
|
|
765
|
+
required: false
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "output_mode",
|
|
769
|
+
type: "string",
|
|
770
|
+
description: "Output mode: content, files_with_matches, or count",
|
|
771
|
+
required: false,
|
|
772
|
+
default: "files_with_matches",
|
|
773
|
+
enum: ["content", "files_with_matches", "count"]
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: "glob",
|
|
777
|
+
type: "string",
|
|
778
|
+
description: 'Glob filter for files (e.g. "*.ts")',
|
|
779
|
+
required: false
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
name: "type",
|
|
783
|
+
type: "string",
|
|
784
|
+
description: 'File type filter (e.g. "ts", "py")',
|
|
785
|
+
required: false
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
name: "context",
|
|
789
|
+
type: "number",
|
|
790
|
+
description: "Lines of context around matches",
|
|
791
|
+
required: false
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
name: "-B",
|
|
795
|
+
type: "number",
|
|
796
|
+
description: "Lines of context before matches",
|
|
797
|
+
required: false
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: "-A",
|
|
801
|
+
type: "number",
|
|
802
|
+
description: "Lines of context after matches",
|
|
803
|
+
required: false
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
name: "-i",
|
|
807
|
+
type: "boolean",
|
|
808
|
+
description: "Case insensitive search",
|
|
809
|
+
required: false,
|
|
810
|
+
default: false
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
name: "multiline",
|
|
814
|
+
type: "boolean",
|
|
815
|
+
description: "Enable multiline matching",
|
|
816
|
+
required: false,
|
|
817
|
+
default: false
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "head_limit",
|
|
821
|
+
type: "number",
|
|
822
|
+
description: "Limit output to first N entries",
|
|
823
|
+
required: false,
|
|
824
|
+
default: 0
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
name: "offset",
|
|
828
|
+
type: "number",
|
|
829
|
+
description: "Skip first N entries",
|
|
830
|
+
required: false,
|
|
831
|
+
default: 0
|
|
832
|
+
}
|
|
833
|
+
]
|
|
834
|
+
},
|
|
835
|
+
category: "search",
|
|
836
|
+
requiresApproval: (_mode, _args) => {
|
|
837
|
+
return false;
|
|
838
|
+
},
|
|
839
|
+
execute: async (args) => {
|
|
840
|
+
const pattern = args["pattern"];
|
|
841
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
842
|
+
return {
|
|
843
|
+
toolCallId: "",
|
|
844
|
+
name: "grep",
|
|
845
|
+
content: "pattern parameter is required and must be a non-empty string.",
|
|
846
|
+
isError: true
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
let searchPath;
|
|
850
|
+
if (typeof args["path"] === "string" && args["path"].length > 0) {
|
|
851
|
+
const resolved = resolve(projectRoot5, args["path"]);
|
|
852
|
+
searchPath = validatePath(resolved, projectRoot5);
|
|
853
|
+
} else {
|
|
854
|
+
searchPath = projectRoot5;
|
|
855
|
+
}
|
|
856
|
+
const outputMode = args["output_mode"] === "content" || args["output_mode"] === "count" ? args["output_mode"] : "files_with_matches";
|
|
857
|
+
const headLimit = typeof args["head_limit"] === "number" ? args["head_limit"] : DEFAULT_HEAD_LIMIT;
|
|
858
|
+
const offset = typeof args["offset"] === "number" ? args["offset"] : 0;
|
|
859
|
+
const binary = await findSearchBinary();
|
|
860
|
+
const searchArgs = binary === "rg" ? buildRipgrepArgs(pattern, searchPath, outputMode, {
|
|
861
|
+
glob: typeof args["glob"] === "string" ? args["glob"] : void 0,
|
|
862
|
+
fileType: typeof args["type"] === "string" ? args["type"] : void 0,
|
|
863
|
+
contextLines: typeof args["context"] === "number" ? args["context"] : void 0,
|
|
864
|
+
beforeContext: typeof args["-B"] === "number" ? args["-B"] : void 0,
|
|
865
|
+
afterContext: typeof args["-A"] === "number" ? args["-A"] : void 0,
|
|
866
|
+
caseInsensitive: args["-i"] === true,
|
|
867
|
+
multiline: args["multiline"] === true}) : buildGrepArgs(pattern, searchPath, outputMode, {
|
|
868
|
+
caseInsensitive: args["-i"] === true,
|
|
869
|
+
contextLines: typeof args["context"] === "number" ? args["context"] : void 0
|
|
870
|
+
});
|
|
871
|
+
try {
|
|
872
|
+
const { stdout } = await execFileAsync(binary, searchArgs, {
|
|
873
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
874
|
+
timeout: 3e4
|
|
875
|
+
});
|
|
876
|
+
const result = applyLimits(stdout.trim(), headLimit, offset);
|
|
877
|
+
if (result.length === 0) {
|
|
878
|
+
return {
|
|
879
|
+
toolCallId: "",
|
|
880
|
+
name: "grep",
|
|
881
|
+
content: "No matches found.",
|
|
882
|
+
isError: false
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
logger.debug({ binary, pattern, outputMode, matches: result.split("\n").length }, "Grep complete");
|
|
886
|
+
return {
|
|
887
|
+
toolCallId: "",
|
|
888
|
+
name: "grep",
|
|
889
|
+
content: result,
|
|
890
|
+
isError: false
|
|
891
|
+
};
|
|
892
|
+
} catch (err) {
|
|
893
|
+
if (err instanceof Error && "code" in err && err.code === "1") {
|
|
894
|
+
return {
|
|
895
|
+
toolCallId: "",
|
|
896
|
+
name: "grep",
|
|
897
|
+
content: "No matches found.",
|
|
898
|
+
isError: false
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const execError = err;
|
|
902
|
+
if (execError.status === 1) {
|
|
903
|
+
return {
|
|
904
|
+
toolCallId: "",
|
|
905
|
+
name: "grep",
|
|
906
|
+
content: "No matches found.",
|
|
907
|
+
isError: false
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const msg = err instanceof Error ? err.message : "Search failed";
|
|
911
|
+
logger.error({ binary, pattern, error: msg }, "Grep failed");
|
|
912
|
+
return {
|
|
913
|
+
toolCallId: "",
|
|
914
|
+
name: "grep",
|
|
915
|
+
content: `Search failed: ${msg}`,
|
|
916
|
+
isError: true
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
923
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
924
|
+
var MAX_OUTPUT_LENGTH2 = 3e4;
|
|
925
|
+
var DANGEROUS_PATTERNS = [
|
|
926
|
+
"rm -rf /",
|
|
927
|
+
"rm -rf ~",
|
|
928
|
+
"mkfs",
|
|
929
|
+
"dd if=",
|
|
930
|
+
"> /dev/sd",
|
|
931
|
+
"chmod -R 777 /",
|
|
932
|
+
":(){ :|:& };:",
|
|
933
|
+
"fork bomb",
|
|
934
|
+
"shutdown",
|
|
935
|
+
"reboot",
|
|
936
|
+
"init 0",
|
|
937
|
+
"init 6"
|
|
938
|
+
];
|
|
939
|
+
var ALWAYS_DANGEROUS_COMMANDS = [
|
|
940
|
+
"push --force",
|
|
941
|
+
"push -f",
|
|
942
|
+
"reset --hard",
|
|
943
|
+
"clean -fd",
|
|
944
|
+
"branch -D",
|
|
945
|
+
"rm -rf",
|
|
946
|
+
"drop table",
|
|
947
|
+
"drop database",
|
|
948
|
+
"truncate table"
|
|
949
|
+
];
|
|
950
|
+
var SENSITIVE_ENV_PATTERNS = [
|
|
951
|
+
"API_KEY",
|
|
952
|
+
"SECRET",
|
|
953
|
+
"TOKEN",
|
|
954
|
+
"PASSWORD",
|
|
955
|
+
"CREDENTIAL",
|
|
956
|
+
"ANTHROPIC_API",
|
|
957
|
+
"OPENAI_API",
|
|
958
|
+
"GOOGLE_API",
|
|
959
|
+
"MOONSHOT_API",
|
|
960
|
+
"KIMI_API",
|
|
961
|
+
"SESSION_TOKEN",
|
|
962
|
+
"REFRESH_TOKEN",
|
|
963
|
+
"ACCESS_TOKEN",
|
|
964
|
+
"PRIVATE_KEY"
|
|
965
|
+
];
|
|
966
|
+
function filterSensitiveEnv(env) {
|
|
967
|
+
const filtered = {};
|
|
968
|
+
for (const [key, value] of Object.entries(env)) {
|
|
969
|
+
const upperKey = key.toUpperCase();
|
|
970
|
+
const isSensitive = SENSITIVE_ENV_PATTERNS.some((pattern) => upperKey.includes(pattern));
|
|
971
|
+
if (!isSensitive) {
|
|
972
|
+
filtered[key] = value;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return filtered;
|
|
976
|
+
}
|
|
977
|
+
function isDangerousCommand(command) {
|
|
978
|
+
const lowerCommand = command.toLowerCase().trim();
|
|
979
|
+
return DANGEROUS_PATTERNS.some((p) => lowerCommand.includes(p)) || ALWAYS_DANGEROUS_COMMANDS.some((p) => lowerCommand.includes(p.toLowerCase()));
|
|
980
|
+
}
|
|
981
|
+
function truncateOutput(output) {
|
|
982
|
+
if (output.length <= MAX_OUTPUT_LENGTH2) {
|
|
983
|
+
return output;
|
|
984
|
+
}
|
|
985
|
+
return output.substring(0, MAX_OUTPUT_LENGTH2) + "\n...(truncated)";
|
|
986
|
+
}
|
|
987
|
+
var workingDirectory = process.cwd();
|
|
988
|
+
var blockedCommands = [];
|
|
989
|
+
function setBashWorkingDirectory(dir) {
|
|
990
|
+
workingDirectory = dir;
|
|
991
|
+
}
|
|
992
|
+
function setBashBlockedCommands(commands) {
|
|
993
|
+
blockedCommands = commands;
|
|
994
|
+
}
|
|
995
|
+
function createBashTool() {
|
|
996
|
+
return {
|
|
997
|
+
definition: {
|
|
998
|
+
name: "bash",
|
|
999
|
+
description: "Execute a shell command. Supports timeout. Dangerous commands require approval.",
|
|
1000
|
+
parameters: [
|
|
1001
|
+
{
|
|
1002
|
+
name: "command",
|
|
1003
|
+
type: "string",
|
|
1004
|
+
description: "The shell command to execute",
|
|
1005
|
+
required: true
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
name: "description",
|
|
1009
|
+
type: "string",
|
|
1010
|
+
description: "Short description of what this command does",
|
|
1011
|
+
required: false
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
name: "timeout",
|
|
1015
|
+
type: "number",
|
|
1016
|
+
description: "Timeout in milliseconds (max 600000)",
|
|
1017
|
+
required: false,
|
|
1018
|
+
default: DEFAULT_TIMEOUT_MS
|
|
1019
|
+
}
|
|
1020
|
+
]
|
|
1021
|
+
},
|
|
1022
|
+
category: "shell",
|
|
1023
|
+
requiresApproval: (mode, args) => {
|
|
1024
|
+
const command = typeof args["command"] === "string" ? args["command"] : "";
|
|
1025
|
+
if (isDangerousCommand(command) || isCommandBlocked(command, blockedCommands)) {
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
if (mode === "strict") {
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
return false;
|
|
1032
|
+
},
|
|
1033
|
+
execute: async (args) => {
|
|
1034
|
+
const command = args["command"];
|
|
1035
|
+
if (typeof command !== "string" || command.length === 0) {
|
|
1036
|
+
return {
|
|
1037
|
+
toolCallId: "",
|
|
1038
|
+
name: "bash",
|
|
1039
|
+
content: "command parameter is required and must be a non-empty string.",
|
|
1040
|
+
isError: true
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
const lowerCommand = command.toLowerCase().trim();
|
|
1044
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
1045
|
+
if (lowerCommand.includes(pattern)) {
|
|
1046
|
+
return {
|
|
1047
|
+
toolCallId: "",
|
|
1048
|
+
name: "bash",
|
|
1049
|
+
content: `Blocked: command matches dangerous pattern "${pattern}".`,
|
|
1050
|
+
isError: true
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (isCommandBlocked(command, blockedCommands)) {
|
|
1055
|
+
return {
|
|
1056
|
+
toolCallId: "",
|
|
1057
|
+
name: "bash",
|
|
1058
|
+
content: "Command is on the blocked list and cannot be executed.",
|
|
1059
|
+
isError: true
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
1063
|
+
if (typeof args["timeout"] === "number") {
|
|
1064
|
+
timeoutMs = Math.max(1e3, Math.min(args["timeout"], MAX_TIMEOUT_MS));
|
|
1065
|
+
}
|
|
1066
|
+
logger.debug({ command: redactSecrets(command), timeout: timeoutMs, cwd: workingDirectory }, "Executing bash command");
|
|
1067
|
+
try {
|
|
1068
|
+
const result = await execaCommand(command, {
|
|
1069
|
+
cwd: workingDirectory,
|
|
1070
|
+
timeout: timeoutMs,
|
|
1071
|
+
reject: false,
|
|
1072
|
+
shell: true,
|
|
1073
|
+
env: {
|
|
1074
|
+
...filterSensitiveEnv(process.env),
|
|
1075
|
+
TERM: "dumb",
|
|
1076
|
+
NO_COLOR: "1"
|
|
1077
|
+
},
|
|
1078
|
+
stripFinalNewline: true
|
|
1079
|
+
});
|
|
1080
|
+
const stdout = result.stdout ? truncateOutput(result.stdout) : "";
|
|
1081
|
+
const stderr = result.stderr ? truncateOutput(result.stderr) : "";
|
|
1082
|
+
let content = "";
|
|
1083
|
+
if (stdout.length > 0) {
|
|
1084
|
+
content += stdout;
|
|
1085
|
+
}
|
|
1086
|
+
if (stderr.length > 0) {
|
|
1087
|
+
content += (content.length > 0 ? "\n\nSTDERR:\n" : "STDERR:\n") + stderr;
|
|
1088
|
+
}
|
|
1089
|
+
if (content.length === 0) {
|
|
1090
|
+
content = `Command completed with exit code ${result.exitCode ?? 0}.`;
|
|
1091
|
+
}
|
|
1092
|
+
const isError = (result.exitCode ?? 0) !== 0;
|
|
1093
|
+
if (isError) {
|
|
1094
|
+
content = `Exit code: ${result.exitCode ?? "unknown"}
|
|
1095
|
+
${content}`;
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
toolCallId: "",
|
|
1099
|
+
name: "bash",
|
|
1100
|
+
content,
|
|
1101
|
+
isError
|
|
1102
|
+
};
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
if (err instanceof Error && err.message.includes("timed out")) {
|
|
1105
|
+
throw new ExecutionTimeoutError(command, timeoutMs);
|
|
1106
|
+
}
|
|
1107
|
+
const msg = err instanceof Error ? err.message : "Command execution failed";
|
|
1108
|
+
logger.error({ command: redactSecrets(command), error: msg }, "Bash execution failed");
|
|
1109
|
+
return {
|
|
1110
|
+
toolCallId: "",
|
|
1111
|
+
name: "bash",
|
|
1112
|
+
content: `Execution failed: ${msg}`,
|
|
1113
|
+
isError: true
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/tools/web-search.ts
|
|
1121
|
+
var searchProvider;
|
|
1122
|
+
function setWebSearchProvider(provider) {
|
|
1123
|
+
searchProvider = provider;
|
|
1124
|
+
}
|
|
1125
|
+
function createWebSearchTool() {
|
|
1126
|
+
return {
|
|
1127
|
+
definition: {
|
|
1128
|
+
name: "web_search",
|
|
1129
|
+
description: "Search the web for up-to-date information. Requires a configured search provider.",
|
|
1130
|
+
parameters: [
|
|
1131
|
+
{
|
|
1132
|
+
name: "query",
|
|
1133
|
+
type: "string",
|
|
1134
|
+
description: "The search query",
|
|
1135
|
+
required: true
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
name: "allowed_domains",
|
|
1139
|
+
type: "array",
|
|
1140
|
+
description: "Only include results from these domains",
|
|
1141
|
+
required: false
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
name: "blocked_domains",
|
|
1145
|
+
type: "array",
|
|
1146
|
+
description: "Exclude results from these domains",
|
|
1147
|
+
required: false
|
|
1148
|
+
}
|
|
1149
|
+
]
|
|
1150
|
+
},
|
|
1151
|
+
category: "web",
|
|
1152
|
+
requiresApproval: (_mode, _args) => {
|
|
1153
|
+
return false;
|
|
1154
|
+
},
|
|
1155
|
+
execute: async (args) => {
|
|
1156
|
+
const query = args["query"];
|
|
1157
|
+
if (typeof query !== "string" || query.length === 0) {
|
|
1158
|
+
return {
|
|
1159
|
+
toolCallId: "",
|
|
1160
|
+
name: "web_search",
|
|
1161
|
+
content: "query parameter is required and must be a non-empty string.",
|
|
1162
|
+
isError: true
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
if (!searchProvider) {
|
|
1166
|
+
return {
|
|
1167
|
+
toolCallId: "",
|
|
1168
|
+
name: "web_search",
|
|
1169
|
+
content: "Web search is not configured. Set up a search provider in your configuration.",
|
|
1170
|
+
isError: true
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const allowedDomains = Array.isArray(args["allowed_domains"]) ? args["allowed_domains"].filter(
|
|
1174
|
+
(d) => typeof d === "string"
|
|
1175
|
+
) : void 0;
|
|
1176
|
+
const blockedDomains = Array.isArray(args["blocked_domains"]) ? args["blocked_domains"].filter(
|
|
1177
|
+
(d) => typeof d === "string"
|
|
1178
|
+
) : void 0;
|
|
1179
|
+
try {
|
|
1180
|
+
logger.debug({ query }, "Web search executing");
|
|
1181
|
+
const results = await searchProvider(query, {
|
|
1182
|
+
allowedDomains,
|
|
1183
|
+
blockedDomains
|
|
1184
|
+
});
|
|
1185
|
+
if (results.length === 0) {
|
|
1186
|
+
return {
|
|
1187
|
+
toolCallId: "",
|
|
1188
|
+
name: "web_search",
|
|
1189
|
+
content: `No results found for: ${query}`,
|
|
1190
|
+
isError: false
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
toolCallId: "",
|
|
1195
|
+
name: "web_search",
|
|
1196
|
+
content: results,
|
|
1197
|
+
isError: false
|
|
1198
|
+
};
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
const msg = err instanceof Error ? err.message : "Search failed";
|
|
1201
|
+
logger.error({ query, error: msg }, "Web search failed");
|
|
1202
|
+
return {
|
|
1203
|
+
toolCallId: "",
|
|
1204
|
+
name: "web_search",
|
|
1205
|
+
content: `Web search failed: ${msg}`,
|
|
1206
|
+
isError: true
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/tools/web-fetch.ts
|
|
1214
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
1215
|
+
var MAX_CONTENT_LENGTH = 1e5;
|
|
1216
|
+
var MAX_URL_LENGTH = 2048;
|
|
1217
|
+
function stripHtmlTags(html) {
|
|
1218
|
+
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
1219
|
+
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
1220
|
+
text = text.replace(/<\/(p|div|h[1-6]|li|tr|br|hr)[^>]*>/gi, "\n");
|
|
1221
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
1222
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
1223
|
+
text = text.replace(/&/g, "&");
|
|
1224
|
+
text = text.replace(/</g, "<");
|
|
1225
|
+
text = text.replace(/>/g, ">");
|
|
1226
|
+
text = text.replace(/"/g, '"');
|
|
1227
|
+
text = text.replace(/'/g, "'");
|
|
1228
|
+
text = text.replace(/ /g, " ");
|
|
1229
|
+
text = text.replace(/[ \t]+/g, " ");
|
|
1230
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
1231
|
+
return text.trim();
|
|
1232
|
+
}
|
|
1233
|
+
function isPrivateHostname(hostname) {
|
|
1234
|
+
if (hostname === "localhost" || hostname === "[::1]") {
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
const ipMatch = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
1238
|
+
if (ipMatch) {
|
|
1239
|
+
const [, a, b] = ipMatch;
|
|
1240
|
+
const first = parseInt(a, 10);
|
|
1241
|
+
const second = parseInt(b, 10);
|
|
1242
|
+
if (first === 127) return true;
|
|
1243
|
+
if (first === 10) return true;
|
|
1244
|
+
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
1245
|
+
if (first === 192 && second === 168) return true;
|
|
1246
|
+
if (first === 169 && second === 254) return true;
|
|
1247
|
+
if (first === 0) return true;
|
|
1248
|
+
}
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
function isValidUrl(urlString) {
|
|
1252
|
+
if (urlString.length > MAX_URL_LENGTH) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
try {
|
|
1256
|
+
const url = new URL(urlString);
|
|
1257
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
if (isPrivateHostname(url.hostname)) {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function enforceHttps(urlString) {
|
|
1269
|
+
try {
|
|
1270
|
+
const url = new URL(urlString);
|
|
1271
|
+
if (url.protocol === "http:") {
|
|
1272
|
+
url.protocol = "https:";
|
|
1273
|
+
}
|
|
1274
|
+
return url.toString();
|
|
1275
|
+
} catch {
|
|
1276
|
+
return urlString;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function createWebFetchTool() {
|
|
1280
|
+
return {
|
|
1281
|
+
definition: {
|
|
1282
|
+
name: "web_fetch",
|
|
1283
|
+
description: "Fetch content from a URL and convert HTML to readable text. HTTP URLs are upgraded to HTTPS.",
|
|
1284
|
+
parameters: [
|
|
1285
|
+
{
|
|
1286
|
+
name: "url",
|
|
1287
|
+
type: "string",
|
|
1288
|
+
description: "The URL to fetch content from",
|
|
1289
|
+
required: true
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
name: "prompt",
|
|
1293
|
+
type: "string",
|
|
1294
|
+
description: "What information to extract from the page",
|
|
1295
|
+
required: false
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
name: "timeout",
|
|
1299
|
+
type: "number",
|
|
1300
|
+
description: "Timeout in milliseconds (default 30000)",
|
|
1301
|
+
required: false,
|
|
1302
|
+
default: DEFAULT_TIMEOUT_MS2
|
|
1303
|
+
}
|
|
1304
|
+
]
|
|
1305
|
+
},
|
|
1306
|
+
category: "web",
|
|
1307
|
+
requiresApproval: (_mode, _args) => {
|
|
1308
|
+
return false;
|
|
1309
|
+
},
|
|
1310
|
+
execute: async (args) => {
|
|
1311
|
+
const rawUrl = args["url"];
|
|
1312
|
+
if (typeof rawUrl !== "string" || rawUrl.length === 0) {
|
|
1313
|
+
return {
|
|
1314
|
+
toolCallId: "",
|
|
1315
|
+
name: "web_fetch",
|
|
1316
|
+
content: "url parameter is required and must be a non-empty string.",
|
|
1317
|
+
isError: true
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
if (!isValidUrl(rawUrl)) {
|
|
1321
|
+
return {
|
|
1322
|
+
toolCallId: "",
|
|
1323
|
+
name: "web_fetch",
|
|
1324
|
+
content: `Invalid URL: "${rawUrl}". Must be a valid HTTP(S) URL.`,
|
|
1325
|
+
isError: true
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
const url = enforceHttps(rawUrl);
|
|
1329
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS2;
|
|
1330
|
+
if (typeof args["timeout"] === "number") {
|
|
1331
|
+
timeoutMs = Math.max(5e3, Math.min(args["timeout"], 6e4));
|
|
1332
|
+
}
|
|
1333
|
+
logger.debug({ url, timeout: timeoutMs }, "Fetching URL");
|
|
1334
|
+
const controller = new AbortController();
|
|
1335
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1336
|
+
try {
|
|
1337
|
+
const response = await fetch(url, {
|
|
1338
|
+
signal: controller.signal,
|
|
1339
|
+
headers: {
|
|
1340
|
+
"User-Agent": "AemeathCLI/1.0",
|
|
1341
|
+
"Accept": "text/html, application/json, text/plain, */*"
|
|
1342
|
+
},
|
|
1343
|
+
redirect: "follow"
|
|
1344
|
+
});
|
|
1345
|
+
clearTimeout(timeoutId);
|
|
1346
|
+
if (!response.ok) {
|
|
1347
|
+
return {
|
|
1348
|
+
toolCallId: "",
|
|
1349
|
+
name: "web_fetch",
|
|
1350
|
+
content: `HTTP ${response.status} ${response.statusText} for ${url}`,
|
|
1351
|
+
isError: true
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1355
|
+
const rawBody = await response.text();
|
|
1356
|
+
let content;
|
|
1357
|
+
if (contentType.includes("text/html")) {
|
|
1358
|
+
content = stripHtmlTags(rawBody);
|
|
1359
|
+
} else {
|
|
1360
|
+
content = rawBody;
|
|
1361
|
+
}
|
|
1362
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
1363
|
+
content = content.substring(0, MAX_CONTENT_LENGTH) + "\n...(content truncated)";
|
|
1364
|
+
}
|
|
1365
|
+
if (content.length === 0) {
|
|
1366
|
+
return {
|
|
1367
|
+
toolCallId: "",
|
|
1368
|
+
name: "web_fetch",
|
|
1369
|
+
content: `Fetched ${url} but the response body was empty.`,
|
|
1370
|
+
isError: false
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
toolCallId: "",
|
|
1375
|
+
name: "web_fetch",
|
|
1376
|
+
content,
|
|
1377
|
+
isError: false
|
|
1378
|
+
};
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
clearTimeout(timeoutId);
|
|
1381
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1382
|
+
return {
|
|
1383
|
+
toolCallId: "",
|
|
1384
|
+
name: "web_fetch",
|
|
1385
|
+
content: `Request timed out after ${timeoutMs}ms for ${url}`,
|
|
1386
|
+
isError: true
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
const msg = err instanceof Error ? err.message : "Fetch failed";
|
|
1390
|
+
logger.error({ url, error: msg }, "Web fetch failed");
|
|
1391
|
+
return {
|
|
1392
|
+
toolCallId: "",
|
|
1393
|
+
name: "web_fetch",
|
|
1394
|
+
content: `Fetch failed for ${url}: ${msg}`,
|
|
1395
|
+
isError: true
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
var GIT_TIMEOUT_MS = 6e4;
|
|
1402
|
+
var MAX_OUTPUT_LENGTH3 = 3e4;
|
|
1403
|
+
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1404
|
+
"status",
|
|
1405
|
+
"diff",
|
|
1406
|
+
"log",
|
|
1407
|
+
"show",
|
|
1408
|
+
"branch",
|
|
1409
|
+
"tag",
|
|
1410
|
+
"add",
|
|
1411
|
+
"commit",
|
|
1412
|
+
"checkout",
|
|
1413
|
+
"switch",
|
|
1414
|
+
"merge",
|
|
1415
|
+
"rebase",
|
|
1416
|
+
"cherry-pick",
|
|
1417
|
+
"stash",
|
|
1418
|
+
"fetch",
|
|
1419
|
+
"pull",
|
|
1420
|
+
"push",
|
|
1421
|
+
"remote",
|
|
1422
|
+
"rev-parse",
|
|
1423
|
+
"describe",
|
|
1424
|
+
"blame"
|
|
1425
|
+
]);
|
|
1426
|
+
var DANGEROUS_FLAGS = [
|
|
1427
|
+
"--force",
|
|
1428
|
+
"-f",
|
|
1429
|
+
"--force-with-lease",
|
|
1430
|
+
"--hard",
|
|
1431
|
+
"--no-verify",
|
|
1432
|
+
"-D"
|
|
1433
|
+
];
|
|
1434
|
+
var READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1435
|
+
"status",
|
|
1436
|
+
"diff",
|
|
1437
|
+
"log",
|
|
1438
|
+
"show",
|
|
1439
|
+
"branch",
|
|
1440
|
+
"tag",
|
|
1441
|
+
"remote",
|
|
1442
|
+
"rev-parse",
|
|
1443
|
+
"describe",
|
|
1444
|
+
"blame"
|
|
1445
|
+
]);
|
|
1446
|
+
function parseGitSubcommand(command) {
|
|
1447
|
+
const parts = command.trim().split(/\s+/);
|
|
1448
|
+
if (parts[0] !== "git" || parts.length < 2) {
|
|
1449
|
+
return void 0;
|
|
1450
|
+
}
|
|
1451
|
+
return parts[1];
|
|
1452
|
+
}
|
|
1453
|
+
function hasDangerousFlags(command) {
|
|
1454
|
+
const lowerCommand = command.toLowerCase();
|
|
1455
|
+
return DANGEROUS_FLAGS.some((flag) => {
|
|
1456
|
+
const pattern = new RegExp(`(^|\\s)${flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`);
|
|
1457
|
+
return pattern.test(lowerCommand);
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
function truncateOutput2(output) {
|
|
1461
|
+
if (output.length <= MAX_OUTPUT_LENGTH3) {
|
|
1462
|
+
return output;
|
|
1463
|
+
}
|
|
1464
|
+
return output.substring(0, MAX_OUTPUT_LENGTH3) + "\n...(truncated)";
|
|
1465
|
+
}
|
|
1466
|
+
var workingDirectory2 = process.cwd();
|
|
1467
|
+
function setGitWorkingDirectory(dir) {
|
|
1468
|
+
workingDirectory2 = dir;
|
|
1469
|
+
}
|
|
1470
|
+
function createGitTool() {
|
|
1471
|
+
return {
|
|
1472
|
+
definition: {
|
|
1473
|
+
name: "git",
|
|
1474
|
+
description: "Execute git commands safely. Supports status, diff, log, add, commit, branch operations. Never force pushes by default.",
|
|
1475
|
+
parameters: [
|
|
1476
|
+
{
|
|
1477
|
+
name: "command",
|
|
1478
|
+
type: "string",
|
|
1479
|
+
description: 'Full git command to execute (e.g. "git status", "git diff HEAD")',
|
|
1480
|
+
required: true
|
|
1481
|
+
}
|
|
1482
|
+
]
|
|
1483
|
+
},
|
|
1484
|
+
category: "git",
|
|
1485
|
+
requiresApproval: (mode, args) => {
|
|
1486
|
+
const command = typeof args["command"] === "string" ? args["command"] : "";
|
|
1487
|
+
const subcommand = parseGitSubcommand(command);
|
|
1488
|
+
if (hasDangerousFlags(command)) {
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
if (subcommand && READ_ONLY_SUBCOMMANDS.has(subcommand)) {
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
if (mode === "strict") {
|
|
1495
|
+
return true;
|
|
1496
|
+
}
|
|
1497
|
+
if (subcommand === "push" && mode === "standard") {
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
return false;
|
|
1501
|
+
},
|
|
1502
|
+
execute: async (args) => {
|
|
1503
|
+
const command = args["command"];
|
|
1504
|
+
if (typeof command !== "string" || command.length === 0) {
|
|
1505
|
+
return {
|
|
1506
|
+
toolCallId: "",
|
|
1507
|
+
name: "git",
|
|
1508
|
+
content: "command parameter is required and must be a non-empty string.",
|
|
1509
|
+
isError: true
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
const trimmedCommand = command.trim();
|
|
1513
|
+
if (!trimmedCommand.startsWith("git ")) {
|
|
1514
|
+
return {
|
|
1515
|
+
toolCallId: "",
|
|
1516
|
+
name: "git",
|
|
1517
|
+
content: 'Command must start with "git".',
|
|
1518
|
+
isError: true
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const subcommand = parseGitSubcommand(trimmedCommand);
|
|
1522
|
+
if (!subcommand || !ALLOWED_SUBCOMMANDS.has(subcommand)) {
|
|
1523
|
+
return {
|
|
1524
|
+
toolCallId: "",
|
|
1525
|
+
name: "git",
|
|
1526
|
+
content: `Git subcommand "${subcommand ?? "unknown"}" is not allowed. Allowed: ${[...ALLOWED_SUBCOMMANDS].join(", ")}`,
|
|
1527
|
+
isError: true
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
const lowerCommand = trimmedCommand.toLowerCase();
|
|
1531
|
+
if (subcommand === "push" && (lowerCommand.includes("--force") || lowerCommand.includes(" -f"))) {
|
|
1532
|
+
return {
|
|
1533
|
+
toolCallId: "",
|
|
1534
|
+
name: "git",
|
|
1535
|
+
content: "Force push is blocked by default. Use --force-with-lease if needed (requires approval).",
|
|
1536
|
+
isError: true
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
logger.debug({ command: trimmedCommand, subcommand, cwd: workingDirectory2 }, "Executing git command");
|
|
1540
|
+
try {
|
|
1541
|
+
const result = await execaCommand(trimmedCommand, {
|
|
1542
|
+
cwd: workingDirectory2,
|
|
1543
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1544
|
+
reject: false,
|
|
1545
|
+
env: {
|
|
1546
|
+
...process.env,
|
|
1547
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1548
|
+
GIT_PAGER: "cat"
|
|
1549
|
+
},
|
|
1550
|
+
stripFinalNewline: true
|
|
1551
|
+
});
|
|
1552
|
+
const stdout = result.stdout ? truncateOutput2(result.stdout) : "";
|
|
1553
|
+
const stderr = result.stderr ? truncateOutput2(result.stderr) : "";
|
|
1554
|
+
let content = "";
|
|
1555
|
+
if (stdout.length > 0) {
|
|
1556
|
+
content += stdout;
|
|
1557
|
+
}
|
|
1558
|
+
if (stderr.length > 0) {
|
|
1559
|
+
content += (content.length > 0 ? "\n\nSTDERR:\n" : "") + stderr;
|
|
1560
|
+
}
|
|
1561
|
+
if (content.length === 0) {
|
|
1562
|
+
content = `Git command completed with exit code ${result.exitCode ?? 0}.`;
|
|
1563
|
+
}
|
|
1564
|
+
const isError = (result.exitCode ?? 0) !== 0;
|
|
1565
|
+
return {
|
|
1566
|
+
toolCallId: "",
|
|
1567
|
+
name: "git",
|
|
1568
|
+
content,
|
|
1569
|
+
isError
|
|
1570
|
+
};
|
|
1571
|
+
} catch (err) {
|
|
1572
|
+
const msg = err instanceof Error ? err.message : "Git command failed";
|
|
1573
|
+
logger.error({ command: trimmedCommand, error: msg }, "Git execution failed");
|
|
1574
|
+
return {
|
|
1575
|
+
toolCallId: "",
|
|
1576
|
+
name: "git",
|
|
1577
|
+
content: `Git command failed: ${msg}`,
|
|
1578
|
+
isError: true
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/tools/index.ts
|
|
1586
|
+
function createDefaultRegistry(context) {
|
|
1587
|
+
setReadProjectRoot(context.projectRoot);
|
|
1588
|
+
setWriteProjectRoot(context.projectRoot);
|
|
1589
|
+
setEditProjectRoot(context.projectRoot);
|
|
1590
|
+
setGlobProjectRoot(context.projectRoot);
|
|
1591
|
+
setGrepProjectRoot(context.projectRoot);
|
|
1592
|
+
setBashWorkingDirectory(context.workingDirectory);
|
|
1593
|
+
setBashBlockedCommands(context.blockedCommands);
|
|
1594
|
+
setGitWorkingDirectory(context.workingDirectory);
|
|
1595
|
+
const registry = new ToolRegistry();
|
|
1596
|
+
registry.register(createReadTool());
|
|
1597
|
+
registry.register(createWriteTool());
|
|
1598
|
+
registry.register(createEditTool());
|
|
1599
|
+
registry.register(createGlobTool());
|
|
1600
|
+
registry.register(createGrepTool());
|
|
1601
|
+
registry.register(createBashTool());
|
|
1602
|
+
registry.register(createWebSearchTool());
|
|
1603
|
+
registry.register(createWebFetchTool());
|
|
1604
|
+
registry.register(createGitTool());
|
|
1605
|
+
return registry;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
export { ToolRegistry, createBashTool, createDefaultRegistry, createEditTool, createGitTool, createGlobTool, createGrepTool, createReadTool, createWebFetchTool, createWebSearchTool, createWriteTool, setBashBlockedCommands, setBashWorkingDirectory, setEditProjectRoot, setGitWorkingDirectory, setGlobProjectRoot, setGrepProjectRoot, setReadProjectRoot, setWebSearchProvider, setWriteProjectRoot };
|
|
1609
|
+
//# sourceMappingURL=chunk-Y5XVD2CD.js.map
|
|
1610
|
+
//# sourceMappingURL=chunk-Y5XVD2CD.js.map
|