@evanovation/open-cursor 2.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +270 -0
- package/dist/cli/discover.js +527 -0
- package/dist/cli/mcptool.js +10339 -0
- package/dist/cli/opencode-cursor.js +2989 -0
- package/dist/index.js +20588 -0
- package/dist/plugin-entry.js +19848 -0
- package/package.json +82 -0
- package/scripts/cursor-agent-runner.mjs +272 -0
- package/scripts/sdk-runner.mjs +412 -0
- package/src/acp/metrics.ts +83 -0
- package/src/acp/sessions.ts +107 -0
- package/src/acp/tools.ts +209 -0
- package/src/auth.ts +175 -0
- package/src/cli/discover.ts +53 -0
- package/src/cli/mcptool.ts +133 -0
- package/src/cli/model-discovery.ts +71 -0
- package/src/cli/opencode-cursor.ts +1195 -0
- package/src/client/cursor-agent-child.ts +459 -0
- package/src/client/sdk-child.ts +550 -0
- package/src/client/simple.ts +293 -0
- package/src/commands/status.ts +39 -0
- package/src/index.ts +39 -0
- package/src/mcp/client-manager.ts +166 -0
- package/src/mcp/config.ts +169 -0
- package/src/mcp/tool-bridge.ts +133 -0
- package/src/models/config.ts +64 -0
- package/src/models/discovery.ts +105 -0
- package/src/models/index.ts +3 -0
- package/src/models/pricing.ts +196 -0
- package/src/models/sync.ts +247 -0
- package/src/models/types.ts +11 -0
- package/src/models/variants.ts +446 -0
- package/src/plugin-entry.ts +28 -0
- package/src/plugin-toggle.ts +81 -0
- package/src/plugin.ts +2802 -0
- package/src/provider/backend.ts +71 -0
- package/src/provider/boundary.ts +168 -0
- package/src/provider/passthrough-tracker.ts +38 -0
- package/src/provider/runtime-interception.ts +818 -0
- package/src/provider/tool-loop-guard.ts +644 -0
- package/src/provider/tool-schema-compat.ts +800 -0
- package/src/provider.ts +268 -0
- package/src/proxy/formatter.ts +60 -0
- package/src/proxy/handler.ts +29 -0
- package/src/proxy/incremental-prompt.ts +74 -0
- package/src/proxy/prompt-builder.ts +204 -0
- package/src/proxy/server.ts +207 -0
- package/src/proxy/session-resume.ts +312 -0
- package/src/proxy/tool-loop.ts +359 -0
- package/src/proxy/types.ts +13 -0
- package/src/services/toast-service.ts +81 -0
- package/src/streaming/ai-sdk-parts.ts +109 -0
- package/src/streaming/delta-tracker.ts +89 -0
- package/src/streaming/line-buffer.ts +44 -0
- package/src/streaming/openai-sse.ts +118 -0
- package/src/streaming/parser.ts +22 -0
- package/src/streaming/types.ts +158 -0
- package/src/tools/core/executor.ts +25 -0
- package/src/tools/core/registry.ts +27 -0
- package/src/tools/core/types.ts +31 -0
- package/src/tools/defaults.ts +954 -0
- package/src/tools/discovery.ts +140 -0
- package/src/tools/executors/cli.ts +59 -0
- package/src/tools/executors/local.ts +25 -0
- package/src/tools/executors/mcp.ts +39 -0
- package/src/tools/executors/sdk.ts +39 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/registry.ts +34 -0
- package/src/tools/router.ts +123 -0
- package/src/tools/schema.ts +58 -0
- package/src/tools/skills/loader.ts +61 -0
- package/src/tools/skills/resolver.ts +21 -0
- package/src/tools/types.ts +29 -0
- package/src/types.ts +8 -0
- package/src/usage.ts +112 -0
- package/src/utils/binary.ts +71 -0
- package/src/utils/errors.ts +224 -0
- package/src/utils/logger.ts +191 -0
- package/src/utils/perf.ts +76 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import type { ToolRegistry } from "./core/registry.js";
|
|
2
|
+
import { createLogger } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register default OpenCode tools in the registry
|
|
6
|
+
*/
|
|
7
|
+
export function registerDefaultTools(registry: ToolRegistry): void {
|
|
8
|
+
// 1. Bash tool - Execute shell commands
|
|
9
|
+
registry.register({
|
|
10
|
+
id: "bash",
|
|
11
|
+
name: "bash",
|
|
12
|
+
description: "Execute a shell command. Use this to run programs/tests; prefer write/edit for creating or modifying files.",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
command: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "The shell command to execute"
|
|
19
|
+
},
|
|
20
|
+
timeout: {
|
|
21
|
+
type: "number",
|
|
22
|
+
description: "Timeout in seconds (default: 30)"
|
|
23
|
+
},
|
|
24
|
+
cwd: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Working directory for the command"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
required: ["command"]
|
|
30
|
+
},
|
|
31
|
+
source: "local" as const
|
|
32
|
+
}, async (args) => {
|
|
33
|
+
const { spawn } = await import("child_process");
|
|
34
|
+
|
|
35
|
+
const command = resolveBashCommand(args);
|
|
36
|
+
if (!command) {
|
|
37
|
+
throw new Error("bash: missing required argument 'command'");
|
|
38
|
+
}
|
|
39
|
+
const timeoutMs = resolveTimeoutMs(args.timeout);
|
|
40
|
+
const cwd = resolveWorkingDirectory(args);
|
|
41
|
+
|
|
42
|
+
return new Promise<string>((resolve, reject) => {
|
|
43
|
+
const proc = spawn(command, {
|
|
44
|
+
shell: resolveShellOption(),
|
|
45
|
+
cwd,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const stdoutChunks: Buffer[] = [];
|
|
49
|
+
const stderrChunks: Buffer[] = [];
|
|
50
|
+
let timedOut = false;
|
|
51
|
+
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
timedOut = true;
|
|
54
|
+
proc.kill("SIGTERM");
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
|
|
57
|
+
proc.stdout.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
58
|
+
proc.stderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
59
|
+
|
|
60
|
+
proc.on("close", (code) => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
63
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
64
|
+
const output = stdout || stderr || "Command executed successfully";
|
|
65
|
+
if (timedOut) {
|
|
66
|
+
resolve(`Command timed out after ${timeoutMs / 1000}s\n${output}`);
|
|
67
|
+
} else if (code !== 0) {
|
|
68
|
+
resolve(`${output}\n[Exit code: ${code}]`);
|
|
69
|
+
} else {
|
|
70
|
+
resolve(output);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
proc.on("error", reject);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 2. Read tool - Read file contents
|
|
79
|
+
registry.register({
|
|
80
|
+
id: "read",
|
|
81
|
+
name: "read",
|
|
82
|
+
description: "Read the contents of a file",
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
path: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Absolute path to the file to read"
|
|
89
|
+
},
|
|
90
|
+
offset: {
|
|
91
|
+
type: "number",
|
|
92
|
+
description: "Line number to start reading from"
|
|
93
|
+
},
|
|
94
|
+
limit: {
|
|
95
|
+
type: "number",
|
|
96
|
+
description: "Maximum number of lines to read"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
required: ["path"]
|
|
100
|
+
},
|
|
101
|
+
source: "local" as const
|
|
102
|
+
}, async (args) => {
|
|
103
|
+
const fs = await import("fs");
|
|
104
|
+
try {
|
|
105
|
+
const path = args.path as string;
|
|
106
|
+
const offset = args.offset as number | undefined;
|
|
107
|
+
const limit = args.limit as number | undefined;
|
|
108
|
+
let content = fs.readFileSync(path, "utf-8");
|
|
109
|
+
|
|
110
|
+
if (offset !== undefined || limit !== undefined) {
|
|
111
|
+
const lines = content.split("\n");
|
|
112
|
+
const start = offset || 0;
|
|
113
|
+
const end = limit ? start + limit : lines.length;
|
|
114
|
+
content = lines.slice(start, end).join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return content;
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 3. Write tool - Write file contents
|
|
124
|
+
registry.register({
|
|
125
|
+
id: "write",
|
|
126
|
+
name: "write",
|
|
127
|
+
description: "Write content to a file (creates or overwrites). Prefer this over using bash redirection/heredocs for file creation.",
|
|
128
|
+
parameters: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
path: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Absolute path to the file to write"
|
|
134
|
+
},
|
|
135
|
+
content: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Content to write to the file"
|
|
138
|
+
},
|
|
139
|
+
force: {
|
|
140
|
+
type: "boolean",
|
|
141
|
+
description: "Set true only when intentionally replacing an existing file with complete content"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
required: ["path", "content"]
|
|
145
|
+
},
|
|
146
|
+
source: "local" as const
|
|
147
|
+
}, async (args) => {
|
|
148
|
+
const fs = await import("fs");
|
|
149
|
+
const path = await import("path");
|
|
150
|
+
try {
|
|
151
|
+
const filePath = args.path as string;
|
|
152
|
+
const content = args.content as string;
|
|
153
|
+
const force = args.force === true;
|
|
154
|
+
return writeFullFileWithOverwriteGuard(fs, path, filePath, content, force, "write");
|
|
155
|
+
} catch (error: any) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// 4. Edit tool - Edit file contents
|
|
161
|
+
registry.register({
|
|
162
|
+
id: "edit",
|
|
163
|
+
name: "edit",
|
|
164
|
+
description: "Edit a file by replacing old text with new text. Use for targeted replacements; use write to overwrite an entire file.",
|
|
165
|
+
parameters: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
path: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Absolute path to the file to edit"
|
|
171
|
+
},
|
|
172
|
+
old_string: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "The text to replace"
|
|
175
|
+
},
|
|
176
|
+
new_string: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "The replacement text"
|
|
179
|
+
},
|
|
180
|
+
content: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "Compatibility field for full-file content emitted by cursor-agent"
|
|
183
|
+
},
|
|
184
|
+
streamContent: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "Compatibility field for full-file content emitted by cursor-agent"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
required: ["path", "old_string", "new_string"]
|
|
190
|
+
},
|
|
191
|
+
source: "local" as const
|
|
192
|
+
}, async (args) => {
|
|
193
|
+
const fs = await import("fs");
|
|
194
|
+
const path = await import("path");
|
|
195
|
+
try {
|
|
196
|
+
const resolvedArgs = resolveEditArguments(args);
|
|
197
|
+
const filePath = resolvedArgs.path;
|
|
198
|
+
const oldString = resolvedArgs.old_string;
|
|
199
|
+
const newString = resolvedArgs.new_string;
|
|
200
|
+
if (!filePath) {
|
|
201
|
+
throw new Error("edit: missing required argument 'path'");
|
|
202
|
+
}
|
|
203
|
+
if (typeof oldString !== "string") {
|
|
204
|
+
const fullFileContent = coerceToString(args.streamContent ?? args.content);
|
|
205
|
+
if (fullFileContent !== null) {
|
|
206
|
+
return writeFullFileWithOverwriteGuard(fs, path, filePath, fullFileContent, args.force === true, "edit", 2);
|
|
207
|
+
}
|
|
208
|
+
throw new Error("edit: missing required argument 'old_string'");
|
|
209
|
+
}
|
|
210
|
+
if (oldString.length === 0) {
|
|
211
|
+
throw new Error("edit: old_string must not be empty; use write to overwrite an entire file");
|
|
212
|
+
}
|
|
213
|
+
if (typeof newString !== "string") {
|
|
214
|
+
throw new Error("edit: missing required argument 'new_string'");
|
|
215
|
+
}
|
|
216
|
+
let content = "";
|
|
217
|
+
try {
|
|
218
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
219
|
+
} catch (error: any) {
|
|
220
|
+
if (error?.code === "ENOENT") {
|
|
221
|
+
const dir = path.dirname(filePath);
|
|
222
|
+
if (!fs.existsSync(dir)) {
|
|
223
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
fs.writeFileSync(filePath, newString, "utf-8");
|
|
226
|
+
return `File did not exist. Created and wrote content: ${filePath}`;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!content.includes(oldString)) {
|
|
232
|
+
return `Error: Could not find the text to replace in ${filePath}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
content = content.replaceAll(oldString, newString);
|
|
236
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
237
|
+
|
|
238
|
+
return `File edited successfully: ${filePath}`;
|
|
239
|
+
} catch (error: any) {
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 5. Grep tool - Search file contents
|
|
245
|
+
registry.register({
|
|
246
|
+
id: "grep",
|
|
247
|
+
name: "grep",
|
|
248
|
+
description: "Search for a pattern in files",
|
|
249
|
+
parameters: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
pattern: {
|
|
253
|
+
type: "string",
|
|
254
|
+
description: "The search pattern (regex supported)"
|
|
255
|
+
},
|
|
256
|
+
path: {
|
|
257
|
+
type: "string",
|
|
258
|
+
description: "Directory or file to search in"
|
|
259
|
+
},
|
|
260
|
+
include: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "File pattern to include (e.g., '*.ts')"
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
required: ["pattern", "path"]
|
|
266
|
+
},
|
|
267
|
+
source: "local" as const
|
|
268
|
+
}, async (args) => {
|
|
269
|
+
const { execFile } = await import("child_process");
|
|
270
|
+
const { promisify } = await import("util");
|
|
271
|
+
const execFileAsync = promisify(execFile);
|
|
272
|
+
|
|
273
|
+
const pattern = args.pattern as string;
|
|
274
|
+
const path = args.path as string;
|
|
275
|
+
const include = args.include as string | undefined;
|
|
276
|
+
|
|
277
|
+
if (process.platform === "win32") {
|
|
278
|
+
return nodeFallbackGrep(pattern, path, include);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const grepArgs = ["-r", "-n"];
|
|
282
|
+
if (include) {
|
|
283
|
+
grepArgs.push(`--include=${include}`);
|
|
284
|
+
}
|
|
285
|
+
grepArgs.push("-e", pattern, path);
|
|
286
|
+
|
|
287
|
+
const runGrep = async (extraArgs: string[] = []) => {
|
|
288
|
+
return execFileAsync("grep", [...extraArgs, ...grepArgs], { timeout: 30000 });
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const { stdout } = await runGrep();
|
|
293
|
+
return stdout || "No matches found";
|
|
294
|
+
} catch (error: any) {
|
|
295
|
+
// grep exits with code 1 when no matches found — not an error
|
|
296
|
+
if (error.code === 1) {
|
|
297
|
+
return "No matches found";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
301
|
+
const isRegexSyntaxError = error.code === 2
|
|
302
|
+
&& /(invalid regular expression|invalid repetition count|braces not balanced|repetition-operator operand invalid|unmatched(\s*\\?\{)?)/i.test(stderr);
|
|
303
|
+
|
|
304
|
+
// BSD grep uses basic regex by default and can reject patterns that work in ERE.
|
|
305
|
+
// Retry with -E so patterns like \$\{[A-Z_][A-Z0-9_]*:- are handled.
|
|
306
|
+
if (isRegexSyntaxError) {
|
|
307
|
+
try {
|
|
308
|
+
const { stdout } = await runGrep(["-E"]);
|
|
309
|
+
return stdout || "No matches found";
|
|
310
|
+
} catch (extendedError: any) {
|
|
311
|
+
if (extendedError.code === 1) {
|
|
312
|
+
return "No matches found";
|
|
313
|
+
}
|
|
314
|
+
throw extendedError;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// 6. LS tool - List directory contents
|
|
323
|
+
registry.register({
|
|
324
|
+
id: "ls",
|
|
325
|
+
name: "ls",
|
|
326
|
+
description: "List directory contents",
|
|
327
|
+
parameters: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
path: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "Absolute path to the directory"
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
required: ["path"]
|
|
336
|
+
},
|
|
337
|
+
source: "local" as const
|
|
338
|
+
}, async (args) => {
|
|
339
|
+
const fs = await import("fs");
|
|
340
|
+
const path = await import("path");
|
|
341
|
+
try {
|
|
342
|
+
const dirPath = args.path as string;
|
|
343
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
344
|
+
|
|
345
|
+
const result = entries.map(entry => {
|
|
346
|
+
const type = entry.isDirectory() ? "d" :
|
|
347
|
+
entry.isSymbolicLink() ? "l" :
|
|
348
|
+
entry.isFile() ? "f" : "?";
|
|
349
|
+
return `[${type}] ${entry.name}`;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return result.join("\n") || "Empty directory";
|
|
353
|
+
} catch (error: any) {
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// 7. Glob tool - Find files matching pattern
|
|
359
|
+
registry.register({
|
|
360
|
+
id: "glob",
|
|
361
|
+
name: "glob",
|
|
362
|
+
description: "Find files matching a glob pattern",
|
|
363
|
+
parameters: {
|
|
364
|
+
type: "object",
|
|
365
|
+
properties: {
|
|
366
|
+
pattern: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Glob pattern (e.g., '**/*.ts')"
|
|
369
|
+
},
|
|
370
|
+
path: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "Directory to search in (default: current directory)"
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
required: ["pattern"]
|
|
376
|
+
},
|
|
377
|
+
source: "local" as const
|
|
378
|
+
}, async (args) => {
|
|
379
|
+
const { execFile } = await import("child_process");
|
|
380
|
+
const { promisify } = await import("util");
|
|
381
|
+
const execFileAsync = promisify(execFile);
|
|
382
|
+
|
|
383
|
+
const pattern = resolveGlobPattern(args);
|
|
384
|
+
if (!pattern) {
|
|
385
|
+
throw new Error("glob: missing required argument 'pattern'");
|
|
386
|
+
}
|
|
387
|
+
const path = resolvePathArg(args, "glob");
|
|
388
|
+
const cwd = path || ".";
|
|
389
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
390
|
+
|
|
391
|
+
if (process.platform === "win32") {
|
|
392
|
+
return nodeFallbackGlob(normalizedPattern, cwd);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const isPathPattern = normalizedPattern.includes("/");
|
|
396
|
+
const findArgs = [cwd, "-type", "f"];
|
|
397
|
+
if (isPathPattern) {
|
|
398
|
+
if (cwd === "." || cwd === "./") {
|
|
399
|
+
const dotPattern = normalizedPattern.startsWith("./")
|
|
400
|
+
? normalizedPattern
|
|
401
|
+
: `./${normalizedPattern}`;
|
|
402
|
+
findArgs.push("(", "-path", normalizedPattern, "-o", "-path", dotPattern, ")");
|
|
403
|
+
} else {
|
|
404
|
+
findArgs.push("-path", normalizedPattern);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
findArgs.push("-name", normalizedPattern);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const { stdout } = await execFileAsync("find", findArgs, { timeout: 30000 });
|
|
412
|
+
// Limit output to 50 lines (replaces piped `| head -50`)
|
|
413
|
+
const lines = (stdout || "").split("\n").filter(Boolean);
|
|
414
|
+
return lines.slice(0, 50).join("\n") || "No files found";
|
|
415
|
+
} catch (error: any) {
|
|
416
|
+
const stdout = typeof error?.stdout === "string" ? error.stdout : "";
|
|
417
|
+
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
418
|
+
// Permission-denied and "no results" scenarios from find should not be fatal.
|
|
419
|
+
if (error?.code === 1 || stderr.includes("Permission denied")) {
|
|
420
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
421
|
+
return lines.slice(0, 50).join("\n") || "No files found";
|
|
422
|
+
}
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// 8. Mkdir tool - Create directories
|
|
428
|
+
registry.register({
|
|
429
|
+
id: "mkdir",
|
|
430
|
+
name: "mkdir",
|
|
431
|
+
description: "Create a directory, including parent directories if needed",
|
|
432
|
+
parameters: {
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: {
|
|
435
|
+
path: {
|
|
436
|
+
type: "string",
|
|
437
|
+
description: "Directory path to create"
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
required: ["path"]
|
|
441
|
+
},
|
|
442
|
+
source: "local" as const
|
|
443
|
+
}, async (args) => {
|
|
444
|
+
const { mkdir } = await import("fs/promises");
|
|
445
|
+
const { resolve } = await import("path");
|
|
446
|
+
const rawPath = resolvePathArg(args, "mkdir");
|
|
447
|
+
if (!rawPath) {
|
|
448
|
+
throw new Error("mkdir: missing required argument 'path'");
|
|
449
|
+
}
|
|
450
|
+
const target = resolve(rawPath);
|
|
451
|
+
await mkdir(target, { recursive: true });
|
|
452
|
+
return `Created directory: ${target}`;
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// 9. Rm tool - Delete files/directories
|
|
456
|
+
registry.register({
|
|
457
|
+
id: "rm",
|
|
458
|
+
name: "rm",
|
|
459
|
+
description: "Delete a file or directory. Use force: true for non-empty directories.",
|
|
460
|
+
parameters: {
|
|
461
|
+
type: "object",
|
|
462
|
+
properties: {
|
|
463
|
+
path: {
|
|
464
|
+
type: "string",
|
|
465
|
+
description: "Path to delete"
|
|
466
|
+
},
|
|
467
|
+
force: {
|
|
468
|
+
type: "boolean",
|
|
469
|
+
description: "If true, recursively delete non-empty directories"
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
required: ["path"]
|
|
473
|
+
},
|
|
474
|
+
source: "local" as const
|
|
475
|
+
}, async (args) => {
|
|
476
|
+
const { rm, stat } = await import("fs/promises");
|
|
477
|
+
const { resolve } = await import("path");
|
|
478
|
+
const rawPath = resolvePathArg(args, "rm");
|
|
479
|
+
if (!rawPath) {
|
|
480
|
+
throw new Error("rm: missing required argument 'path'");
|
|
481
|
+
}
|
|
482
|
+
const target = resolve(rawPath);
|
|
483
|
+
const force = resolveBoolean(args.force, false);
|
|
484
|
+
const info = await stat(target);
|
|
485
|
+
if (info.isDirectory() && !force) {
|
|
486
|
+
throw new Error("Directory not empty. Use force: true to delete recursively.");
|
|
487
|
+
}
|
|
488
|
+
await rm(target, { recursive: force });
|
|
489
|
+
return `Deleted: ${target}`;
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// 10. Stat tool - Get file/directory metadata
|
|
493
|
+
registry.register({
|
|
494
|
+
id: "stat",
|
|
495
|
+
name: "stat",
|
|
496
|
+
description: "Get file or directory information: size, type, permissions, timestamps",
|
|
497
|
+
parameters: {
|
|
498
|
+
type: "object",
|
|
499
|
+
properties: {
|
|
500
|
+
path: {
|
|
501
|
+
type: "string",
|
|
502
|
+
description: "Path to inspect"
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
required: ["path"]
|
|
506
|
+
},
|
|
507
|
+
source: "local" as const
|
|
508
|
+
}, async (args) => {
|
|
509
|
+
const { stat } = await import("fs/promises");
|
|
510
|
+
const { resolve } = await import("path");
|
|
511
|
+
const rawPath = resolvePathArg(args, "stat");
|
|
512
|
+
if (!rawPath) {
|
|
513
|
+
throw new Error("stat: missing required argument 'path'");
|
|
514
|
+
}
|
|
515
|
+
const target = resolve(rawPath);
|
|
516
|
+
const info = await stat(target);
|
|
517
|
+
return JSON.stringify({
|
|
518
|
+
path: target,
|
|
519
|
+
type: info.isDirectory() ? "directory" : info.isFile() ? "file" : "other",
|
|
520
|
+
size: info.size,
|
|
521
|
+
mode: info.mode.toString(8),
|
|
522
|
+
modified: info.mtime.toISOString(),
|
|
523
|
+
created: info.birthtime.toISOString(),
|
|
524
|
+
}, null, 2);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function writeFullFileWithOverwriteGuard(
|
|
529
|
+
fs: typeof import("fs"),
|
|
530
|
+
path: typeof import("path"),
|
|
531
|
+
filePath: string,
|
|
532
|
+
content: string,
|
|
533
|
+
force: boolean,
|
|
534
|
+
toolName: string,
|
|
535
|
+
minimumExistingLines = 5,
|
|
536
|
+
): string {
|
|
537
|
+
const dir = path.dirname(filePath);
|
|
538
|
+
if (!fs.existsSync(dir)) {
|
|
539
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!force && fs.existsSync(filePath)) {
|
|
543
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
544
|
+
const suspicious = detectSuspiciousPartialOverwrite(existing, content, minimumExistingLines);
|
|
545
|
+
if (suspicious) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`${toolName}: refusing suspicious partial overwrite of existing file ${filePath} `
|
|
548
|
+
+ `(${suspicious.existingLines} lines -> ${suspicious.nextLines} lines). `
|
|
549
|
+
+ "write/edit full-file replacement overwrites the whole file; use edit with old_string/new_string "
|
|
550
|
+
+ "for targeted changes, or pass force: true only when intentionally replacing the full file.",
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
556
|
+
return `File written successfully: ${filePath}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function resolveEditArguments(args: Record<string, unknown>): {
|
|
560
|
+
path: string;
|
|
561
|
+
old_string: string | undefined;
|
|
562
|
+
new_string: string | undefined;
|
|
563
|
+
} {
|
|
564
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
565
|
+
let oldString = typeof args.old_string === "string" ? args.old_string : undefined;
|
|
566
|
+
let newString = typeof args.new_string === "string" ? args.new_string : undefined;
|
|
567
|
+
|
|
568
|
+
if (newString === undefined) {
|
|
569
|
+
const fallbackContent = coerceToString(args.content);
|
|
570
|
+
if (fallbackContent !== null) {
|
|
571
|
+
newString = fallbackContent;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
path,
|
|
577
|
+
old_string: oldString,
|
|
578
|
+
new_string: newString,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function detectSuspiciousPartialOverwrite(
|
|
583
|
+
existing: string,
|
|
584
|
+
next: string,
|
|
585
|
+
minimumExistingLines = 5,
|
|
586
|
+
): { existingLines: number; nextLines: number } | null {
|
|
587
|
+
if (process.env.CURSOR_ACP_WRITE_OVERWRITE_GUARD === "false") {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
if (existing.length === 0) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const existingLines = countLogicalLines(existing);
|
|
595
|
+
const nextLines = countLogicalLines(next);
|
|
596
|
+
if (existingLines < minimumExistingLines) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const lineShrink = nextLines <= Math.max(3, Math.floor(existingLines * 0.1));
|
|
601
|
+
const byteShrink = next.length <= Math.max(120, Math.floor(existing.length * 0.1));
|
|
602
|
+
return lineShrink && byteShrink ? { existingLines, nextLines } : null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function countLogicalLines(value: string): number {
|
|
606
|
+
if (value.length === 0) {
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
const withoutTrailingNewline = value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
610
|
+
if (withoutTrailingNewline.length === 0) {
|
|
611
|
+
return 1;
|
|
612
|
+
}
|
|
613
|
+
return withoutTrailingNewline.split("\n").length;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function resolveBashCommand(args: Record<string, unknown>): string | null {
|
|
617
|
+
const direct = coerceToString(args.command ?? args.cmd ?? args.script ?? args.input);
|
|
618
|
+
if (direct !== null && direct.trim().length > 0) {
|
|
619
|
+
return direct;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (Array.isArray(args.command)) {
|
|
623
|
+
const parts = args.command
|
|
624
|
+
.map((part) => coerceToString(part))
|
|
625
|
+
.filter((part): part is string => typeof part === "string" && part.trim().length > 0);
|
|
626
|
+
if (parts.length > 0) {
|
|
627
|
+
return parts.join(" ");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const commandObject = args.command;
|
|
632
|
+
if (typeof commandObject === "object" && commandObject !== null && !Array.isArray(commandObject)) {
|
|
633
|
+
const record = commandObject as Record<string, unknown>;
|
|
634
|
+
const base = coerceToString(record.command ?? record.cmd);
|
|
635
|
+
if (base !== null && base.trim().length > 0) {
|
|
636
|
+
if (Array.isArray(record.args)) {
|
|
637
|
+
const argParts = record.args
|
|
638
|
+
.map((entry) => coerceToString(entry))
|
|
639
|
+
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
|
|
640
|
+
return argParts.length > 0 ? `${base} ${argParts.join(" ")}` : base;
|
|
641
|
+
}
|
|
642
|
+
return base;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function resolveWorkingDirectory(args: Record<string, unknown>): string | undefined {
|
|
650
|
+
const cwd = coerceToString(args.cwd ?? args.workdir ?? args.path);
|
|
651
|
+
if (cwd !== null && cwd.trim().length > 0) {
|
|
652
|
+
return cwd;
|
|
653
|
+
}
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function resolveGlobPattern(args: Record<string, unknown>): string | null {
|
|
658
|
+
const direct = coerceToString(
|
|
659
|
+
args.pattern
|
|
660
|
+
?? args.globPattern
|
|
661
|
+
?? args.filePattern
|
|
662
|
+
?? args.searchPattern
|
|
663
|
+
?? args.includePattern,
|
|
664
|
+
);
|
|
665
|
+
if (direct !== null && direct.trim().length > 0) {
|
|
666
|
+
return direct;
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function resolvePathArg(args: Record<string, unknown>, toolName: string): string | null {
|
|
672
|
+
const value = coerceToString(
|
|
673
|
+
args.path
|
|
674
|
+
?? args.filePath
|
|
675
|
+
?? args.targetPath
|
|
676
|
+
?? args.directory
|
|
677
|
+
?? args.dir
|
|
678
|
+
?? args.folder
|
|
679
|
+
?? args.targetDirectory
|
|
680
|
+
?? args.targetFile,
|
|
681
|
+
);
|
|
682
|
+
if (value !== null && value.trim().length > 0) {
|
|
683
|
+
return value;
|
|
684
|
+
}
|
|
685
|
+
if (toolName === "glob") {
|
|
686
|
+
return ".";
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function resolveTimeout(value: unknown): number | undefined {
|
|
692
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
if (typeof value === "string") {
|
|
696
|
+
const parsed = Number(value.trim());
|
|
697
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
698
|
+
return parsed;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Convert model-supplied timeout (seconds) to milliseconds. Falls back to 30s.
|
|
705
|
+
function resolveTimeoutMs(value: unknown): number {
|
|
706
|
+
const raw = resolveTimeout(value);
|
|
707
|
+
if (raw === undefined) return 30_000;
|
|
708
|
+
// Values ≤ 600 are treated as seconds (no real use case for a <600ms shell timeout).
|
|
709
|
+
return raw <= 600 ? raw * 1000 : raw;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function resolveShellOption(deps: {
|
|
713
|
+
platform?: NodeJS.Platform;
|
|
714
|
+
env?: Record<string, string | undefined>;
|
|
715
|
+
} = {}): string | boolean {
|
|
716
|
+
const platform = deps.platform ?? process.platform;
|
|
717
|
+
const env = deps.env ?? process.env;
|
|
718
|
+
|
|
719
|
+
if (platform === "win32") {
|
|
720
|
+
return env.ComSpec || env.COMSPEC || true;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return env.SHELL || "/bin/bash";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function resolveBoolean(value: unknown, defaultValue: boolean): boolean {
|
|
727
|
+
if (typeof value === "boolean") {
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
if (typeof value === "number") {
|
|
731
|
+
return value !== 0;
|
|
732
|
+
}
|
|
733
|
+
if (typeof value === "string") {
|
|
734
|
+
const normalized = value.trim().toLowerCase();
|
|
735
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return defaultValue;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function coerceToString(value: unknown): string | null {
|
|
746
|
+
if (typeof value === "string") {
|
|
747
|
+
return value;
|
|
748
|
+
}
|
|
749
|
+
if (value === null || value === undefined) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
if (Array.isArray(value)) {
|
|
753
|
+
const parts: string[] = [];
|
|
754
|
+
for (const item of value) {
|
|
755
|
+
if (typeof item === "string") {
|
|
756
|
+
parts.push(item);
|
|
757
|
+
} else if (typeof item === "object" && item !== null) {
|
|
758
|
+
const record = item as Record<string, unknown>;
|
|
759
|
+
if (typeof record.text === "string") {
|
|
760
|
+
parts.push(record.text);
|
|
761
|
+
} else if (typeof record.content === "string") {
|
|
762
|
+
parts.push(record.content);
|
|
763
|
+
} else if (typeof record.value === "string") {
|
|
764
|
+
parts.push(record.value);
|
|
765
|
+
} else {
|
|
766
|
+
parts.push(JSON.stringify(record));
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
parts.push(String(item));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return parts.length > 0 ? parts.join("") : null;
|
|
773
|
+
}
|
|
774
|
+
if (typeof value === "object") {
|
|
775
|
+
const record = value as Record<string, unknown>;
|
|
776
|
+
if (typeof record.text === "string") {
|
|
777
|
+
return record.text;
|
|
778
|
+
}
|
|
779
|
+
if (typeof record.content === "string") {
|
|
780
|
+
return record.content;
|
|
781
|
+
}
|
|
782
|
+
if (typeof record.value === "string") {
|
|
783
|
+
return record.value;
|
|
784
|
+
}
|
|
785
|
+
return JSON.stringify(record);
|
|
786
|
+
}
|
|
787
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
788
|
+
return String(value);
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Get the names of all default tools
|
|
795
|
+
*/
|
|
796
|
+
export function getDefaultToolNames(): string[] {
|
|
797
|
+
return ["bash", "read", "write", "edit", "grep", "ls", "glob", "mkdir", "rm", "stat"];
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const FALLBACK_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build"]);
|
|
801
|
+
const fallbackLog = createLogger("tools:fallback");
|
|
802
|
+
|
|
803
|
+
export async function nodeFallbackGrep(
|
|
804
|
+
pattern: string,
|
|
805
|
+
searchPath: string,
|
|
806
|
+
include?: string,
|
|
807
|
+
): Promise<string> {
|
|
808
|
+
const fs = await import("fs/promises");
|
|
809
|
+
const path = await import("path");
|
|
810
|
+
|
|
811
|
+
let regex: RegExp;
|
|
812
|
+
try {
|
|
813
|
+
regex = new RegExp(pattern);
|
|
814
|
+
} catch {
|
|
815
|
+
return "Invalid regex pattern";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
let includeRegex: RegExp | undefined;
|
|
819
|
+
if (include) {
|
|
820
|
+
const incPattern = include.replace(/\./g, "\\.").replace(/\?/g, ".").replace(/\*/g, ".*");
|
|
821
|
+
includeRegex = new RegExp(`^${incPattern}$`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const results: string[] = [];
|
|
825
|
+
|
|
826
|
+
async function walk(dir: string): Promise<void> {
|
|
827
|
+
if (results.length >= 100) return;
|
|
828
|
+
let entries;
|
|
829
|
+
try {
|
|
830
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
831
|
+
} catch (err: any) {
|
|
832
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
833
|
+
fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
for (const entry of entries) {
|
|
838
|
+
if (results.length >= 100) return;
|
|
839
|
+
const fullPath = path.join(dir, entry.name);
|
|
840
|
+
if (entry.isDirectory()) {
|
|
841
|
+
if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
|
|
842
|
+
await walk(fullPath);
|
|
843
|
+
}
|
|
844
|
+
} else if (entry.isFile()) {
|
|
845
|
+
if (includeRegex && !includeRegex.test(entry.name)) continue;
|
|
846
|
+
let content: string;
|
|
847
|
+
try {
|
|
848
|
+
content = await fs.readFile(fullPath, "utf-8");
|
|
849
|
+
} catch (err: any) {
|
|
850
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
851
|
+
fallbackLog.error("Unexpected error reading file", { path: fullPath, code: err?.code, message: err?.message });
|
|
852
|
+
}
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const lines = content.split("\n");
|
|
856
|
+
for (let i = 0; i < lines.length; i++) {
|
|
857
|
+
if (regex.test(lines[i])) {
|
|
858
|
+
results.push(`${fullPath}:${i + 1}:${lines[i]}`);
|
|
859
|
+
if (results.length >= 100) break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let stat;
|
|
867
|
+
try {
|
|
868
|
+
stat = await fs.stat(searchPath);
|
|
869
|
+
} catch {
|
|
870
|
+
return "Path not found";
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (stat.isFile()) {
|
|
874
|
+
let content: string;
|
|
875
|
+
try {
|
|
876
|
+
content = await fs.readFile(searchPath, "utf-8");
|
|
877
|
+
} catch (err: any) {
|
|
878
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
879
|
+
fallbackLog.error("Unexpected error reading file", { path: searchPath, code: err?.code, message: err?.message });
|
|
880
|
+
}
|
|
881
|
+
return "Path not found";
|
|
882
|
+
}
|
|
883
|
+
const lines = content.split("\n");
|
|
884
|
+
for (let i = 0; i < lines.length; i++) {
|
|
885
|
+
if (regex.test(lines[i])) {
|
|
886
|
+
results.push(`${searchPath}:${i + 1}:${lines[i]}`);
|
|
887
|
+
if (results.length >= 100) break;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
} else {
|
|
891
|
+
await walk(searchPath);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return results.join("\n") || "No matches found";
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export async function nodeFallbackGlob(
|
|
898
|
+
pattern: string,
|
|
899
|
+
searchPath: string,
|
|
900
|
+
): Promise<string> {
|
|
901
|
+
const fs = await import("fs/promises");
|
|
902
|
+
const path = await import("path");
|
|
903
|
+
|
|
904
|
+
const results: string[] = [];
|
|
905
|
+
const isPathPattern = pattern.includes("/");
|
|
906
|
+
|
|
907
|
+
// Handle ** before * so double-star → .* and single-star → [^/]*
|
|
908
|
+
let regexPattern = pattern
|
|
909
|
+
.replace(/\./g, "\\.")
|
|
910
|
+
.replace(/\*\*/g, "\x00") // placeholder for **
|
|
911
|
+
.replace(/\*/g, "[^/]*")
|
|
912
|
+
.replace(/\x00/g, ".*"); // restore ** as .*
|
|
913
|
+
|
|
914
|
+
let regex: RegExp;
|
|
915
|
+
try {
|
|
916
|
+
regex = isPathPattern
|
|
917
|
+
? new RegExp(`${regexPattern}$`)
|
|
918
|
+
: new RegExp(`^${regexPattern}$`);
|
|
919
|
+
} catch {
|
|
920
|
+
return "No files found";
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async function walk(dir: string): Promise<void> {
|
|
924
|
+
if (results.length >= 50) return;
|
|
925
|
+
let entries;
|
|
926
|
+
try {
|
|
927
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
928
|
+
} catch (err: any) {
|
|
929
|
+
if (err?.code !== "ENOENT" && err?.code !== "EACCES") {
|
|
930
|
+
fallbackLog.error("Unexpected error reading directory", { dir, code: err?.code, message: err?.message });
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
for (const entry of entries) {
|
|
935
|
+
if (results.length >= 50) return;
|
|
936
|
+
const fullPath = path.join(dir, entry.name);
|
|
937
|
+
if (entry.isDirectory()) {
|
|
938
|
+
if (!FALLBACK_SKIP_DIRS.has(entry.name)) {
|
|
939
|
+
await walk(fullPath);
|
|
940
|
+
}
|
|
941
|
+
} else if (entry.isFile()) {
|
|
942
|
+
const matchTarget = isPathPattern
|
|
943
|
+
? fullPath.replace(/\\/g, "/")
|
|
944
|
+
: entry.name;
|
|
945
|
+
if (regex.test(matchTarget)) {
|
|
946
|
+
results.push(fullPath);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
await walk(searchPath);
|
|
953
|
+
return results.join("\n") || "No files found";
|
|
954
|
+
}
|