@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.
Files changed (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +270 -0
  3. package/dist/cli/discover.js +527 -0
  4. package/dist/cli/mcptool.js +10339 -0
  5. package/dist/cli/opencode-cursor.js +2989 -0
  6. package/dist/index.js +20588 -0
  7. package/dist/plugin-entry.js +19848 -0
  8. package/package.json +82 -0
  9. package/scripts/cursor-agent-runner.mjs +272 -0
  10. package/scripts/sdk-runner.mjs +412 -0
  11. package/src/acp/metrics.ts +83 -0
  12. package/src/acp/sessions.ts +107 -0
  13. package/src/acp/tools.ts +209 -0
  14. package/src/auth.ts +175 -0
  15. package/src/cli/discover.ts +53 -0
  16. package/src/cli/mcptool.ts +133 -0
  17. package/src/cli/model-discovery.ts +71 -0
  18. package/src/cli/opencode-cursor.ts +1195 -0
  19. package/src/client/cursor-agent-child.ts +459 -0
  20. package/src/client/sdk-child.ts +550 -0
  21. package/src/client/simple.ts +293 -0
  22. package/src/commands/status.ts +39 -0
  23. package/src/index.ts +39 -0
  24. package/src/mcp/client-manager.ts +166 -0
  25. package/src/mcp/config.ts +169 -0
  26. package/src/mcp/tool-bridge.ts +133 -0
  27. package/src/models/config.ts +64 -0
  28. package/src/models/discovery.ts +105 -0
  29. package/src/models/index.ts +3 -0
  30. package/src/models/pricing.ts +196 -0
  31. package/src/models/sync.ts +247 -0
  32. package/src/models/types.ts +11 -0
  33. package/src/models/variants.ts +446 -0
  34. package/src/plugin-entry.ts +28 -0
  35. package/src/plugin-toggle.ts +81 -0
  36. package/src/plugin.ts +2802 -0
  37. package/src/provider/backend.ts +71 -0
  38. package/src/provider/boundary.ts +168 -0
  39. package/src/provider/passthrough-tracker.ts +38 -0
  40. package/src/provider/runtime-interception.ts +818 -0
  41. package/src/provider/tool-loop-guard.ts +644 -0
  42. package/src/provider/tool-schema-compat.ts +800 -0
  43. package/src/provider.ts +268 -0
  44. package/src/proxy/formatter.ts +60 -0
  45. package/src/proxy/handler.ts +29 -0
  46. package/src/proxy/incremental-prompt.ts +74 -0
  47. package/src/proxy/prompt-builder.ts +204 -0
  48. package/src/proxy/server.ts +207 -0
  49. package/src/proxy/session-resume.ts +312 -0
  50. package/src/proxy/tool-loop.ts +359 -0
  51. package/src/proxy/types.ts +13 -0
  52. package/src/services/toast-service.ts +81 -0
  53. package/src/streaming/ai-sdk-parts.ts +109 -0
  54. package/src/streaming/delta-tracker.ts +89 -0
  55. package/src/streaming/line-buffer.ts +44 -0
  56. package/src/streaming/openai-sse.ts +118 -0
  57. package/src/streaming/parser.ts +22 -0
  58. package/src/streaming/types.ts +158 -0
  59. package/src/tools/core/executor.ts +25 -0
  60. package/src/tools/core/registry.ts +27 -0
  61. package/src/tools/core/types.ts +31 -0
  62. package/src/tools/defaults.ts +954 -0
  63. package/src/tools/discovery.ts +140 -0
  64. package/src/tools/executors/cli.ts +59 -0
  65. package/src/tools/executors/local.ts +25 -0
  66. package/src/tools/executors/mcp.ts +39 -0
  67. package/src/tools/executors/sdk.ts +39 -0
  68. package/src/tools/index.ts +8 -0
  69. package/src/tools/registry.ts +34 -0
  70. package/src/tools/router.ts +123 -0
  71. package/src/tools/schema.ts +58 -0
  72. package/src/tools/skills/loader.ts +61 -0
  73. package/src/tools/skills/resolver.ts +21 -0
  74. package/src/tools/types.ts +29 -0
  75. package/src/types.ts +8 -0
  76. package/src/usage.ts +112 -0
  77. package/src/utils/binary.ts +71 -0
  78. package/src/utils/errors.ts +224 -0
  79. package/src/utils/logger.ts +191 -0
  80. 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
+ }