@wrongstack/tools 0.1.10 → 0.3.1

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 (74) hide show
  1. package/README.md +1 -1
  2. package/dist/audit.js +1 -0
  3. package/dist/audit.js.map +1 -1
  4. package/dist/bash.js +1 -0
  5. package/dist/bash.js.map +1 -1
  6. package/dist/batch-tool-use.js +2 -1
  7. package/dist/batch-tool-use.js.map +1 -1
  8. package/dist/builtin.js +161 -10
  9. package/dist/builtin.js.map +1 -1
  10. package/dist/diff.js +1 -0
  11. package/dist/diff.js.map +1 -1
  12. package/dist/document.js +1 -0
  13. package/dist/document.js.map +1 -1
  14. package/dist/edit.js +1 -0
  15. package/dist/edit.js.map +1 -1
  16. package/dist/exec.js +2 -1
  17. package/dist/exec.js.map +1 -1
  18. package/dist/fetch.js +1 -0
  19. package/dist/fetch.js.map +1 -1
  20. package/dist/format.js +1 -0
  21. package/dist/format.js.map +1 -1
  22. package/dist/git.js +1 -0
  23. package/dist/git.js.map +1 -1
  24. package/dist/glob.js +1 -0
  25. package/dist/glob.js.map +1 -1
  26. package/dist/grep.js +18 -1
  27. package/dist/grep.js.map +1 -1
  28. package/dist/index.d.ts +38 -1
  29. package/dist/index.js +172 -11
  30. package/dist/index.js.map +1 -1
  31. package/dist/install.js +1 -0
  32. package/dist/install.js.map +1 -1
  33. package/dist/json.js +1 -0
  34. package/dist/json.js.map +1 -1
  35. package/dist/lint.js +1 -0
  36. package/dist/lint.js.map +1 -1
  37. package/dist/logs.js +1 -0
  38. package/dist/logs.js.map +1 -1
  39. package/dist/memory.js +2 -0
  40. package/dist/memory.js.map +1 -1
  41. package/dist/mode.js +1 -0
  42. package/dist/mode.js.map +1 -1
  43. package/dist/outdated.js +1 -0
  44. package/dist/outdated.js.map +1 -1
  45. package/dist/pack.d.ts +9 -0
  46. package/dist/pack.js +4216 -0
  47. package/dist/pack.js.map +1 -0
  48. package/dist/patch.js +1 -0
  49. package/dist/patch.js.map +1 -1
  50. package/dist/read.js +1 -0
  51. package/dist/read.js.map +1 -1
  52. package/dist/replace.js +12 -5
  53. package/dist/replace.js.map +1 -1
  54. package/dist/scaffold.js +1 -0
  55. package/dist/scaffold.js.map +1 -1
  56. package/dist/search.js +1 -0
  57. package/dist/search.js.map +1 -1
  58. package/dist/test.js +1 -0
  59. package/dist/test.js.map +1 -1
  60. package/dist/todo.js +1 -0
  61. package/dist/todo.js.map +1 -1
  62. package/dist/tool-help.js +1 -0
  63. package/dist/tool-help.js.map +1 -1
  64. package/dist/tool-search.js +1 -0
  65. package/dist/tool-search.js.map +1 -1
  66. package/dist/tool-use.js +2 -1
  67. package/dist/tool-use.js.map +1 -1
  68. package/dist/tree.js +1 -0
  69. package/dist/tree.js.map +1 -1
  70. package/dist/typecheck.js +1 -0
  71. package/dist/typecheck.js.map +1 -1
  72. package/dist/write.js +1 -0
  73. package/dist/write.js.map +1 -1
  74. package/package.json +6 -2
package/dist/pack.js ADDED
@@ -0,0 +1,4216 @@
1
+ import { spawn } from 'child_process';
2
+ import { buildChildEnv, stripAnsi, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, removePlanItem, setPlanItemStatus, addPlanItem, formatPlan } from '@wrongstack/core';
3
+ import * as path from 'path';
4
+ import { dirname } from 'path';
5
+ import * as os from 'os';
6
+ import { statSync } from 'fs';
7
+ import * as fs9 from 'fs/promises';
8
+ import * as dns from 'dns/promises';
9
+ import * as net from 'net';
10
+
11
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
13
+ }) : x)(function(x) {
14
+ if (typeof require !== "undefined") return require.apply(this, arguments);
15
+ throw Error('Dynamic require of "' + x + '" is not supported');
16
+ });
17
+ async function* spawnStream(opts) {
18
+ const max = opts.maxBytes ?? 2e5;
19
+ const flushAt = opts.flushBytes ?? 4 * 1024;
20
+ let stdout = "";
21
+ let stderr = "";
22
+ let pending = "";
23
+ let error;
24
+ const child = spawn(opts.cmd, opts.args, {
25
+ cwd: opts.cwd,
26
+ signal: opts.signal,
27
+ env: buildChildEnv(),
28
+ stdio: ["ignore", "pipe", "pipe"]
29
+ });
30
+ const queue = [];
31
+ let waiter;
32
+ const wake = () => {
33
+ if (waiter) {
34
+ const w = waiter;
35
+ waiter = void 0;
36
+ w();
37
+ }
38
+ };
39
+ child.stdout?.on("data", (c) => {
40
+ const s = c.toString();
41
+ if (stdout.length < max) stdout += s;
42
+ queue.push({ kind: "out", data: s });
43
+ wake();
44
+ });
45
+ child.stderr?.on("data", (c) => {
46
+ const s = c.toString();
47
+ if (stderr.length < max) stderr += s;
48
+ queue.push({ kind: "err", data: s });
49
+ wake();
50
+ });
51
+ child.on("error", (e) => {
52
+ error = e.message;
53
+ queue.push({ kind: "error", data: e.message });
54
+ wake();
55
+ });
56
+ child.on("close", (code) => {
57
+ queue.push({ kind: "close", data: "", code: code ?? 0 });
58
+ wake();
59
+ });
60
+ let exitCode = 0;
61
+ let spawnFailed = false;
62
+ for (; ; ) {
63
+ while (queue.length === 0) {
64
+ await new Promise((resolve4) => {
65
+ waiter = resolve4;
66
+ });
67
+ }
68
+ const chunk = queue.shift();
69
+ if (chunk.kind === "close") {
70
+ if (!spawnFailed) exitCode = chunk.code ?? 0;
71
+ break;
72
+ }
73
+ if (chunk.kind === "error") {
74
+ spawnFailed = true;
75
+ exitCode = 1;
76
+ continue;
77
+ }
78
+ pending += chunk.data;
79
+ if (pending.length >= flushAt) {
80
+ yield { type: "partial_output", text: pending };
81
+ pending = "";
82
+ }
83
+ }
84
+ if (pending.length > 0) {
85
+ yield { type: "partial_output", text: pending };
86
+ }
87
+ return {
88
+ stdout,
89
+ stderr,
90
+ exitCode,
91
+ truncated: stdout.length >= max || stderr.length >= max,
92
+ error
93
+ };
94
+ }
95
+ function resolvePath(input, ctx) {
96
+ return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
97
+ }
98
+ function ensureInsideRoot(absPath, ctx) {
99
+ const root = path.resolve(ctx.projectRoot);
100
+ const target = path.resolve(absPath);
101
+ const rel = path.relative(root, target);
102
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
103
+ throw new Error(`Path "${absPath}" is outside project root "${root}"`);
104
+ }
105
+ return target;
106
+ }
107
+ function safeResolve(input, ctx) {
108
+ return ensureInsideRoot(resolvePath(input, ctx), ctx);
109
+ }
110
+ function truncateMiddle(s, max) {
111
+ if (Buffer.byteLength(s, "utf8") <= max) return s;
112
+ const half = Math.floor(max / 2);
113
+ return s.slice(0, half) + `
114
+ \u2026[truncated ${Buffer.byteLength(s, "utf8") - max} bytes from middle]\u2026
115
+ ` + s.slice(-half);
116
+ }
117
+ function isBinaryBuffer(buf) {
118
+ const len = Math.min(buf.length, 8192);
119
+ for (let i = 0; i < len; i++) {
120
+ if (buf[i] === 0) return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ // src/audit.ts
126
+ var auditTool = {
127
+ name: "audit",
128
+ category: "Package Management",
129
+ description: "Run npm/pnpm security audit. Returns vulnerabilities sorted by severity.",
130
+ usageHint: "Set `level` to filter minimum severity. `fix` attempts auto-fix. `packages` checks specific packages.",
131
+ permission: "confirm",
132
+ mutating: false,
133
+ timeoutMs: 6e4,
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
138
+ level: {
139
+ type: "string",
140
+ enum: ["low", "moderate", "high", "critical"],
141
+ description: "Minimum severity level to report"
142
+ },
143
+ fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
144
+ packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
145
+ }
146
+ },
147
+ async execute(input, ctx, opts) {
148
+ let final;
149
+ for await (const ev of auditTool.executeStream(input, ctx, opts)) {
150
+ if (ev.type === "final") final = ev.output;
151
+ }
152
+ if (!final) throw new Error("audit: stream ended without final event");
153
+ return final;
154
+ },
155
+ async *executeStream(input, ctx, opts) {
156
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
157
+ const manager = await detectManager(cwd);
158
+ yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
159
+ const args = ["audit", "--json"];
160
+ if (input.fix) args.push("--fix");
161
+ if (input.packages) {
162
+ const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
163
+ args.push(...pkgs.map((p) => p.trim()));
164
+ }
165
+ const result = yield* spawnStream({
166
+ cmd: manager,
167
+ args,
168
+ cwd,
169
+ signal: opts.signal,
170
+ maxBytes: 1e5
171
+ });
172
+ yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
173
+ }
174
+ };
175
+ async function detectManager(cwd) {
176
+ const { stat: stat9 } = await import('fs/promises');
177
+ try {
178
+ await stat9(`${cwd}/pnpm-lock.yaml`);
179
+ return "pnpm";
180
+ } catch {
181
+ }
182
+ try {
183
+ await stat9(`${cwd}/yarn.lock`);
184
+ return "yarn";
185
+ } catch {
186
+ }
187
+ return "npm";
188
+ }
189
+ function parseAuditOutput(json, exitCode) {
190
+ if (!json) {
191
+ return {
192
+ exit_code: exitCode,
193
+ vulnerabilities: [],
194
+ total: 0,
195
+ summary: exitCode === 0 ? "No vulnerabilities found" : "Audit failed",
196
+ output: "",
197
+ truncated: false
198
+ };
199
+ }
200
+ try {
201
+ const data = JSON.parse(json);
202
+ const advisories = [];
203
+ const ads = data.advisories ?? {};
204
+ for (const id of Object.keys(ads)) {
205
+ const adv = ads[id];
206
+ advisories.push({
207
+ severity: adv.severity ?? "unknown",
208
+ package: adv.module_name ?? id,
209
+ title: adv.title ?? "Unknown vulnerability",
210
+ url: adv.url ?? ""
211
+ });
212
+ }
213
+ const total = advisories.length;
214
+ const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
215
+ return {
216
+ exit_code: exitCode,
217
+ vulnerabilities: advisories,
218
+ total,
219
+ summary,
220
+ output: json,
221
+ truncated: json.length >= 1e5
222
+ };
223
+ } catch {
224
+ return {
225
+ exit_code: exitCode,
226
+ vulnerabilities: [],
227
+ total: 0,
228
+ summary: "Could not parse audit output",
229
+ output: json,
230
+ truncated: false
231
+ };
232
+ }
233
+ }
234
+
235
+ // src/bash.ts
236
+ var MAX_OUTPUT = 32768;
237
+ var DEFAULT_TIMEOUT = 3e4;
238
+ var STREAM_FLUSH_INTERVAL_MS = 200;
239
+ var STREAM_FLUSH_BYTES = 4 * 1024;
240
+ var bashTool = {
241
+ name: "bash",
242
+ category: "Shell",
243
+ description: "Run a shell command. stdout and stderr are merged.",
244
+ usageHint: "Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.",
245
+ permission: "confirm",
246
+ mutating: true,
247
+ // Trust rules match on the literal `command` string. Without subjectKey
248
+ // the policy heuristic would have done the same here, but declaring it
249
+ // explicitly removes the implicit cross-tool aliasing.
250
+ subjectKey: "command",
251
+ timeoutMs: 3e4,
252
+ maxOutputBytes: MAX_OUTPUT,
253
+ estimatedDurationMs: 3e3,
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ command: { type: "string" },
258
+ timeout_ms: { type: "integer" },
259
+ background: { type: "boolean" }
260
+ },
261
+ required: ["command"]
262
+ },
263
+ async execute(input, ctx, opts) {
264
+ let final;
265
+ for await (const ev of bashTool.executeStream(input, ctx, opts)) {
266
+ if (ev.type === "final") final = ev.output;
267
+ }
268
+ if (!final) throw new Error("bash: stream ended without final event");
269
+ return final;
270
+ },
271
+ async *executeStream(input, ctx, opts) {
272
+ if (!input?.command) throw new Error("bash: command is required");
273
+ const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 6e5));
274
+ const isWin = os.platform() === "win32";
275
+ const shell = isWin ? process.env["COMSPEC"] ?? "cmd.exe" : process.env["SHELL"] ?? "/bin/bash";
276
+ const args = isWin ? ["/c", input.command] : ["-c", input.command];
277
+ const env = buildChildEnv(ctx.session?.id);
278
+ const detached = isWin ? !!input.background : true;
279
+ const child = spawn(shell, args, {
280
+ cwd: ctx.projectRoot,
281
+ env,
282
+ stdio: input.background ? "ignore" : ["ignore", "pipe", "pipe"],
283
+ detached,
284
+ signal: opts.signal
285
+ });
286
+ if (input.background) {
287
+ const pid = child.pid;
288
+ if (typeof pid === "number") child.unref();
289
+ yield {
290
+ type: "final",
291
+ output: {
292
+ output: `[background] pid=${pid ?? "unknown"}`,
293
+ exit_code: null,
294
+ timed_out: false,
295
+ pid
296
+ }
297
+ };
298
+ return;
299
+ }
300
+ let buf = "";
301
+ let pending = "";
302
+ let timedOut = false;
303
+ const timers = [];
304
+ const timer = setTimeout(() => {
305
+ timedOut = true;
306
+ if (isWin) {
307
+ try {
308
+ child.kill();
309
+ } catch {
310
+ }
311
+ } else {
312
+ try {
313
+ if (typeof child.pid === "number") {
314
+ try {
315
+ process.kill(-child.pid, "SIGTERM");
316
+ } catch {
317
+ child.kill("SIGTERM");
318
+ }
319
+ } else {
320
+ child.kill("SIGTERM");
321
+ }
322
+ const killTimer = setTimeout(() => {
323
+ try {
324
+ if (typeof child.pid === "number") {
325
+ try {
326
+ process.kill(-child.pid, "SIGKILL");
327
+ } catch {
328
+ child.kill("SIGKILL");
329
+ }
330
+ } else {
331
+ child.kill("SIGKILL");
332
+ }
333
+ } catch {
334
+ }
335
+ }, 2e3);
336
+ timers.push(killTimer);
337
+ } catch {
338
+ }
339
+ }
340
+ }, timeoutMs);
341
+ timers.push(timer);
342
+ timer.unref?.();
343
+ const queue = [];
344
+ let resolveNext = null;
345
+ const push = (c) => {
346
+ if (resolveNext) {
347
+ const r = resolveNext;
348
+ resolveNext = null;
349
+ r(c);
350
+ } else {
351
+ queue.push(c);
352
+ }
353
+ };
354
+ const next = () => new Promise((resolve4) => {
355
+ const c = queue.shift();
356
+ if (c) resolve4(c);
357
+ else resolveNext = resolve4;
358
+ });
359
+ let lastFlush = Date.now();
360
+ const flush = () => {
361
+ if (pending.length === 0) return null;
362
+ const text = pending;
363
+ pending = "";
364
+ lastFlush = Date.now();
365
+ return text;
366
+ };
367
+ child.stdout?.on("data", (chunk) => {
368
+ const text = chunk.toString();
369
+ buf += text;
370
+ pending += text;
371
+ push({ kind: "data", text });
372
+ });
373
+ child.stderr?.on("data", (chunk) => {
374
+ const text = chunk.toString();
375
+ buf += text;
376
+ pending += text;
377
+ push({ kind: "data", text });
378
+ });
379
+ child.on("error", (err) => {
380
+ for (const t of timers) clearTimeout(t);
381
+ push({ kind: "error", err });
382
+ });
383
+ child.on("close", (code) => {
384
+ for (const t of timers) clearTimeout(t);
385
+ push({ kind: "end", code });
386
+ });
387
+ try {
388
+ while (true) {
389
+ const c = await next();
390
+ if (c.kind === "error") throw c.err;
391
+ if (c.kind === "end") {
392
+ const remainder = flush();
393
+ if (remainder !== null) {
394
+ yield { type: "partial_output", text: remainder };
395
+ }
396
+ const cleaned = stripAnsi(buf).replace(/\r\n?/g, "\n");
397
+ yield {
398
+ type: "final",
399
+ output: {
400
+ output: truncateMiddle(cleaned, MAX_OUTPUT),
401
+ exit_code: c.code,
402
+ timed_out: timedOut
403
+ }
404
+ };
405
+ return;
406
+ }
407
+ const now = Date.now();
408
+ if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
409
+ const text = flush();
410
+ if (text) yield { type: "partial_output", text };
411
+ }
412
+ }
413
+ } finally {
414
+ for (const t of timers) clearTimeout(t);
415
+ }
416
+ }
417
+ };
418
+
419
+ // src/batch-tool-use.ts
420
+ var batchToolUseTool = {
421
+ name: "batch_tool_use",
422
+ category: "Meta",
423
+ description: "Execute multiple tool calls in sequence or parallel. Returns all results.",
424
+ usageHint: "Set `calls` array with tool names and inputs. `stop_on_error` halts on first failure. `parallel` runs concurrently (default: true).",
425
+ permission: "confirm",
426
+ mutating: true,
427
+ timeoutMs: 12e4,
428
+ inputSchema: {
429
+ type: "object",
430
+ properties: {
431
+ calls: {
432
+ type: "array",
433
+ items: {
434
+ type: "object",
435
+ properties: {
436
+ tool: { type: "string" },
437
+ input: { type: "object" }
438
+ },
439
+ required: ["tool"]
440
+ },
441
+ description: "Array of tool calls to execute"
442
+ },
443
+ stop_on_error: {
444
+ type: "boolean",
445
+ description: "Stop execution on first error (default: false)"
446
+ },
447
+ parallel: {
448
+ type: "boolean",
449
+ description: "Execute calls in parallel (default: true)"
450
+ }
451
+ },
452
+ required: ["calls"]
453
+ },
454
+ async execute(input, ctx, opts) {
455
+ if (!input?.calls || input.calls.length === 0) {
456
+ return {
457
+ results: [],
458
+ total: 0,
459
+ succeeded: 0,
460
+ failed: 0,
461
+ stop_on_error: false
462
+ };
463
+ }
464
+ const results = [];
465
+ let succeeded = 0;
466
+ let failed = 0;
467
+ if (input.parallel !== false) {
468
+ const promises = input.calls.map(async (call) => executeSingle(call, ctx, opts));
469
+ const allResults = await Promise.all(promises);
470
+ results.push(...allResults);
471
+ succeeded = allResults.filter((r) => r.success).length;
472
+ failed = allResults.filter((r) => !r.success).length;
473
+ } else {
474
+ for (const call of input.calls) {
475
+ const result = await executeSingle(call, ctx, opts);
476
+ results.push(result);
477
+ if (result.success) {
478
+ succeeded++;
479
+ } else {
480
+ failed++;
481
+ if (input.stop_on_error) break;
482
+ }
483
+ }
484
+ }
485
+ return {
486
+ results,
487
+ total: input.calls.length,
488
+ succeeded,
489
+ failed,
490
+ stop_on_error: input.stop_on_error ?? false
491
+ };
492
+ }
493
+ };
494
+ async function executeSingle(call, ctx, opts) {
495
+ const start = Date.now();
496
+ const tool = ctx.tools.find((t) => t.name === call.tool);
497
+ if (!tool) {
498
+ return {
499
+ tool: call.tool,
500
+ success: false,
501
+ error: `tool "${call.tool}" not found`,
502
+ executionMs: Date.now() - start
503
+ };
504
+ }
505
+ try {
506
+ const result = await tool.execute(call.input, ctx, opts);
507
+ return {
508
+ tool: call.tool,
509
+ success: true,
510
+ result,
511
+ executionMs: Date.now() - start
512
+ };
513
+ } catch (e) {
514
+ return {
515
+ tool: call.tool,
516
+ success: false,
517
+ error: e instanceof Error ? e.message : String(e),
518
+ executionMs: Date.now() - start
519
+ };
520
+ }
521
+ }
522
+ var diffTool = {
523
+ name: "diff",
524
+ category: "Filesystem",
525
+ description: "Show differences between files, commits, or branches. Supports staged vs working tree.",
526
+ usageHint: "Use `files` for file paths, `a`/`b` for commit refs, `staged` for git index. `mode`: unified (default), stat, side-by-side.",
527
+ permission: "auto",
528
+ mutating: false,
529
+ timeoutMs: 1e4,
530
+ inputSchema: {
531
+ type: "object",
532
+ properties: {
533
+ path: { type: "string", description: "Working directory for diff" },
534
+ files: {
535
+ type: "string",
536
+ description: 'File(s) to diff: single path, comma-separated, or "**/*.ts" glob'
537
+ },
538
+ a: { type: "string", description: "First commit/branch/ref (for git diff)" },
539
+ b: { type: "string", description: "Second commit/branch/ref (for git diff)" },
540
+ staged: { type: "boolean", description: "Diff staged changes only" },
541
+ mode: {
542
+ type: "string",
543
+ enum: ["unified", "side-by-side", "stat"],
544
+ description: "Output mode (default: unified)"
545
+ },
546
+ context: { type: "integer", description: "Context lines for unified diff (default: 3)" }
547
+ }
548
+ },
549
+ async execute(input, ctx, opts) {
550
+ if (input.a !== void 0 || input.b !== void 0) {
551
+ return await gitDiff(input, ctx, opts.signal);
552
+ }
553
+ return await fileDiff(input, ctx, opts.signal);
554
+ }
555
+ };
556
+ async function gitDiff(input, ctx, signal) {
557
+ const gitDir = findGitDir(ctx.cwd);
558
+ if (!gitDir) {
559
+ return { diff: "", files: [], truncated: false, mode: "unified" };
560
+ }
561
+ const args = ["diff", "--no-color"];
562
+ if (input.staged) args.push("--staged");
563
+ if (input.a) args.push(input.a);
564
+ if (input.b) args.push(input.b);
565
+ if (input.files) {
566
+ const files = Array.isArray(input.files) ? input.files : input.files.split(",");
567
+ args.push("--", ...files.map((f) => f.trim()));
568
+ }
569
+ const result = await runGit(args, gitDir, signal);
570
+ return {
571
+ diff: result.stdout,
572
+ files: [],
573
+ truncated: result.stdout.length > 1e5,
574
+ mode: "unified"
575
+ };
576
+ }
577
+ function findGitDir(cwd) {
578
+ let dir = cwd;
579
+ for (let i = 0; i < 20; i++) {
580
+ try {
581
+ const stat9 = statSync(path.join(dir, ".git"));
582
+ if (stat9.isDirectory()) return dir;
583
+ } catch {
584
+ }
585
+ const parent = path.dirname(dir);
586
+ if (parent === dir) break;
587
+ dir = parent;
588
+ }
589
+ return null;
590
+ }
591
+ function runGit(args, cwd, signal) {
592
+ return new Promise((resolve4) => {
593
+ let stdout = "";
594
+ let stderr = "";
595
+ const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
596
+ child.stdout?.on("data", (c) => {
597
+ stdout += c.toString();
598
+ });
599
+ child.stderr?.on("data", (c) => {
600
+ stderr += c.toString();
601
+ });
602
+ child.on("close", (code) => resolve4({ stdout, stderr, exitCode: code ?? 0 }));
603
+ child.on("error", (e) => resolve4({ stdout: "", stderr: e.message, exitCode: 1 }));
604
+ });
605
+ }
606
+ async function fileDiff(input, ctx, signal) {
607
+ input.path ? safeResolve(input.path, ctx) : ctx.cwd;
608
+ input.context ?? 3;
609
+ const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()).filter(Boolean) : [];
610
+ if (files.length === 0) {
611
+ return {
612
+ diff: "No files specified",
613
+ files: [],
614
+ truncated: false,
615
+ mode: input.mode ?? "unified"
616
+ };
617
+ }
618
+ const results = [];
619
+ for (const file of files) {
620
+ const absPath = safeResolve(file, ctx);
621
+ const stat9 = await fs9.stat(absPath).catch(() => null);
622
+ if (!stat9?.isFile()) continue;
623
+ const content = await fs9.readFile(absPath, "utf8");
624
+ const lines = content.split(/\r?\n/);
625
+ results.push(`--- ${file}
626
+ +++ ${file}
627
+ ${formatUnified(lines)}`);
628
+ }
629
+ return {
630
+ diff: results.join("\n"),
631
+ files,
632
+ truncated: false,
633
+ mode: input.mode ?? "unified"
634
+ };
635
+ }
636
+ function formatUnified(lines, context) {
637
+ return lines.map((line, i) => ` ${line}`).join("\n");
638
+ }
639
+ var documentTool = {
640
+ name: "document",
641
+ category: "Project",
642
+ description: "Generate or update documentation comments for functions, classes, and types. Supports JSDoc, TSDoc, and block comments.",
643
+ usageHint: "Set `target` for what to document. `files` for paths. `style` for comment format. `overwrite` replaces existing docs.",
644
+ permission: "confirm",
645
+ mutating: true,
646
+ timeoutMs: 3e4,
647
+ inputSchema: {
648
+ type: "object",
649
+ properties: {
650
+ target: {
651
+ type: "string",
652
+ enum: ["file", "function", "class", "type", "all"],
653
+ description: "What to document"
654
+ },
655
+ path: {
656
+ type: "string",
657
+ description: "Specific file path to document"
658
+ },
659
+ files: {
660
+ type: "string",
661
+ description: "File(s) to process: single path, comma-separated list, or glob"
662
+ },
663
+ style: {
664
+ type: "string",
665
+ enum: ["jsdoc", "tsdoc", "block"],
666
+ description: "Documentation style (default: jsdoc)"
667
+ },
668
+ overwrite: {
669
+ type: "boolean",
670
+ description: "Overwrite existing docstrings (default: false)"
671
+ },
672
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
673
+ }
674
+ },
675
+ async execute(input, ctx) {
676
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
677
+ const style = input.style ?? "jsdoc";
678
+ const results = [];
679
+ let filesProcessed = 0;
680
+ let itemsDocumented = 0;
681
+ const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
682
+ for (const absPath of fileList) {
683
+ try {
684
+ const content = await fs9.readFile(absPath, "utf8");
685
+ filesProcessed++;
686
+ const processed = processFile(
687
+ content,
688
+ absPath,
689
+ style,
690
+ input.overwrite ?? false,
691
+ input.target ?? "all"
692
+ );
693
+ results.push(...processed);
694
+ itemsDocumented += processed.filter((r) => r.status === "documented").length;
695
+ } catch (e) {
696
+ results.push({
697
+ path: absPath,
698
+ name: absPath.split("/").pop() ?? absPath,
699
+ signature: "",
700
+ docstring: "",
701
+ status: "error",
702
+ error: e instanceof Error ? e.message : String(e)
703
+ });
704
+ }
705
+ }
706
+ return {
707
+ files_processed: filesProcessed,
708
+ items_documented: itemsDocumented,
709
+ results,
710
+ style
711
+ };
712
+ }
713
+ };
714
+ async function resolveFiles(filesInput, cwd) {
715
+ const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
716
+ const resolved = [];
717
+ for (const f of files) {
718
+ const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
719
+ try {
720
+ const stat9 = await fs9.stat(absPath);
721
+ if (stat9.isFile()) resolved.push(absPath);
722
+ } catch {
723
+ }
724
+ }
725
+ return resolved;
726
+ }
727
+ function processFile(content, absPath, style, overwrite, target) {
728
+ const results = [];
729
+ content.split("\n");
730
+ const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
731
+ const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
732
+ const classRegex = /class\s+(\w+)/g;
733
+ const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
734
+ const allMatches = [];
735
+ if (target === "all" || target === "function") {
736
+ for (const m of content.matchAll(functionRegex)) {
737
+ if (!m[1]) continue;
738
+ allMatches.push({
739
+ name: m[1],
740
+ sig: m[2] ?? "",
741
+ type: "function",
742
+ line: content.slice(0, m.index).split("\n").length
743
+ });
744
+ }
745
+ for (const m of content.matchAll(arrowRegex)) {
746
+ if (!m[1]) continue;
747
+ allMatches.push({
748
+ name: m[1],
749
+ sig: m[2] ?? "",
750
+ type: "arrow",
751
+ line: content.slice(0, m.index).split("\n").length
752
+ });
753
+ }
754
+ }
755
+ if (target === "all" || target === "class") {
756
+ for (const m of content.matchAll(classRegex)) {
757
+ if (!m[1]) continue;
758
+ allMatches.push({
759
+ name: m[1],
760
+ sig: "",
761
+ type: "class",
762
+ line: content.slice(0, m.index).split("\n").length
763
+ });
764
+ }
765
+ }
766
+ if (target === "all" || target === "type") {
767
+ for (const m of content.matchAll(typeRegex)) {
768
+ if (!m[1]) continue;
769
+ allMatches.push({
770
+ name: m[1],
771
+ sig: m[0] ?? "",
772
+ type: "type",
773
+ line: content.slice(0, m.index).split("\n").length
774
+ });
775
+ }
776
+ }
777
+ for (const m of allMatches) {
778
+ results.push({
779
+ path: absPath,
780
+ name: m.name,
781
+ signature: m.sig,
782
+ docstring: `/** ${m.name} - documented at line ${m.line} */`,
783
+ status: "skipped"
784
+ });
785
+ }
786
+ return results;
787
+ }
788
+ var editTool = {
789
+ name: "edit",
790
+ category: "Filesystem",
791
+ description: "Make a surgical edit by replacing exact text. Fails if `old_string` is not unique unless `replace_all` is true.",
792
+ usageHint: "Always `read` the file first. `old_string` must be an EXACT match (whitespace included). If multiple matches exist, either narrow `old_string` with more context or set `replace_all: true`.",
793
+ permission: "confirm",
794
+ mutating: true,
795
+ timeoutMs: 5e3,
796
+ inputSchema: {
797
+ type: "object",
798
+ properties: {
799
+ path: { type: "string" },
800
+ old_string: { type: "string" },
801
+ new_string: { type: "string" },
802
+ replace_all: { type: "boolean" }
803
+ },
804
+ required: ["path", "old_string", "new_string"]
805
+ },
806
+ async execute(input, ctx) {
807
+ if (!input?.path) throw new Error("edit: path is required");
808
+ if (input.old_string === void 0) throw new Error("edit: old_string is required");
809
+ if (input.new_string === void 0) throw new Error("edit: new_string is required");
810
+ if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
811
+ const absPath = safeResolve(input.path, ctx);
812
+ const stat9 = await fs9.stat(absPath).catch((err) => {
813
+ if (err.code === "ENOENT") {
814
+ throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
815
+ }
816
+ throw err;
817
+ });
818
+ if (!stat9.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
819
+ if (!ctx.hasRead(absPath)) {
820
+ throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
821
+ }
822
+ const lastReadMtime = ctx.lastReadMtime(absPath);
823
+ const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
824
+ if (lastReadMtime !== void 0 && stat9.mtimeMs > lastReadMtime + mtimeTolerance) {
825
+ throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
826
+ }
827
+ const original = await fs9.readFile(absPath, "utf8");
828
+ const style = detectNewlineStyle(original);
829
+ const fileLf = normalizeToLf(original);
830
+ const oldLf = normalizeToLf(input.old_string);
831
+ const newLf = normalizeToLf(input.new_string);
832
+ if (oldLf === newLf) {
833
+ return {
834
+ path: absPath,
835
+ replacements: 0,
836
+ diff: "(no-op: old and new are identical)"
837
+ };
838
+ }
839
+ let count = 0;
840
+ let idx = fileLf.indexOf(oldLf);
841
+ const matches = [];
842
+ while (idx !== -1) {
843
+ matches.push(idx);
844
+ count++;
845
+ idx = fileLf.indexOf(oldLf, idx + 1);
846
+ }
847
+ if (count === 0) {
848
+ const hint = findSimilarity(fileLf, oldLf);
849
+ throw new Error(
850
+ `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`
851
+ );
852
+ }
853
+ if (count > 1 && !input.replace_all) {
854
+ const lines = lineNumbersFor(fileLf, matches);
855
+ throw new Error(
856
+ `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`
857
+ );
858
+ }
859
+ const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
860
+ const newFile = toStyle(newFileLf, style);
861
+ await atomicWrite(absPath, newFile, { mode: stat9.mode & 511 });
862
+ const updated = await fs9.stat(absPath);
863
+ ctx.recordRead(absPath, updated.mtimeMs);
864
+ const diff = unifiedDiff(original, newFile, {
865
+ fromFile: input.path,
866
+ toFile: input.path
867
+ });
868
+ return {
869
+ path: absPath,
870
+ replacements: input.replace_all ? count : 1,
871
+ diff
872
+ };
873
+ }
874
+ };
875
+ function lineNumbersFor(text, indices) {
876
+ const out = [];
877
+ let pos = 0;
878
+ let line = 1;
879
+ for (const target of indices) {
880
+ while (pos < target) {
881
+ if (text.charCodeAt(pos) === 10) line++;
882
+ pos++;
883
+ }
884
+ out.push(line);
885
+ }
886
+ return out;
887
+ }
888
+ function findSimilarity(haystack, needle) {
889
+ if (needle.length < 20) return void 0;
890
+ const probe = needle.slice(0, Math.min(40, needle.length));
891
+ const idx = haystack.indexOf(probe);
892
+ if (idx === -1) return void 0;
893
+ let line = 1;
894
+ for (let i = 0; i < idx; i++) {
895
+ if (haystack.charCodeAt(i) === 10) line++;
896
+ }
897
+ return line;
898
+ }
899
+ var ALLOWED_COMMANDS = {
900
+ node: ["--version", "-e", "-p", "-r", "--input-type=module"],
901
+ npm: ["--version", "init", "install", "test", "run", "list", "pkg", "doctor"],
902
+ pnpm: ["--version", "init", "install", "add", "remove", "exec", "list", "run", "dlx"],
903
+ npx: ["--version"],
904
+ git: [
905
+ "--version",
906
+ "status",
907
+ "log",
908
+ "diff",
909
+ "branch",
910
+ "checkout",
911
+ "stash",
912
+ "add",
913
+ "commit",
914
+ "push",
915
+ "pull"
916
+ ],
917
+ ls: ["-la", "-l", "-a"],
918
+ cat: [],
919
+ head: ["-n"],
920
+ tail: ["-n"],
921
+ wc: ["-l", "-w", "-c"],
922
+ grep: [],
923
+ find: [],
924
+ echo: [],
925
+ mkdir: ["-p"],
926
+ cp: ["-r"],
927
+ mv: [],
928
+ rm: ["-rf"],
929
+ touch: [],
930
+ bun: ["--version", "run", "add", "init"],
931
+ tsc: ["--version", "--noEmit", "--project"],
932
+ vitest: ["--version", "run", "--coverage"],
933
+ biome: ["--version", "lint", "format", "check"],
934
+ cargo: ["--version", "build", "test", "check"],
935
+ rustc: ["--version"],
936
+ go: ["version", "run", "build", "test"],
937
+ python: ["--version", "-c"],
938
+ pip: ["--version", "install", "list"],
939
+ docker: ["--version", "ps", "images", "build"],
940
+ kubectl: ["version", "get", "describe", "logs"]
941
+ };
942
+ var MAX_ARGS = 20;
943
+ var MAX_OUTPUT2 = 2e5;
944
+ var TIMEOUT_MS = 3e4;
945
+ var execTool = {
946
+ name: "exec",
947
+ category: "Shell",
948
+ description: "Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.",
949
+ usageHint: "Set `command` (must be in allowlist). `args` passed through. For arbitrary shell access use the `bash` tool instead.",
950
+ permission: "confirm",
951
+ mutating: true,
952
+ timeoutMs: TIMEOUT_MS,
953
+ inputSchema: {
954
+ type: "object",
955
+ properties: {
956
+ command: { type: "string", description: "Command to run (must be in allowlist)" },
957
+ args: { type: "array", items: { type: "string" }, description: "Arguments" },
958
+ cwd: { type: "string", description: "Working directory (must resolve inside project root)" },
959
+ timeout: { type: "integer", description: "Timeout in ms (default: 30000)" }
960
+ },
961
+ required: ["command"]
962
+ },
963
+ async execute(input, ctx, opts) {
964
+ const cmd = input.command.trim();
965
+ if (!cmd)
966
+ return {
967
+ command: cmd,
968
+ args: [],
969
+ stdout: "",
970
+ stderr: "Empty command",
971
+ exitCode: 1,
972
+ truncated: false,
973
+ allowed: false
974
+ };
975
+ if (!(cmd in ALLOWED_COMMANDS)) {
976
+ return {
977
+ command: cmd,
978
+ args: input.args ?? [],
979
+ stdout: "",
980
+ stderr: `Command "${cmd}" not in allowlist. Use the bash tool for arbitrary commands.`,
981
+ exitCode: 1,
982
+ truncated: false,
983
+ allowed: false
984
+ };
985
+ }
986
+ const args = (input.args ?? []).slice(0, MAX_ARGS);
987
+ const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));
988
+ const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
989
+ const rel = path.relative(ctx.projectRoot, requestedCwd);
990
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
991
+ return {
992
+ command: cmd,
993
+ args,
994
+ stdout: "",
995
+ stderr: `cwd "${input.cwd}" resolves outside project root`,
996
+ exitCode: 1,
997
+ truncated: false,
998
+ allowed: false
999
+ };
1000
+ }
1001
+ const cwd = requestedCwd;
1002
+ const signal = opts.signal;
1003
+ return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);
1004
+ }
1005
+ };
1006
+ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1007
+ return new Promise((resolve4) => {
1008
+ let stdout = "";
1009
+ let stderr = "";
1010
+ let killed = false;
1011
+ const child = spawn(cmd, args, {
1012
+ cwd,
1013
+ signal,
1014
+ env: buildChildEnv(sessionId),
1015
+ stdio: ["ignore", "pipe", "pipe"]
1016
+ });
1017
+ const timer = setTimeout(() => {
1018
+ killed = true;
1019
+ child.kill("SIGTERM");
1020
+ }, timeout);
1021
+ child.stdout?.on("data", (chunk) => {
1022
+ if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
1023
+ });
1024
+ child.stderr?.on("data", (chunk) => {
1025
+ if (stderr.length < MAX_OUTPUT2) stderr += chunk.toString();
1026
+ });
1027
+ child.on("close", (code) => {
1028
+ clearTimeout(timer);
1029
+ resolve4({
1030
+ command: cmd,
1031
+ args,
1032
+ stdout: stdout.slice(0, MAX_OUTPUT2),
1033
+ stderr: stderr.slice(0, MAX_OUTPUT2),
1034
+ exitCode: killed ? 124 : code ?? 1,
1035
+ truncated: stdout.length >= MAX_OUTPUT2 || stderr.length >= MAX_OUTPUT2,
1036
+ allowed: true
1037
+ });
1038
+ });
1039
+ child.on("error", (err) => {
1040
+ clearTimeout(timer);
1041
+ resolve4({
1042
+ command: cmd,
1043
+ args,
1044
+ stdout: stdout.slice(0, MAX_OUTPUT2),
1045
+ stderr: err.message,
1046
+ exitCode: 1,
1047
+ truncated: false,
1048
+ allowed: true
1049
+ });
1050
+ });
1051
+ });
1052
+ }
1053
+ var MAX_BYTES = 131072;
1054
+ var TIMEOUT_MS2 = 2e4;
1055
+ var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
1056
+ async function fetchWithRedirectLimit(url, maxRedirects, signal) {
1057
+ const headers = {
1058
+ "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
1059
+ accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
1060
+ };
1061
+ let redirectCount = 0;
1062
+ let currentUrl = url;
1063
+ for (; ; ) {
1064
+ const parsed = new URL(currentUrl);
1065
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
1066
+ throw new Error(`fetch: redirect to unsupported protocol "${parsed.protocol}"`);
1067
+ }
1068
+ if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
1069
+ throw new Error("fetch: redirect to http:// blocked (HTTPS required by default)");
1070
+ }
1071
+ await assertNotPrivate(parsed.hostname);
1072
+ const res = await fetch(currentUrl, {
1073
+ redirect: "manual",
1074
+ signal,
1075
+ headers
1076
+ });
1077
+ if (res.status < 300 || res.status > 399) {
1078
+ return res;
1079
+ }
1080
+ redirectCount++;
1081
+ if (redirectCount > maxRedirects) {
1082
+ throw new Error(`fetch: exceeded ${maxRedirects} redirects`);
1083
+ }
1084
+ const location = res.headers.get("location");
1085
+ if (!location) {
1086
+ throw new Error("fetch: redirect status with no location header");
1087
+ }
1088
+ currentUrl = new URL(location, currentUrl).toString();
1089
+ }
1090
+ }
1091
+ var fetchTool = {
1092
+ name: "fetch",
1093
+ category: "Network",
1094
+ description: "Fetch the contents of a URL. HTML is converted to markdown by default.",
1095
+ usageHint: "HTTPS only by default. Localhost and RFC1918 ranges blocked unless WRONGSTACK_FETCH_ALLOW_PRIVATE=1. Max 5 redirects, 20s timeout, 128KB cap.",
1096
+ permission: "confirm",
1097
+ mutating: false,
1098
+ // Trust rules for fetch match on the literal URL — declare it explicitly
1099
+ // so a user can trust `https://api.example.com/*` without accidentally
1100
+ // matching that pattern on any other tool that happens to have a `url`
1101
+ // input field.
1102
+ subjectKey: "url",
1103
+ timeoutMs: TIMEOUT_MS2,
1104
+ maxOutputBytes: MAX_BYTES,
1105
+ inputSchema: {
1106
+ type: "object",
1107
+ properties: {
1108
+ url: { type: "string" },
1109
+ format: { type: "string", enum: ["markdown", "text", "raw"] }
1110
+ },
1111
+ required: ["url"]
1112
+ },
1113
+ async execute(input, ctx, opts) {
1114
+ let final;
1115
+ for await (const ev of fetchTool.executeStream(input, ctx, opts)) {
1116
+ if (ev.type === "final") final = ev.output;
1117
+ }
1118
+ if (!final) throw new Error("fetch: stream ended without final event");
1119
+ return final;
1120
+ },
1121
+ async *executeStream(input, _ctx, opts) {
1122
+ if (!input?.url) throw new Error("fetch: url is required");
1123
+ const u = new URL(input.url);
1124
+ if (u.protocol !== "https:" && u.protocol !== "http:") {
1125
+ throw new Error(`fetch: unsupported protocol "${u.protocol}"`);
1126
+ }
1127
+ if (u.protocol === "http:" && !ALLOW_PRIVATE) {
1128
+ throw new Error("fetch: http:// blocked (HTTPS required by default)");
1129
+ }
1130
+ await assertNotPrivate(u.hostname);
1131
+ yield { type: "log", text: `GET ${input.url}` };
1132
+ const ctrl = new AbortController();
1133
+ const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS2);
1134
+ const combined = combineSignals(opts.signal, ctrl.signal);
1135
+ try {
1136
+ const res = await fetchWithRedirectLimit(input.url, 5, combined);
1137
+ const ct = res.headers.get("content-type") ?? "application/octet-stream";
1138
+ if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
1139
+ throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
1140
+ }
1141
+ yield {
1142
+ type: "log",
1143
+ text: `HTTP ${res.status} ${ct}`,
1144
+ data: { status: res.status, contentType: ct }
1145
+ };
1146
+ const reader = res.body?.getReader();
1147
+ let received = 0;
1148
+ const chunks = [];
1149
+ let pendingBytes = 0;
1150
+ const FLUSH_AT = 4 * 1024;
1151
+ if (reader) {
1152
+ for (; ; ) {
1153
+ const { value, done } = await reader.read();
1154
+ if (done) break;
1155
+ if (!value) continue;
1156
+ received += value.byteLength;
1157
+ pendingBytes += value.byteLength;
1158
+ chunks.push(value);
1159
+ if (pendingBytes >= FLUSH_AT) {
1160
+ const recent = Buffer.from(value).toString("utf8");
1161
+ yield {
1162
+ type: "partial_output",
1163
+ text: recent,
1164
+ data: { received }
1165
+ };
1166
+ pendingBytes = 0;
1167
+ }
1168
+ if (received > MAX_BYTES) break;
1169
+ }
1170
+ }
1171
+ const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
1172
+ const format = input.format ?? (ct.includes("text/html") ? "markdown" : "text");
1173
+ let content;
1174
+ if (format === "raw") content = text;
1175
+ else if (format === "markdown" && ct.includes("text/html")) content = htmlToMarkdown(text);
1176
+ else if (ct.includes("application/json")) content = prettyJson(text);
1177
+ else content = text;
1178
+ yield {
1179
+ type: "final",
1180
+ output: {
1181
+ content: truncateMiddle(content, MAX_BYTES),
1182
+ status: res.status,
1183
+ content_type: ct,
1184
+ url: res.url
1185
+ }
1186
+ };
1187
+ } finally {
1188
+ clearTimeout(timer);
1189
+ }
1190
+ }
1191
+ };
1192
+ async function assertNotPrivate(hostname) {
1193
+ if (ALLOW_PRIVATE) return;
1194
+ const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1195
+ if (host === "localhost" || host.endsWith(".localhost")) {
1196
+ throw new Error("fetch: blocked localhost target");
1197
+ }
1198
+ const ipVersion = net.isIP(host);
1199
+ if (ipVersion === 4) {
1200
+ if (isPrivateIPv4(host)) {
1201
+ throw new Error(`fetch: blocked private/loopback address "${host}"`);
1202
+ }
1203
+ } else if (ipVersion === 6) {
1204
+ if (isPrivateIPv6(host)) {
1205
+ throw new Error(`fetch: blocked private/loopback address "${host}"`);
1206
+ }
1207
+ } else {
1208
+ try {
1209
+ const records = await dns.lookup(host, { all: true });
1210
+ for (const r of records) {
1211
+ const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
1212
+ if (bad) {
1213
+ throw new Error(`fetch: resolved to private address ${r.address}`);
1214
+ }
1215
+ }
1216
+ } catch (err) {
1217
+ if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
1218
+ }
1219
+ }
1220
+ }
1221
+ function isPrivateIPv4(addr) {
1222
+ const parts = addr.split(".").map((p) => Number.parseInt(p, 10));
1223
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
1224
+ return true;
1225
+ }
1226
+ const [a, b, c] = parts;
1227
+ if (a === 0) return true;
1228
+ if (a === 10) return true;
1229
+ if (a === 127) return true;
1230
+ if (a === 169 && b === 254) return true;
1231
+ if (a === 172 && b >= 16 && b <= 31) return true;
1232
+ if (a === 192 && b === 168) return true;
1233
+ if (a === 192 && b === 0 && c === 0) return true;
1234
+ if (a === 100 && b >= 64 && b <= 127) return true;
1235
+ if (a >= 224) return true;
1236
+ return false;
1237
+ }
1238
+ function isPrivateIPv6(addr) {
1239
+ const lower = addr.toLowerCase();
1240
+ if (lower === "::" || lower === "::1") return true;
1241
+ const groups = expandIPv6(lower);
1242
+ if (!groups) return true;
1243
+ if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
1244
+ const a = (groups[6] ?? 0) >> 8;
1245
+ const b = (groups[6] ?? 0) & 255;
1246
+ const c = (groups[7] ?? 0) >> 8;
1247
+ const d = (groups[7] ?? 0) & 255;
1248
+ return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
1249
+ }
1250
+ const high = groups[0] ?? 0;
1251
+ if ((high & 65024) === 64512) return true;
1252
+ if ((high & 65472) === 65152) return true;
1253
+ if ((high & 65280) === 65280) return true;
1254
+ return false;
1255
+ }
1256
+ function expandIPv6(addr) {
1257
+ const parts = addr.split("::");
1258
+ if (parts.length > 2) return null;
1259
+ const parseGroups = (s) => {
1260
+ if (s === "") return [];
1261
+ const out = [];
1262
+ for (const g of s.split(":")) {
1263
+ if (g.length === 0 || g.length > 4) return null;
1264
+ const n = Number.parseInt(g, 16);
1265
+ if (Number.isNaN(n) || n < 0 || n > 65535) return null;
1266
+ out.push(n);
1267
+ }
1268
+ return out;
1269
+ };
1270
+ if (parts.length === 1) {
1271
+ const groups = parseGroups(parts[0] ?? "");
1272
+ if (!groups || groups.length !== 8) return null;
1273
+ return groups;
1274
+ }
1275
+ const head = parseGroups(parts[0] ?? "");
1276
+ const tail = parseGroups(parts[1] ?? "");
1277
+ if (!head || !tail) return null;
1278
+ const fill = 8 - head.length - tail.length;
1279
+ if (fill < 0) return null;
1280
+ return [...head, ...new Array(fill).fill(0), ...tail];
1281
+ }
1282
+ function combineSignals(...sigs) {
1283
+ if (typeof AbortSignal.any === "function") {
1284
+ return AbortSignal.any(sigs);
1285
+ }
1286
+ const ctrl = new AbortController();
1287
+ const cleanups = [];
1288
+ const detach = () => {
1289
+ for (const fn of cleanups) fn();
1290
+ cleanups.length = 0;
1291
+ };
1292
+ for (const s of sigs) {
1293
+ if (s.aborted) {
1294
+ detach();
1295
+ ctrl.abort(s.reason);
1296
+ return ctrl.signal;
1297
+ }
1298
+ const onAbort = () => {
1299
+ detach();
1300
+ ctrl.abort(s.reason);
1301
+ };
1302
+ s.addEventListener("abort", onAbort, { once: true });
1303
+ cleanups.push(() => s.removeEventListener("abort", onAbort));
1304
+ }
1305
+ ctrl.signal.addEventListener("abort", detach, { once: true });
1306
+ return ctrl.signal;
1307
+ }
1308
+ function prettyJson(s) {
1309
+ try {
1310
+ return JSON.stringify(JSON.parse(s), null, 2);
1311
+ } catch {
1312
+ return s;
1313
+ }
1314
+ }
1315
+ function htmlToMarkdown(html) {
1316
+ let s = html;
1317
+ s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
1318
+ s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
1319
+ s = s.replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
1320
+ s = s.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, n, c) => {
1321
+ return "\n" + "#".repeat(Number(n)) + " " + stripTags(c).trim() + "\n";
1322
+ });
1323
+ s = s.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**");
1324
+ s = s.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*");
1325
+ s = s.replace(/<a [^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
1326
+ s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_m, c) => "\n```\n" + stripTags(c) + "\n```\n");
1327
+ s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
1328
+ s = s.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n");
1329
+ s = s.replace(/<br\s*\/?>/gi, "\n");
1330
+ s = s.replace(/<\/p>/gi, "\n\n");
1331
+ s = stripTags(s);
1332
+ s = s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ");
1333
+ return s.replace(/\n{3,}/g, "\n\n").trim();
1334
+ }
1335
+ function stripTags(s) {
1336
+ return s.replace(/<[^>]+>/g, "");
1337
+ }
1338
+
1339
+ // src/format.ts
1340
+ var formatTool = {
1341
+ name: "format",
1342
+ category: "Code Quality",
1343
+ description: "Format files with biome or prettier. Use `check` to verify without modifying.",
1344
+ usageHint: "Set `files` (glob or comma-separated). `check` only validates. `fixer` forces tool.",
1345
+ permission: "confirm",
1346
+ mutating: true,
1347
+ timeoutMs: 6e4,
1348
+ inputSchema: {
1349
+ type: "object",
1350
+ properties: {
1351
+ files: {
1352
+ type: "string",
1353
+ description: "Files/patterns: single path, comma-separated list, or glob"
1354
+ },
1355
+ fixer: {
1356
+ type: "string",
1357
+ enum: ["biome", "prettier", "auto"],
1358
+ description: "Formatter to use (default: auto-detect)"
1359
+ },
1360
+ check: {
1361
+ type: "boolean",
1362
+ description: "Verify only, do not modify files (default: false)"
1363
+ },
1364
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
1365
+ }
1366
+ },
1367
+ async execute(input, ctx, opts) {
1368
+ let final;
1369
+ for await (const ev of formatTool.executeStream(input, ctx, opts)) {
1370
+ if (ev.type === "final") final = ev.output;
1371
+ }
1372
+ if (!final) throw new Error("format: stream ended without final event");
1373
+ return final;
1374
+ },
1375
+ async *executeStream(input, ctx, opts) {
1376
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
1377
+ const fixer = input.fixer ?? "auto";
1378
+ const detected = fixer === "auto" ? await detectFixer(cwd) : fixer;
1379
+ if (!detected) {
1380
+ yield {
1381
+ type: "final",
1382
+ output: {
1383
+ fixer: "none",
1384
+ files_checked: 0,
1385
+ files_changed: 0,
1386
+ output: "No formatter found (biome.json, .prettierrc)",
1387
+ truncated: false
1388
+ }
1389
+ };
1390
+ return;
1391
+ }
1392
+ yield {
1393
+ type: "log",
1394
+ text: `Running ${detected}\u2026`,
1395
+ data: { fixer: detected, check: !!input.check }
1396
+ };
1397
+ const args = ["format", "--write"];
1398
+ if (input.check) args[args.length - 1] = "--check";
1399
+ if (input.files) {
1400
+ const files = Array.isArray(input.files) ? input.files : input.files.split(",");
1401
+ args.push("--", ...files.map((f) => f.trim()));
1402
+ }
1403
+ const result = yield* spawnStream({
1404
+ cmd: detected,
1405
+ args,
1406
+ cwd,
1407
+ signal: opts.signal,
1408
+ maxBytes: 1e5
1409
+ });
1410
+ const changed = (result.stdout.match(/changed/g) || []).length;
1411
+ yield {
1412
+ type: "final",
1413
+ output: {
1414
+ fixer: detected,
1415
+ files_checked: 0,
1416
+ files_changed: changed,
1417
+ output: result.stdout || result.stderr || result.error || "",
1418
+ truncated: result.truncated
1419
+ }
1420
+ };
1421
+ }
1422
+ };
1423
+ async function detectFixer(cwd) {
1424
+ const { stat: stat9 } = await import('fs/promises');
1425
+ try {
1426
+ await stat9(`${cwd}/biome.json`);
1427
+ return "biome";
1428
+ } catch {
1429
+ try {
1430
+ await stat9(`${cwd}/.prettierrc`);
1431
+ return "prettier";
1432
+ } catch {
1433
+ return "biome";
1434
+ }
1435
+ }
1436
+ }
1437
+ var TIMEOUT_MS3 = 3e4;
1438
+ var MAX_OUTPUT3 = 1e5;
1439
+ var gitTool = {
1440
+ name: "git",
1441
+ category: "Git",
1442
+ description: "Run git commands. Wraps common operations: status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset.",
1443
+ usageHint: "Prefer built-in subcommands over raw args. `command` is required. `message` for commits. `branch` for checkout/branch. `files` for status/diff. `format` for log.",
1444
+ permission: "confirm",
1445
+ // Conservative: any of these may mutate. The non-mutating commands
1446
+ // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
1447
+ // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
1448
+ mutating: true,
1449
+ timeoutMs: TIMEOUT_MS3,
1450
+ inputSchema: {
1451
+ type: "object",
1452
+ properties: {
1453
+ command: {
1454
+ type: "string",
1455
+ enum: [
1456
+ "status",
1457
+ "log",
1458
+ "diff",
1459
+ "commit",
1460
+ "branch",
1461
+ "checkout",
1462
+ "stash",
1463
+ "push",
1464
+ "pull",
1465
+ "fetch",
1466
+ "reset"
1467
+ ],
1468
+ description: "Git subcommand"
1469
+ },
1470
+ files: {
1471
+ type: "string",
1472
+ description: 'File(s) for status/diff: single path, comma-separated list, or "**/*.ts" glob'
1473
+ },
1474
+ message: { type: "string", description: "Commit message (required for commit)" },
1475
+ branch: { type: "string", description: "Branch name for checkout/branch" },
1476
+ format: {
1477
+ type: "string",
1478
+ enum: ["short", "oneline", "stat", "graph"],
1479
+ description: "Log format (default: short)"
1480
+ },
1481
+ limit: { type: "integer", description: "Limit for log (default: 20)" },
1482
+ dry_run: { type: "boolean", description: "For commit: show what would be committed" }
1483
+ },
1484
+ required: ["command"]
1485
+ },
1486
+ async execute(input, ctx, opts) {
1487
+ if (!input?.command) throw new Error("git: command is required");
1488
+ const gitDir = findGitDir2(ctx.cwd, ctx.projectRoot);
1489
+ if (!gitDir) {
1490
+ return {
1491
+ command: input.command,
1492
+ stdout: "",
1493
+ stderr: "Not in a git repository (within project root)",
1494
+ exitCode: 128,
1495
+ truncated: false
1496
+ };
1497
+ }
1498
+ const args = buildArgs(input);
1499
+ return await runGit2(args, gitDir, opts.signal);
1500
+ }
1501
+ };
1502
+ function findGitDir2(cwd, projectRoot) {
1503
+ const root = projectRoot;
1504
+ let dir = cwd;
1505
+ for (let i = 0; i < 20; i++) {
1506
+ try {
1507
+ const stat9 = statSync(`${dir}/.git`);
1508
+ if (stat9.isDirectory()) return dir;
1509
+ } catch {
1510
+ }
1511
+ if (dir === root) break;
1512
+ const parent = dirname(dir);
1513
+ if (parent === dir) break;
1514
+ dir = parent;
1515
+ }
1516
+ return null;
1517
+ }
1518
+ function buildArgs(input) {
1519
+ const limit = input.limit ?? 20;
1520
+ const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((s) => s.trim()).filter(Boolean) : [];
1521
+ switch (input.command) {
1522
+ case "status":
1523
+ return ["status", ...files.length ? ["--", ...files] : []];
1524
+ case "log":
1525
+ return [
1526
+ "log",
1527
+ `--max-count=${limit}`,
1528
+ ...input.format === "oneline" ? ["--oneline"] : [],
1529
+ ...input.format === "stat" ? ["--stat"] : [],
1530
+ ...input.format === "graph" ? ["--oneline", "--graph", "--decorate"] : [],
1531
+ ...input.format === "short" || !input.format ? [] : []
1532
+ ];
1533
+ case "diff":
1534
+ return ["diff", "--no-color", ...files.length ? ["--", ...files] : []];
1535
+ case "commit":
1536
+ return [
1537
+ "commit",
1538
+ ...input.dry_run ? ["--dry-run", "--porcelain"] : [],
1539
+ ...input.message ? ["-m", input.message] : [],
1540
+ ...files.length ? ["--", ...files] : []
1541
+ ];
1542
+ case "branch":
1543
+ return input.branch ? ["branch", input.branch] : ["branch"];
1544
+ case "checkout":
1545
+ return [
1546
+ "checkout",
1547
+ ...input.branch ? [input.branch] : [],
1548
+ ...files.length ? ["--", ...files] : []
1549
+ ];
1550
+ case "stash":
1551
+ return input.message ? ["stash", "push", "-m", input.message] : ["stash", "push"];
1552
+ case "push":
1553
+ return ["push"];
1554
+ case "pull":
1555
+ return ["pull"];
1556
+ case "fetch":
1557
+ return ["fetch", ...input.branch ? [input.branch] : ["--all"]];
1558
+ case "reset":
1559
+ return ["reset"];
1560
+ default:
1561
+ return [input.command];
1562
+ }
1563
+ }
1564
+ function runGit2(args, cwd, signal) {
1565
+ return new Promise((resolve4) => {
1566
+ let stdout = "";
1567
+ let stderr = "";
1568
+ const child = spawn("git", args, {
1569
+ cwd,
1570
+ signal,
1571
+ stdio: ["ignore", "pipe", "pipe"]
1572
+ });
1573
+ child.stdout?.on("data", (chunk) => {
1574
+ if (stdout.length < MAX_OUTPUT3) {
1575
+ stdout += chunk.toString();
1576
+ }
1577
+ });
1578
+ child.stderr?.on("data", (chunk) => {
1579
+ if (stderr.length < MAX_OUTPUT3) {
1580
+ stderr += chunk.toString();
1581
+ }
1582
+ });
1583
+ child.on("error", (err) => {
1584
+ resolve4({
1585
+ command: args[0],
1586
+ stdout,
1587
+ stderr: err.message,
1588
+ exitCode: 1,
1589
+ truncated: stdout.length >= MAX_OUTPUT3
1590
+ });
1591
+ });
1592
+ child.on("close", (code) => {
1593
+ resolve4({
1594
+ command: args[0],
1595
+ stdout: stdout.slice(0, MAX_OUTPUT3),
1596
+ stderr: stderr.slice(0, MAX_OUTPUT3),
1597
+ exitCode: code ?? 1,
1598
+ truncated: stdout.length >= MAX_OUTPUT3 || stderr.length >= MAX_OUTPUT3
1599
+ });
1600
+ });
1601
+ });
1602
+ }
1603
+ var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".turbo"];
1604
+ var globTool = {
1605
+ name: "glob",
1606
+ category: "Filesystem",
1607
+ description: "Find files matching a glob pattern. Returns paths sorted by mtime (newest first).",
1608
+ usageHint: "Examples: `**/*.ts`, `src/**/*.test.ts`, `*.json`. Common dirs (node_modules, .git, dist) are ignored by default. Returns up to 1000 paths.",
1609
+ permission: "auto",
1610
+ mutating: false,
1611
+ maxOutputBytes: 65536,
1612
+ timeoutMs: 5e3,
1613
+ inputSchema: {
1614
+ type: "object",
1615
+ properties: {
1616
+ pattern: { type: "string" },
1617
+ path: { type: "string", description: "Base directory (defaults to cwd)" },
1618
+ limit: { type: "integer" }
1619
+ },
1620
+ required: ["pattern"]
1621
+ },
1622
+ async execute(input, ctx) {
1623
+ if (!input?.pattern) throw new Error("glob: pattern is required");
1624
+ const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
1625
+ const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
1626
+ const ignored = await readGitignore(base);
1627
+ const re = compileGlob(input.pattern);
1628
+ const results = [];
1629
+ let truncated = false;
1630
+ const walk = async (dir, relPrefix) => {
1631
+ if (results.length >= limit) {
1632
+ truncated = true;
1633
+ return;
1634
+ }
1635
+ let entries;
1636
+ try {
1637
+ entries = await fs9.readdir(dir, { withFileTypes: true });
1638
+ } catch {
1639
+ return;
1640
+ }
1641
+ for (const e of entries) {
1642
+ const name = e.name;
1643
+ if (DEFAULT_IGNORE.includes(name)) continue;
1644
+ if (ignored.includes(name)) continue;
1645
+ const rel = relPrefix ? `${relPrefix}/${name}` : name;
1646
+ const full = path.join(dir, name);
1647
+ if (e.isDirectory()) {
1648
+ await walk(full, rel);
1649
+ if (truncated) return;
1650
+ } else if (e.isFile()) {
1651
+ if (re.test(rel) || re.test(name)) {
1652
+ try {
1653
+ const st = await fs9.stat(full);
1654
+ results.push({ rel: full, mtime: st.mtimeMs });
1655
+ if (results.length >= limit) {
1656
+ truncated = true;
1657
+ return;
1658
+ }
1659
+ } catch {
1660
+ }
1661
+ }
1662
+ }
1663
+ }
1664
+ };
1665
+ await walk(base, "");
1666
+ results.sort((a, b) => b.mtime - a.mtime);
1667
+ return { files: results.map((r) => r.rel), truncated };
1668
+ }
1669
+ };
1670
+ async function readGitignore(dir) {
1671
+ try {
1672
+ const raw = await fs9.readFile(path.join(dir, ".gitignore"), "utf8");
1673
+ return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
1674
+ } catch {
1675
+ return [];
1676
+ }
1677
+ }
1678
+
1679
+ // src/_regex.ts
1680
+ var MAX_PATTERN_LEN = 512;
1681
+ var DANGEROUS_PATTERNS = [
1682
+ /(\([^)]*[+*][^)]*\))[+*]/,
1683
+ // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
1684
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
1685
+ // same, with non-capturing group
1686
+ ];
1687
+ function compileUserRegex(pattern, flags) {
1688
+ if (typeof pattern !== "string") {
1689
+ return { ok: false, reason: "pattern must be a string" };
1690
+ }
1691
+ if (pattern.length === 0) {
1692
+ return { ok: false, reason: "pattern is empty" };
1693
+ }
1694
+ if (pattern.length > MAX_PATTERN_LEN) {
1695
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
1696
+ }
1697
+ for (const rx of DANGEROUS_PATTERNS) {
1698
+ if (rx.test(pattern)) {
1699
+ return {
1700
+ ok: false,
1701
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
1702
+ };
1703
+ }
1704
+ }
1705
+ try {
1706
+ return { ok: true, regex: new RegExp(pattern, flags) };
1707
+ } catch (err) {
1708
+ return {
1709
+ ok: false,
1710
+ reason: err instanceof Error ? err.message : "invalid regex"
1711
+ };
1712
+ }
1713
+ }
1714
+ var MAX_SUBJECT_LEN = 64 * 1024;
1715
+ function capSubject(line) {
1716
+ return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
1717
+ }
1718
+
1719
+ // src/grep.ts
1720
+ var DEFAULT_IGNORE2 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
1721
+ var grepTool = {
1722
+ name: "grep",
1723
+ category: "Search",
1724
+ description: "Search file contents with a regex. Uses ripgrep when available.",
1725
+ usageHint: 'Pattern is regex. Use `output_mode: "content"` for matched lines, `"files_with_matches"` for paths, `"count"` for tallies. `glob` filters files (e.g. `*.ts`).',
1726
+ permission: "auto",
1727
+ mutating: false,
1728
+ maxOutputBytes: 131072,
1729
+ timeoutMs: 1e4,
1730
+ inputSchema: {
1731
+ type: "object",
1732
+ properties: {
1733
+ pattern: { type: "string" },
1734
+ path: { type: "string" },
1735
+ glob: { type: "string" },
1736
+ output_mode: { type: "string", enum: ["content", "files_with_matches", "count"] },
1737
+ context_lines: { type: "integer" },
1738
+ case_insensitive: { type: "boolean" },
1739
+ limit: { type: "integer" }
1740
+ },
1741
+ required: ["pattern"]
1742
+ },
1743
+ async execute(input, ctx, opts) {
1744
+ let final;
1745
+ for await (const ev of grepTool.executeStream(input, ctx, opts)) {
1746
+ if (ev.type === "final") final = ev.output;
1747
+ }
1748
+ if (!final) throw new Error("grep: stream ended without final event");
1749
+ return final;
1750
+ },
1751
+ async *executeStream(input, ctx, opts) {
1752
+ if (!input?.pattern) throw new Error("grep: pattern is required");
1753
+ const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
1754
+ const mode = input.output_mode ?? "content";
1755
+ const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
1756
+ const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
1757
+ if (!validation.ok) {
1758
+ throw new Error(`grep: ${validation.reason}`);
1759
+ }
1760
+ const rgAvailable = await detectRg(opts.signal);
1761
+ if (rgAvailable) {
1762
+ try {
1763
+ yield* runRgStream(input, base, mode, limit, opts.signal);
1764
+ return;
1765
+ } catch {
1766
+ }
1767
+ }
1768
+ yield { type: "log", text: "Falling back to native grep\u2026" };
1769
+ const out = await runNative(input, base, mode, limit, opts.signal);
1770
+ yield { type: "final", output: out };
1771
+ }
1772
+ };
1773
+ async function detectRg(signal) {
1774
+ return new Promise((resolve4) => {
1775
+ try {
1776
+ const p = spawn("rg", ["--version"], { stdio: "ignore", signal });
1777
+ p.on("error", () => resolve4(false));
1778
+ p.on("close", (code) => resolve4(code === 0));
1779
+ } catch {
1780
+ resolve4(false);
1781
+ }
1782
+ });
1783
+ }
1784
+ async function* runRgStream(input, base, mode, limit, signal) {
1785
+ const args = ["--no-heading"];
1786
+ if (input.case_insensitive) args.push("-i");
1787
+ if (mode === "files_with_matches") args.push("-l");
1788
+ if (mode === "count") args.push("-c");
1789
+ if (mode === "content") {
1790
+ args.push("-n");
1791
+ if (input.context_lines) args.push("-C", String(input.context_lines));
1792
+ }
1793
+ for (const ignored of DEFAULT_IGNORE2) {
1794
+ args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
1795
+ }
1796
+ if (input.glob) args.push("--glob", input.glob);
1797
+ args.push("--", input.pattern, base);
1798
+ const matches = [];
1799
+ let buf = "";
1800
+ let totalLines = 0;
1801
+ let totalCount = 0;
1802
+ let batchSinceFlush = 0;
1803
+ const FLUSH_AT = 16;
1804
+ const MAX_BUF_BYTES = 1e6;
1805
+ let bufOverflow = false;
1806
+ const child = spawn("rg", args, { signal, stdio: ["ignore", "pipe", "pipe"] });
1807
+ const queue = [];
1808
+ let waiter;
1809
+ const wake = () => {
1810
+ if (waiter) {
1811
+ const w = waiter;
1812
+ waiter = void 0;
1813
+ w();
1814
+ }
1815
+ };
1816
+ child.stdout?.on("data", (c) => {
1817
+ queue.push({ kind: "out", data: c.toString() });
1818
+ wake();
1819
+ });
1820
+ child.on("error", (e) => {
1821
+ queue.push({ kind: "error", data: e.message });
1822
+ wake();
1823
+ });
1824
+ child.on("close", () => {
1825
+ queue.push({ kind: "close", data: "" });
1826
+ wake();
1827
+ });
1828
+ let pendingBatch = [];
1829
+ let errored = false;
1830
+ for (; ; ) {
1831
+ while (queue.length === 0) {
1832
+ await new Promise((r) => {
1833
+ waiter = r;
1834
+ });
1835
+ }
1836
+ const c = queue.shift();
1837
+ if (c.kind === "error") {
1838
+ errored = true;
1839
+ continue;
1840
+ }
1841
+ if (c.kind === "close") break;
1842
+ buf += c.data;
1843
+ if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
1844
+ bufOverflow = true;
1845
+ buf = buf.slice(-MAX_BUF_BYTES);
1846
+ try {
1847
+ child.kill("SIGTERM");
1848
+ } catch {
1849
+ }
1850
+ }
1851
+ const idx = buf.lastIndexOf("\n");
1852
+ if (idx === -1) continue;
1853
+ const ready = buf.slice(0, idx);
1854
+ buf = buf.slice(idx + 1);
1855
+ for (const line of ready.split("\n")) {
1856
+ if (!line) continue;
1857
+ totalLines++;
1858
+ if (mode === "count") totalCount += parseRgCountLine(line);
1859
+ if (matches.length < limit) {
1860
+ matches.push(line);
1861
+ pendingBatch.push(line);
1862
+ batchSinceFlush++;
1863
+ }
1864
+ }
1865
+ if (batchSinceFlush >= FLUSH_AT) {
1866
+ yield {
1867
+ type: "partial_output",
1868
+ text: pendingBatch.join("\n"),
1869
+ data: { matches_so_far: matches.length }
1870
+ };
1871
+ pendingBatch = [];
1872
+ batchSinceFlush = 0;
1873
+ }
1874
+ }
1875
+ if (buf.trim()) {
1876
+ for (const line of buf.split("\n")) {
1877
+ if (!line) continue;
1878
+ totalLines++;
1879
+ if (mode === "count") totalCount += parseRgCountLine(line);
1880
+ if (matches.length < limit) {
1881
+ matches.push(line);
1882
+ pendingBatch.push(line);
1883
+ }
1884
+ }
1885
+ }
1886
+ if (pendingBatch.length > 0) {
1887
+ yield {
1888
+ type: "partial_output",
1889
+ text: pendingBatch.join("\n"),
1890
+ data: { matches_so_far: matches.length }
1891
+ };
1892
+ }
1893
+ if (errored) throw new Error("rg: spawn error");
1894
+ yield {
1895
+ type: "final",
1896
+ output: {
1897
+ matches,
1898
+ count: mode === "count" ? totalCount : totalLines,
1899
+ truncated: totalLines > limit || bufOverflow,
1900
+ used: "rg"
1901
+ }
1902
+ };
1903
+ }
1904
+ function parseRgCountLine(line) {
1905
+ const idx = line.lastIndexOf(":");
1906
+ if (idx === -1) return 0;
1907
+ const n = Number.parseInt(line.slice(idx + 1), 10);
1908
+ return Number.isFinite(n) ? n : 0;
1909
+ }
1910
+ async function runNative(input, base, mode, limit, signal) {
1911
+ const flags = input.case_insensitive ? "i" : "";
1912
+ const compiled = compileUserRegex(input.pattern, flags);
1913
+ if (!compiled.ok) {
1914
+ throw new Error(`grep: ${compiled.reason}`);
1915
+ }
1916
+ const re = compiled.regex;
1917
+ const globRe = input.glob ? compileGlob(input.glob) : null;
1918
+ const matches = [];
1919
+ const fileMatches = /* @__PURE__ */ new Map();
1920
+ let total = 0;
1921
+ let stopped = false;
1922
+ const walk = async (dir) => {
1923
+ if (stopped || signal.aborted) return;
1924
+ let entries;
1925
+ try {
1926
+ entries = await fs9.readdir(dir, { withFileTypes: true });
1927
+ } catch {
1928
+ return;
1929
+ }
1930
+ for (const e of entries) {
1931
+ if (stopped) return;
1932
+ if (DEFAULT_IGNORE2.includes(e.name)) continue;
1933
+ if (e.isSymbolicLink()) continue;
1934
+ const full = path.join(dir, e.name);
1935
+ if (e.isDirectory()) {
1936
+ await walk(full);
1937
+ } else if (e.isFile()) {
1938
+ if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
1939
+ if (globRe) globRe.lastIndex = 0;
1940
+ try {
1941
+ const stat9 = await fs9.stat(full);
1942
+ if (stat9.size > 1e6) continue;
1943
+ const head = await fs9.readFile(full);
1944
+ if (isBinaryBuffer(head)) continue;
1945
+ const text = head.toString("utf8");
1946
+ const lines = text.split(/\r?\n/);
1947
+ let fileHits = 0;
1948
+ for (let i = 0; i < lines.length; i++) {
1949
+ const ln = capSubject(lines[i] ?? "");
1950
+ re.lastIndex = 0;
1951
+ if (re.test(ln)) {
1952
+ fileHits++;
1953
+ total++;
1954
+ if (mode === "content" && matches.length < limit) {
1955
+ matches.push(`${full}:${i + 1}:${ln}`);
1956
+ }
1957
+ }
1958
+ }
1959
+ if (fileHits > 0) {
1960
+ fileMatches.set(full, fileHits);
1961
+ if (mode === "files_with_matches" && matches.length < limit) {
1962
+ matches.push(full);
1963
+ }
1964
+ if (mode === "count" && matches.length < limit) {
1965
+ matches.push(`${full}:${fileHits}`);
1966
+ }
1967
+ }
1968
+ if (matches.length >= limit) stopped = true;
1969
+ } catch {
1970
+ }
1971
+ }
1972
+ }
1973
+ };
1974
+ await walk(base);
1975
+ return {
1976
+ matches,
1977
+ count: total,
1978
+ truncated: stopped,
1979
+ used: "native"
1980
+ };
1981
+ }
1982
+
1983
+ // src/install.ts
1984
+ var installTool = {
1985
+ name: "install",
1986
+ category: "Package Management",
1987
+ description: "Install npm packages. Detects pnpm/npm/yarn and uses the right package manager.",
1988
+ usageHint: "Set `packages` to install. `save` as dependency type. `global` for global install. `dry_run` to preview.",
1989
+ permission: "confirm",
1990
+ mutating: true,
1991
+ timeoutMs: 12e4,
1992
+ inputSchema: {
1993
+ type: "object",
1994
+ properties: {
1995
+ packages: {
1996
+ type: "string",
1997
+ description: "Package(s) to install: single name, comma-separated list, or empty for all deps"
1998
+ },
1999
+ save: {
2000
+ type: "string",
2001
+ enum: ["dependency", "dev", "optional"],
2002
+ description: "Save as regular, dev, or optional dependency"
2003
+ },
2004
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
2005
+ dry_run: {
2006
+ type: "boolean",
2007
+ description: "Preview install without modifying (default: false)"
2008
+ },
2009
+ global: { type: "boolean", description: "Install globally (default: false)" }
2010
+ }
2011
+ },
2012
+ async execute(input, ctx, opts) {
2013
+ let final;
2014
+ for await (const ev of installTool.executeStream(input, ctx, opts)) {
2015
+ if (ev.type === "final") final = ev.output;
2016
+ }
2017
+ if (!final) throw new Error("install: stream ended without final event");
2018
+ return final;
2019
+ },
2020
+ async *executeStream(input, ctx, opts) {
2021
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
2022
+ const pkgManager = await detectPackageManager(cwd);
2023
+ yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
2024
+ const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
2025
+ const globalFlag = input.global ? ["-g"] : [];
2026
+ const args = [];
2027
+ if (input.dry_run) args.push("--dry-run");
2028
+ if (pkgManager === "pnpm") {
2029
+ if (save) args.push(save);
2030
+ args.push("add", ...globalFlag);
2031
+ } else if (pkgManager === "yarn") {
2032
+ args.push("add", ...globalFlag);
2033
+ } else {
2034
+ args.push("install", ...globalFlag);
2035
+ }
2036
+ const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
2037
+ (p) => p.trim()
2038
+ ) : [];
2039
+ if (pkgList.length > 0) args.push(...pkgList);
2040
+ yield {
2041
+ type: "log",
2042
+ text: `Fetching ${pkgList.length || "all"} packages\u2026`,
2043
+ data: { phase: "fetch" }
2044
+ };
2045
+ const result = yield* spawnStream({
2046
+ cmd: pkgManager,
2047
+ args,
2048
+ cwd,
2049
+ signal: opts.signal,
2050
+ maxBytes: 1e5
2051
+ });
2052
+ yield {
2053
+ type: "final",
2054
+ output: {
2055
+ packages: pkgList,
2056
+ exit_code: result.exitCode,
2057
+ output: result.stdout || result.stderr || result.error || "",
2058
+ dry_run: args.includes("--dry-run"),
2059
+ truncated: result.truncated
2060
+ }
2061
+ };
2062
+ }
2063
+ };
2064
+ async function detectPackageManager(cwd) {
2065
+ const { stat: stat9 } = await import('fs/promises');
2066
+ try {
2067
+ await stat9(`${cwd}/pnpm-lock.yaml`);
2068
+ return "pnpm";
2069
+ } catch {
2070
+ try {
2071
+ await stat9(`${cwd}/yarn.lock`);
2072
+ return "yarn";
2073
+ } catch {
2074
+ return "npm";
2075
+ }
2076
+ }
2077
+ }
2078
+ var jsonTool = {
2079
+ name: "json",
2080
+ category: "Data",
2081
+ description: "Parse, query, and validate JSON/JSON5/YAML. Use `query` with JMESPath-like paths to extract values.",
2082
+ usageHint: 'Provide `file` path or `data` string. `query` supports dot notation (e.g. "results[0].name"). `format` outputs in specified format.',
2083
+ permission: "auto",
2084
+ mutating: false,
2085
+ timeoutMs: 5e3,
2086
+ inputSchema: {
2087
+ type: "object",
2088
+ properties: {
2089
+ file: { type: "string", description: "Path to JSON/JSON5/YAML file" },
2090
+ data: { type: "string", description: "JSON/JSON5/YAML string (alternative to file)" },
2091
+ query: {
2092
+ type: "string",
2093
+ description: 'JMESPath-like query (e.g. "a.b[0].c" or "a[*].name")'
2094
+ },
2095
+ format: {
2096
+ type: "string",
2097
+ enum: ["json", "json5", "yaml"],
2098
+ description: "Output format (default: json)"
2099
+ },
2100
+ validate: {
2101
+ type: "boolean",
2102
+ description: "Validate syntax only, no output (default: false)"
2103
+ }
2104
+ }
2105
+ },
2106
+ async execute(input) {
2107
+ const format = input.format ?? "json";
2108
+ let parsed;
2109
+ let raw;
2110
+ if (input.file) {
2111
+ try {
2112
+ raw = await fs9.readFile(input.file, "utf8");
2113
+ } catch {
2114
+ return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
2115
+ }
2116
+ } else if (input.data) {
2117
+ raw = input.data;
2118
+ } else {
2119
+ return { data: null, formatted: "", type: "unknown", error: "Provide file or data" };
2120
+ }
2121
+ try {
2122
+ parsed = JSON.parse(raw);
2123
+ } catch (e) {
2124
+ return {
2125
+ data: null,
2126
+ formatted: "",
2127
+ type: "unknown",
2128
+ error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
2129
+ };
2130
+ }
2131
+ if (input.validate) {
2132
+ return {
2133
+ data: parsed,
2134
+ formatted: "valid",
2135
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
2136
+ keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
2137
+ };
2138
+ }
2139
+ const queryResult = input.query ? query(parsed, input.query) : void 0;
2140
+ const formatted = formatOutput(queryResult ?? parsed, format);
2141
+ return {
2142
+ data: parsed,
2143
+ formatted,
2144
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
2145
+ keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0,
2146
+ query_result: queryResult
2147
+ };
2148
+ }
2149
+ };
2150
+ function query(data, path12) {
2151
+ const parts = path12.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
2152
+ let current = data;
2153
+ for (const part of parts) {
2154
+ if (current === null || current === void 0) return void 0;
2155
+ const idx = Number(part);
2156
+ if (!Number.isNaN(idx) && Array.isArray(current)) {
2157
+ current = current[idx];
2158
+ } else if (typeof current === "object" && current !== null) {
2159
+ current = current[part];
2160
+ } else {
2161
+ return void 0;
2162
+ }
2163
+ }
2164
+ return current;
2165
+ }
2166
+ function formatOutput(data, format) {
2167
+ if (format === "json5") {
2168
+ return JSON.stringify(data, null, 2).replace(/,\s*}/g, "}").replace(/,\s*\]/g, "]");
2169
+ }
2170
+ if (format === "yaml") {
2171
+ return toYaml(data);
2172
+ }
2173
+ return JSON.stringify(data, null, 2);
2174
+ }
2175
+ function toYaml(data, indent = 0) {
2176
+ if (data === null) return "null\n";
2177
+ if (data === void 0) return "";
2178
+ if (typeof data === "boolean") return String(data) + "\n";
2179
+ if (typeof data === "number") return String(data) + "\n";
2180
+ if (typeof data === "string") {
2181
+ if (data.includes("\n") || data.includes(":") || data.includes("#")) {
2182
+ return `"${data.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"
2183
+ `;
2184
+ }
2185
+ return data + "\n";
2186
+ }
2187
+ if (Array.isArray(data)) {
2188
+ if (data.length === 0) return "[]\n";
2189
+ const prefix = " ".repeat(indent);
2190
+ return data.map((item) => `${prefix}- ${toYaml(item, indent + 1).trimStart()}`).join("");
2191
+ }
2192
+ if (typeof data === "object") {
2193
+ const prefix = " ".repeat(indent);
2194
+ const entries = Object.entries(data);
2195
+ return entries.map(([k, v]) => `${prefix}${k}: ${toYaml(v, indent + 1)}`).join("");
2196
+ }
2197
+ return String(data) + "\n";
2198
+ }
2199
+
2200
+ // src/lint.ts
2201
+ var lintTool = {
2202
+ name: "lint",
2203
+ category: "Code Quality",
2204
+ description: "Run a linter on files. Auto-detects biome, eslint, or tslint. Use `fix` to auto-fix issues.",
2205
+ usageHint: "Set `files` (glob or comma-separated). `fix` applies corrections. `linter` forces specific tool.",
2206
+ permission: "confirm",
2207
+ mutating: false,
2208
+ timeoutMs: 6e4,
2209
+ inputSchema: {
2210
+ type: "object",
2211
+ properties: {
2212
+ files: {
2213
+ type: "string",
2214
+ description: 'Files/patterns: single path, comma-separated list, or glob (e.g. "src/**/*.ts")'
2215
+ },
2216
+ fix: { type: "boolean", description: "Auto-fix fixable issues (default: false)" },
2217
+ linter: {
2218
+ type: "string",
2219
+ enum: ["biome", "eslint", "tslint", "auto"],
2220
+ description: "Linter to use (default: auto-detect)"
2221
+ },
2222
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
2223
+ }
2224
+ },
2225
+ async execute(input, ctx, opts) {
2226
+ let final;
2227
+ for await (const ev of lintTool.executeStream(input, ctx, opts)) {
2228
+ if (ev.type === "final") final = ev.output;
2229
+ }
2230
+ if (!final) throw new Error("lint: stream ended without final event");
2231
+ return final;
2232
+ },
2233
+ async *executeStream(input, ctx, opts) {
2234
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
2235
+ const linter = input.linter ?? "auto";
2236
+ const detected = linter === "auto" ? await detectLinter(cwd) : linter;
2237
+ if (!detected) {
2238
+ yield {
2239
+ type: "final",
2240
+ output: {
2241
+ linter: "none",
2242
+ files_checked: 0,
2243
+ errors: 0,
2244
+ warnings: 0,
2245
+ output: "No linter found (biome.json, .eslintrc, tslint.json)",
2246
+ fix_applied: false,
2247
+ truncated: false
2248
+ }
2249
+ };
2250
+ return;
2251
+ }
2252
+ yield { type: "log", text: `Running ${detected}\u2026`, data: { linter: detected } };
2253
+ const args = ["lint"];
2254
+ if (input.fix) args.push("--write");
2255
+ if (input.files) {
2256
+ const files = Array.isArray(input.files) ? input.files : input.files.split(",");
2257
+ args.push("--", ...files.map((f) => f.trim()));
2258
+ }
2259
+ const cmd = detected === "biome" ? "biome" : detected;
2260
+ const result = yield* spawnStream({ cmd, args, cwd, signal: opts.signal, maxBytes: 1e5 });
2261
+ const errors = (result.stdout.match(/error/g) || []).length;
2262
+ const warnings = (result.stdout.match(/warning/g) || []).length;
2263
+ yield {
2264
+ type: "final",
2265
+ output: {
2266
+ linter: detected,
2267
+ files_checked: input.files ? Array.isArray(input.files) ? input.files.length : input.files.split(",").length : 0,
2268
+ errors,
2269
+ warnings,
2270
+ output: result.stdout,
2271
+ fix_applied: input.fix ?? false,
2272
+ truncated: result.truncated
2273
+ }
2274
+ };
2275
+ }
2276
+ };
2277
+ async function detectLinter(cwd) {
2278
+ const { stat: stat9 } = await import('fs/promises');
2279
+ const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
2280
+ for (const f of checks) {
2281
+ try {
2282
+ await stat9(`${cwd}/${f}`);
2283
+ if (f.includes("biome")) return "biome";
2284
+ if (f.includes("eslint")) return "eslint";
2285
+ if (f.includes("tslint")) return "tslint";
2286
+ } catch {
2287
+ }
2288
+ }
2289
+ return "biome";
2290
+ }
2291
+ var logsTool = {
2292
+ name: "logs",
2293
+ category: "Logs",
2294
+ description: "Stream or fetch logs from a service or file. Supports Docker, systemd, or plain log files.",
2295
+ usageHint: "Set `service` for Docker/systemd, `path` for file. `lines` limits output. `stream` for tail -f behavior. `filter` regex filters lines.",
2296
+ permission: "confirm",
2297
+ mutating: false,
2298
+ timeoutMs: 3e4,
2299
+ inputSchema: {
2300
+ type: "object",
2301
+ properties: {
2302
+ service: {
2303
+ type: "string",
2304
+ description: "Service name for Docker or systemd journal"
2305
+ },
2306
+ path: {
2307
+ type: "string",
2308
+ description: "Path to log file (alternative to service)"
2309
+ },
2310
+ lines: {
2311
+ type: "integer",
2312
+ description: "Number of log lines to fetch (default: 100, 0 for all)",
2313
+ minimum: 0,
2314
+ maximum: 1e4
2315
+ },
2316
+ stream: {
2317
+ type: "boolean",
2318
+ description: "Stream logs continuously (like tail -f) (default: false)"
2319
+ },
2320
+ filter: {
2321
+ type: "string",
2322
+ description: "Regex pattern to filter log lines"
2323
+ },
2324
+ since: {
2325
+ type: "string",
2326
+ enum: ["1h", "6h", "24h", "all"],
2327
+ description: "Only show logs since duration"
2328
+ },
2329
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
2330
+ }
2331
+ },
2332
+ async execute(input, ctx, opts) {
2333
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
2334
+ const lines = input.lines ?? 100;
2335
+ let filterRe = null;
2336
+ if (input.filter) {
2337
+ const compiled = compileUserRegex(input.filter, "i");
2338
+ if (!compiled.ok) {
2339
+ throw new Error(`logs: ${compiled.reason}`);
2340
+ }
2341
+ filterRe = compiled.regex;
2342
+ }
2343
+ if (input.service) {
2344
+ return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
2345
+ }
2346
+ if (input.path) {
2347
+ return await fileLogs(safeResolve(input.path, ctx), lines, filterRe, input.stream ?? false);
2348
+ }
2349
+ return {
2350
+ source: "none",
2351
+ entries: [],
2352
+ total: 0,
2353
+ truncated: false,
2354
+ stream_mode: false
2355
+ };
2356
+ }
2357
+ };
2358
+ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
2359
+ const args = ["logs"];
2360
+ if (lines > 0) args.push("--tail", String(lines));
2361
+ args.push("--timestamps", service);
2362
+ return new Promise((resolve4) => {
2363
+ let stdout = "";
2364
+ let stderr = "";
2365
+ const MAX = 2e5;
2366
+ const child = spawn("docker", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
2367
+ child.stdout?.on("data", (c) => {
2368
+ if (stdout.length < MAX) stdout += c.toString();
2369
+ });
2370
+ child.stderr?.on("data", (c) => {
2371
+ if (stderr.length < MAX) stderr += c.toString();
2372
+ });
2373
+ child.on("close", (code) => {
2374
+ const output = stdout + stderr;
2375
+ const entries = parseLogLines(output, filterRe);
2376
+ resolve4({
2377
+ source: `docker:${service}`,
2378
+ entries,
2379
+ total: entries.length,
2380
+ truncated: output.length >= MAX,
2381
+ stream_mode: false
2382
+ });
2383
+ });
2384
+ child.on(
2385
+ "error",
2386
+ (e) => resolve4({
2387
+ source: `docker:${service}`,
2388
+ entries: [],
2389
+ total: 0,
2390
+ truncated: false,
2391
+ stream_mode: false
2392
+ })
2393
+ );
2394
+ });
2395
+ }
2396
+ var MAX_TAIL_LINES = 1e5;
2397
+ async function fileLogs(path12, lines, filterRe, stream) {
2398
+ const { createInterface } = await import('readline');
2399
+ const { createReadStream } = await import('fs');
2400
+ const entries = [];
2401
+ const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
2402
+ const window = new Array(effLines);
2403
+ let writeIdx = 0;
2404
+ let totalLines = 0;
2405
+ const rl = createInterface({
2406
+ input: createReadStream(path12),
2407
+ crlfDelay: Number.POSITIVE_INFINITY
2408
+ });
2409
+ for await (const line of rl) {
2410
+ if (filterRe && !filterRe.test(line)) continue;
2411
+ window[writeIdx] = line;
2412
+ writeIdx = (writeIdx + 1) % effLines;
2413
+ totalLines++;
2414
+ }
2415
+ const ordered = [];
2416
+ const start = totalLines >= effLines ? writeIdx : 0;
2417
+ const count = Math.min(totalLines, effLines);
2418
+ for (let i = 0; i < count; i++) {
2419
+ const v = window[(start + i) % effLines];
2420
+ if (v !== void 0) ordered.push(v);
2421
+ }
2422
+ for (const line of ordered) {
2423
+ const parsed = parseLine(line);
2424
+ if (parsed) entries.push(parsed);
2425
+ }
2426
+ return {
2427
+ source: path12,
2428
+ entries,
2429
+ total: entries.length,
2430
+ truncated: totalLines > effLines,
2431
+ stream_mode: stream
2432
+ };
2433
+ }
2434
+ function parseLogLines(output, filterRe) {
2435
+ const lines = output.split("\n").filter(Boolean);
2436
+ const entries = [];
2437
+ for (const line of lines) {
2438
+ if (filterRe && !filterRe.test(line)) continue;
2439
+ const parsed = parseLine(line);
2440
+ if (parsed) entries.push(parsed);
2441
+ }
2442
+ return entries;
2443
+ }
2444
+ function parseLine(line) {
2445
+ const tsRe = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\s+(?:\[?(\w+)\]?)\s*(.*)/;
2446
+ const match = tsRe.exec(line);
2447
+ if (match) {
2448
+ return {
2449
+ timestamp: match[1] ?? "",
2450
+ level: match[2]?.toLowerCase() ?? "info",
2451
+ message: match[3] ?? ""
2452
+ };
2453
+ }
2454
+ const levelRe = /(?:ERROR|WARN|INFO|DEBUG|TRACE)\s+(.*)/i;
2455
+ const levelMatch = levelRe.exec(line);
2456
+ if (levelMatch) {
2457
+ return {
2458
+ timestamp: "",
2459
+ level: levelMatch[1]?.toLowerCase() ?? "info",
2460
+ message: levelMatch[2] ?? line
2461
+ };
2462
+ }
2463
+ return {
2464
+ timestamp: "",
2465
+ level: "info",
2466
+ message: line
2467
+ };
2468
+ }
2469
+ var outdatedTool = {
2470
+ name: "outdated",
2471
+ category: "Package Management",
2472
+ description: "Check for outdated npm packages. Shows current, wanted, and latest versions.",
2473
+ usageHint: "Set `check` to filter specific packages. `format` as list or table. `include_deprecated` shows deprecated packages.",
2474
+ permission: "auto",
2475
+ mutating: false,
2476
+ timeoutMs: 6e4,
2477
+ inputSchema: {
2478
+ type: "object",
2479
+ properties: {
2480
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
2481
+ format: {
2482
+ type: "string",
2483
+ enum: ["list", "table"],
2484
+ description: "Output format (default: list)"
2485
+ },
2486
+ include_deprecated: {
2487
+ type: "boolean",
2488
+ description: "Include deprecated packages (default: false)"
2489
+ },
2490
+ check: {
2491
+ type: "string",
2492
+ description: "Specific package(s) to check (comma-separated)"
2493
+ }
2494
+ }
2495
+ },
2496
+ async execute(input, ctx, opts) {
2497
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
2498
+ const manager = await detectManager2(cwd);
2499
+ const args = ["outdated", "--json"];
2500
+ if (input.format === "table") args.push("--table");
2501
+ if (input.include_deprecated) args.push("--include", "deprecated");
2502
+ return runOutdated(manager, args, cwd, opts.signal);
2503
+ }
2504
+ };
2505
+ async function detectManager2(cwd) {
2506
+ const { stat: stat9 } = __require("fs/promises");
2507
+ try {
2508
+ await stat9(`${cwd}/pnpm-lock.yaml`);
2509
+ return "pnpm";
2510
+ } catch {
2511
+ }
2512
+ try {
2513
+ await stat9(`${cwd}/yarn.lock`);
2514
+ return "yarn";
2515
+ } catch {
2516
+ }
2517
+ return "npm";
2518
+ }
2519
+ function runOutdated(manager, args, cwd, signal) {
2520
+ return new Promise((resolve4) => {
2521
+ let stdout = "";
2522
+ let stderr = "";
2523
+ const MAX = 1e5;
2524
+ const child = spawn(manager, args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
2525
+ child.stdout?.on("data", (c) => {
2526
+ if (stdout.length < MAX) stdout += c.toString();
2527
+ });
2528
+ child.stderr?.on("data", (c) => {
2529
+ if (stderr.length < MAX) stderr += c.toString();
2530
+ });
2531
+ child.on("close", (code) => {
2532
+ const result = parseOutdatedOutput(stdout, code ?? 0);
2533
+ resolve4(result);
2534
+ });
2535
+ child.on(
2536
+ "error",
2537
+ (e) => resolve4({
2538
+ exit_code: 1,
2539
+ packages: [],
2540
+ total: 0,
2541
+ output: e.message,
2542
+ truncated: false
2543
+ })
2544
+ );
2545
+ });
2546
+ }
2547
+ function parseOutdatedOutput(json, exitCode) {
2548
+ const packages = [];
2549
+ if (!json) {
2550
+ return {
2551
+ exit_code: exitCode,
2552
+ packages: [],
2553
+ total: 0,
2554
+ output: exitCode === 0 ? "All packages up to date" : "Could not check outdated packages",
2555
+ truncated: false
2556
+ };
2557
+ }
2558
+ try {
2559
+ const data = JSON.parse(json);
2560
+ for (const name of Object.keys(data)) {
2561
+ const info = data[name];
2562
+ packages.push({
2563
+ name,
2564
+ current: info.current ?? "unknown",
2565
+ latest: info.latest ?? "unknown",
2566
+ wanted: info.wanted ?? "unknown",
2567
+ type: info.type ?? "unknown",
2568
+ location: info.location ?? name
2569
+ });
2570
+ }
2571
+ } catch {
2572
+ }
2573
+ return {
2574
+ exit_code: exitCode,
2575
+ packages,
2576
+ total: packages.length,
2577
+ output: json,
2578
+ truncated: json.length >= 1e5
2579
+ };
2580
+ }
2581
+ var patchTool = {
2582
+ name: "patch",
2583
+ category: "Filesystem",
2584
+ description: "Apply a unified diff patch to files. Writes .orig and .rej files on failure.",
2585
+ usageHint: "Set `patch` (the diff text). `directory` defaults to cwd. `strip` removes leading path components. `dry_run` previews.",
2586
+ permission: "confirm",
2587
+ mutating: true,
2588
+ timeoutMs: 3e4,
2589
+ inputSchema: {
2590
+ type: "object",
2591
+ properties: {
2592
+ patch: { type: "string", description: "Unified diff patch content" },
2593
+ directory: { type: "string", description: "Root directory for patch (default: cwd)" },
2594
+ strip: { type: "integer", description: "Strip leading path components (default: 1)" },
2595
+ dry_run: { type: "boolean", description: "Preview without applying" }
2596
+ },
2597
+ required: ["patch"]
2598
+ },
2599
+ async execute(input, ctx, opts) {
2600
+ if (!input?.patch) throw new Error("patch: patch content is required");
2601
+ const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;
2602
+ const strip = Math.max(1, input.strip ?? 1);
2603
+ const dryRun = input.dry_run ?? false;
2604
+ const targets = extractDiffTargets(input.patch);
2605
+ for (const t of targets) {
2606
+ const stripped = stripPathComponents(t, strip);
2607
+ if (!stripped) continue;
2608
+ const candidate = path.resolve(dir, stripped);
2609
+ const rel = path.relative(ctx.projectRoot, candidate);
2610
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
2611
+ return {
2612
+ applied: 0,
2613
+ rejected: 1,
2614
+ files: [],
2615
+ dry_run: dryRun,
2616
+ message: `patch refused: target "${t}" resolves outside project root`
2617
+ };
2618
+ }
2619
+ }
2620
+ const tmpDir = await fs9.mkdtemp(path.join(dir, ".wstack_patch_"));
2621
+ try {
2622
+ await fs9.chmod(tmpDir, 448).catch(() => {
2623
+ });
2624
+ const patchFile = path.join(tmpDir, "in.diff");
2625
+ await fs9.writeFile(patchFile, input.patch, { mode: 384 });
2626
+ const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
2627
+ const result = await runPatch(args, dir, opts.signal);
2628
+ if (result.exitCode !== 0 && !dryRun) {
2629
+ return {
2630
+ applied: 0,
2631
+ rejected: 1,
2632
+ files: [],
2633
+ dry_run: dryRun,
2634
+ message: `patch failed: ${result.stderr || result.stdout}`
2635
+ };
2636
+ }
2637
+ const patched = extractPatchedFiles(result.stdout);
2638
+ return {
2639
+ applied: patched.length,
2640
+ rejected: 0,
2641
+ files: patched,
2642
+ dry_run: dryRun,
2643
+ message: result.stdout || "patch applied"
2644
+ };
2645
+ } finally {
2646
+ await fs9.rm(tmpDir, { recursive: true, force: true }).catch(() => {
2647
+ });
2648
+ }
2649
+ }
2650
+ };
2651
+ function extractDiffTargets(patch) {
2652
+ const out = [];
2653
+ const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
2654
+ for (const m of patch.matchAll(re)) {
2655
+ const target = m[1]?.trim();
2656
+ if (!target || target === "/dev/null") continue;
2657
+ out.push(target);
2658
+ }
2659
+ return out;
2660
+ }
2661
+ function stripPathComponents(p, strip) {
2662
+ const parts = p.replace(/\\/g, "/").split("/");
2663
+ if (parts.length <= strip) return void 0;
2664
+ return parts.slice(strip).join("/");
2665
+ }
2666
+ function runPatch(args, cwd, signal) {
2667
+ return new Promise((resolve4) => {
2668
+ let stdout = "";
2669
+ let stderr = "";
2670
+ const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
2671
+ const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
2672
+ child.stdout?.on("data", (c) => {
2673
+ stdout += c.toString();
2674
+ });
2675
+ child.stderr?.on("data", (c) => {
2676
+ stderr += c.toString();
2677
+ });
2678
+ child.on("close", (code) => resolve4({ exitCode: code ?? 1, stdout, stderr }));
2679
+ child.on("error", (e) => resolve4({ exitCode: 1, stdout: "", stderr: e.message }));
2680
+ });
2681
+ }
2682
+ function extractPatchedFiles(output) {
2683
+ const files = [];
2684
+ const re = /patching file (.+)/gi;
2685
+ for (const m of output.matchAll(re)) {
2686
+ if (m[1]) files.push(m[1]);
2687
+ }
2688
+ return files;
2689
+ }
2690
+ var planTool = {
2691
+ name: "plan",
2692
+ category: "Session",
2693
+ description: "Inspect or edit the strategic plan board for this session. Plans persist across resume (unlike todos). Use this to lay out the multi-step approach before diving in, then mark steps in_progress/done as the work proceeds.",
2694
+ usageHint: "Set action to one of: show | add | start | done | remove | clear. Pass `title` for add. Pass `target` (item id, 1-based index, or title substring) for start/done/remove. Always returns the formatted plan plus open/total counts.",
2695
+ permission: "auto",
2696
+ mutating: false,
2697
+ timeoutMs: 2e3,
2698
+ inputSchema: {
2699
+ type: "object",
2700
+ properties: {
2701
+ action: {
2702
+ type: "string",
2703
+ enum: ["show", "add", "start", "done", "remove", "clear"]
2704
+ },
2705
+ title: { type: "string", description: "Required when action = add." },
2706
+ details: { type: "string", description: "Optional extra context for add." },
2707
+ target: {
2708
+ type: "string",
2709
+ description: "Plan item id, 1-based index, or title substring. Required for start/done/remove."
2710
+ }
2711
+ },
2712
+ required: ["action"]
2713
+ },
2714
+ async execute(input, ctx) {
2715
+ const planPath = ctx.meta["plan.path"];
2716
+ if (typeof planPath !== "string" || !planPath) {
2717
+ return {
2718
+ ok: false,
2719
+ message: "Plan storage path is not configured for this session.",
2720
+ plan: "",
2721
+ count: 0,
2722
+ open: 0
2723
+ };
2724
+ }
2725
+ const sessionId = ctx.session?.id ?? "unknown";
2726
+ let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
2727
+ switch (input.action) {
2728
+ case "show":
2729
+ break;
2730
+ case "add": {
2731
+ const title = input.title?.trim();
2732
+ if (!title) {
2733
+ return mkResult(plan, false, "add requires `title`.");
2734
+ }
2735
+ ({ plan } = addPlanItem(plan, title, input.details?.trim() || void 0));
2736
+ await savePlan(planPath, plan);
2737
+ break;
2738
+ }
2739
+ case "start":
2740
+ case "done": {
2741
+ if (!input.target) {
2742
+ return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
2743
+ }
2744
+ const next = setPlanItemStatus(
2745
+ plan,
2746
+ input.target,
2747
+ input.action === "start" ? "in_progress" : "done"
2748
+ );
2749
+ if (next === plan) {
2750
+ return mkResult(plan, false, `No plan item matched "${input.target}".`);
2751
+ }
2752
+ plan = next;
2753
+ await savePlan(planPath, plan);
2754
+ break;
2755
+ }
2756
+ case "remove": {
2757
+ if (!input.target) {
2758
+ return mkResult(plan, false, "remove requires `target` (id|index|substring).");
2759
+ }
2760
+ const next = removePlanItem(plan, input.target);
2761
+ if (next === plan) {
2762
+ return mkResult(plan, false, `No plan item matched "${input.target}".`);
2763
+ }
2764
+ plan = next;
2765
+ await savePlan(planPath, plan);
2766
+ break;
2767
+ }
2768
+ case "clear":
2769
+ plan = clearPlan(plan);
2770
+ await savePlan(planPath, plan);
2771
+ break;
2772
+ default:
2773
+ return mkResult(plan, false, `Unknown action "${input.action}".`);
2774
+ }
2775
+ return mkResult(plan, true, `Plan ${input.action} ok.`);
2776
+ }
2777
+ };
2778
+ function mkResult(plan, ok, message) {
2779
+ const open = plan.items.filter((i) => i.status !== "done").length;
2780
+ return {
2781
+ ok,
2782
+ message,
2783
+ plan: formatPlan(plan),
2784
+ count: plan.items.length,
2785
+ open
2786
+ };
2787
+ }
2788
+ var MAX_BYTES2 = 5 * 1024 * 1024;
2789
+ var readTool = {
2790
+ name: "read",
2791
+ category: "Filesystem",
2792
+ description: "Read the contents of a file. Lines are 1-indexed and prefixed with line numbers.",
2793
+ usageHint: "Read a file before editing it. Returns lines numbered like ` 1\u2192content`. Use `offset` and `limit` for large files (default reads up to 2000 lines).",
2794
+ permission: "auto",
2795
+ mutating: false,
2796
+ maxOutputBytes: 262144,
2797
+ timeoutMs: 5e3,
2798
+ inputSchema: {
2799
+ type: "object",
2800
+ properties: {
2801
+ path: { type: "string", description: "File path (absolute or relative to cwd)" },
2802
+ offset: { type: "integer", description: "1-based line number to start from" },
2803
+ limit: { type: "integer", description: "Max lines to read (default 2000)" }
2804
+ },
2805
+ required: ["path"]
2806
+ },
2807
+ async execute(input, ctx) {
2808
+ if (!input?.path) throw new Error("read: path is required");
2809
+ const absPath = safeResolve(input.path, ctx);
2810
+ const stat9 = await fs9.stat(absPath);
2811
+ if (!stat9.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
2812
+ if (stat9.size > MAX_BYTES2) {
2813
+ throw new Error(`read: file too large (${stat9.size} bytes, limit ${MAX_BYTES2})`);
2814
+ }
2815
+ const buf = await fs9.readFile(absPath);
2816
+ if (isBinaryBuffer(buf)) {
2817
+ throw new Error(`read: "${input.path}" appears to be binary`);
2818
+ }
2819
+ const text = buf.toString("utf8");
2820
+ const allLines = text.split(/\r\n|\r|\n/);
2821
+ const total = allLines.length;
2822
+ const offset = Math.max(1, input.offset ?? 1);
2823
+ const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
2824
+ if (limit === 0) {
2825
+ ctx.recordRead(absPath, stat9.mtimeMs);
2826
+ return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
2827
+ }
2828
+ const slice = allLines.slice(offset - 1, offset - 1 + limit);
2829
+ const truncated = offset - 1 + slice.length < total;
2830
+ const width = String(offset + slice.length - 1).length;
2831
+ const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
2832
+ ctx.recordRead(absPath, stat9.mtimeMs);
2833
+ return {
2834
+ text: numbered,
2835
+ total_lines: total,
2836
+ encoding: "utf8",
2837
+ truncated
2838
+ };
2839
+ }
2840
+ };
2841
+ var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
2842
+ var replaceTool = {
2843
+ name: "replace",
2844
+ category: "Transform",
2845
+ description: "Batch replace a pattern across multiple files matched by glob. Returns diff for each modified file.",
2846
+ usageHint: 'Use `glob` for broad patterns (e.g. "**/*.ts"). Set `dry_run: true` to preview without modifying. `files` can be a single path, comma-separated list, or glob pattern.',
2847
+ permission: "confirm",
2848
+ mutating: true,
2849
+ timeoutMs: 3e4,
2850
+ inputSchema: {
2851
+ type: "object",
2852
+ properties: {
2853
+ pattern: { type: "string", description: "Regex pattern to match" },
2854
+ replacement: { type: "string", description: "Replacement string" },
2855
+ files: {
2856
+ type: "string",
2857
+ description: "File(s) to target: single path, comma-separated list, or glob pattern"
2858
+ },
2859
+ glob: { type: "string", description: 'Additional glob filter (e.g. "*.ts")' },
2860
+ replace_all: {
2861
+ type: "boolean",
2862
+ description: "Replace all occurrences in each file (default: true)"
2863
+ },
2864
+ dry_run: { type: "boolean", description: "Preview changes without writing" }
2865
+ },
2866
+ required: ["pattern", "replacement", "files"]
2867
+ },
2868
+ async execute(input, ctx) {
2869
+ if (!input?.pattern) throw new Error("replace: pattern is required");
2870
+ if (input.replacement === void 0) throw new Error("replace: replacement is required");
2871
+ if (!input?.files) throw new Error("replace: files is required");
2872
+ const replaceAll = input.replace_all ?? true;
2873
+ const compiled = compileUserRegex(input.pattern, "g");
2874
+ if (!compiled.ok) {
2875
+ throw new Error(`replace: ${compiled.reason}`);
2876
+ }
2877
+ const re = compiled.regex;
2878
+ const globRe = input.glob ? compileGlob(input.glob) : null;
2879
+ const dryRun = input.dry_run ?? false;
2880
+ const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
2881
+ const fileList = await resolveFiles2(filesInput, ctx, globRe);
2882
+ const results = [];
2883
+ let totalReplacements = 0;
2884
+ for (const absPath of fileList) {
2885
+ const lstat2 = await fs9.lstat(absPath).catch((err) => {
2886
+ if (err.code === "ENOENT") return null;
2887
+ throw err;
2888
+ });
2889
+ if (!lstat2 || !lstat2.isFile()) continue;
2890
+ if (lstat2.isSymbolicLink()) continue;
2891
+ let realPath;
2892
+ try {
2893
+ realPath = await fs9.realpath(absPath);
2894
+ } catch {
2895
+ continue;
2896
+ }
2897
+ const rel = path.relative(ctx.projectRoot, realPath);
2898
+ if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
2899
+ const stat9 = await fs9.stat(realPath).catch(() => null);
2900
+ if (!stat9 || !stat9.isFile()) continue;
2901
+ let content;
2902
+ try {
2903
+ const buf = await fs9.readFile(realPath);
2904
+ if (isBinaryBuffer(buf)) continue;
2905
+ content = buf.toString("utf8");
2906
+ } catch {
2907
+ continue;
2908
+ }
2909
+ const style = detectNewlineStyle(content);
2910
+ const contentLf = normalizeToLf(content);
2911
+ re.lastIndex = 0;
2912
+ const allMatches = [...contentLf.matchAll(re)];
2913
+ if (allMatches.length === 0) continue;
2914
+ const matches = replaceAll ? allMatches : allMatches.slice(0, 1);
2915
+ const count = matches.length;
2916
+ let newContentLf = contentLf;
2917
+ for (let i = matches.length - 1; i >= 0; i--) {
2918
+ const m = matches[i];
2919
+ newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(m.index + m[0].length);
2920
+ }
2921
+ re.lastIndex = 0;
2922
+ totalReplacements += count;
2923
+ if (!dryRun) {
2924
+ const newContent = toStyle(newContentLf, style);
2925
+ await atomicWrite(realPath, newContent, { mode: stat9.mode & 511 });
2926
+ }
2927
+ const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
2928
+ fromFile: absPath,
2929
+ toFile: absPath
2930
+ }) : void 0;
2931
+ results.push({
2932
+ path: absPath,
2933
+ replacements: matches.length,
2934
+ diff
2935
+ });
2936
+ }
2937
+ return {
2938
+ files_modified: results.length,
2939
+ total_replacements: totalReplacements,
2940
+ results,
2941
+ dry_run: dryRun
2942
+ };
2943
+ }
2944
+ };
2945
+ async function resolveFiles2(filesInput, ctx, extraGlob) {
2946
+ const base = ctx.cwd;
2947
+ const normalized = filesInput.trim();
2948
+ if (normalized.startsWith("**/") || normalized.startsWith("*") || normalized.includes("**")) {
2949
+ return await globFiles(normalized, base, extraGlob);
2950
+ }
2951
+ const parts = normalized.split(",").map((s) => s.trim()).filter(Boolean);
2952
+ const resolved = [];
2953
+ for (const p of parts) {
2954
+ const absPath = safeResolve(p, ctx);
2955
+ const stat9 = await fs9.stat(absPath).catch(() => null);
2956
+ if (stat9?.isFile()) {
2957
+ resolved.push(absPath);
2958
+ }
2959
+ }
2960
+ return resolved;
2961
+ }
2962
+ async function globFiles(pattern, base, extraGlob) {
2963
+ const { spawn: spawn11 } = await import('child_process');
2964
+ const rgAvailable = await checkRg();
2965
+ if (rgAvailable) {
2966
+ try {
2967
+ const { promise } = spawnRgFind(pattern, base);
2968
+ return await promise;
2969
+ } catch {
2970
+ }
2971
+ }
2972
+ return await globNative(pattern, base, extraGlob);
2973
+ }
2974
+ function checkRg() {
2975
+ return new Promise((resolve4) => {
2976
+ try {
2977
+ const p = spawn("rg", ["--version"], { stdio: "ignore" });
2978
+ p.on("error", () => resolve4(false));
2979
+ p.on("close", (code) => resolve4(code === 0));
2980
+ } catch {
2981
+ resolve4(false);
2982
+ }
2983
+ });
2984
+ }
2985
+ function spawnRgFind(pattern, base) {
2986
+ const args = ["--files", "--glob", pattern, base];
2987
+ const child = spawn("rg", args, { stdio: ["ignore", "pipe", "pipe"] });
2988
+ let buf = "";
2989
+ child.stdout?.on("data", (chunk) => {
2990
+ buf += chunk.toString();
2991
+ });
2992
+ return {
2993
+ promise: new Promise((resolve4, reject) => {
2994
+ child.on("error", reject);
2995
+ child.on("close", () => {
2996
+ resolve4(buf.split("\n").filter(Boolean));
2997
+ });
2998
+ })
2999
+ };
3000
+ }
3001
+ async function globNative(pattern, base, extraGlob) {
3002
+ const results = [];
3003
+ const globRe = compileGlob(pattern);
3004
+ const walk = async (dir) => {
3005
+ let entries;
3006
+ try {
3007
+ entries = await fs9.readdir(dir, { withFileTypes: true });
3008
+ } catch {
3009
+ return;
3010
+ }
3011
+ for (const e of entries) {
3012
+ if (DEFAULT_IGNORE3.includes(e.name)) continue;
3013
+ const full = path.join(dir, e.name);
3014
+ if (e.isSymbolicLink()) continue;
3015
+ if (e.isDirectory()) {
3016
+ await walk(full);
3017
+ } else if (e.isFile()) {
3018
+ const name = e.name;
3019
+ if (globRe.test(name) || globRe.test(full)) {
3020
+ if (extraGlob && !extraGlob.test(name) && !extraGlob.test(full)) continue;
3021
+ results.push(full);
3022
+ }
3023
+ globRe.lastIndex = 0;
3024
+ if (extraGlob) extraGlob.lastIndex = 0;
3025
+ }
3026
+ }
3027
+ };
3028
+ await walk(base);
3029
+ return results;
3030
+ }
3031
+ var BUILT_IN_TEMPLATES = {
3032
+ "npm-package": {
3033
+ description: "Basic npm package with ESM",
3034
+ files: {
3035
+ "package.json": JSON.stringify(
3036
+ {
3037
+ name: "{{name}}",
3038
+ version: "0.1.1",
3039
+ type: "module",
3040
+ main: "./dist/index.js",
3041
+ scripts: { build: "tsc", test: "vitest run" },
3042
+ devDependencies: { typescript: "^5.0.0" }
3043
+ },
3044
+ null,
3045
+ 2
3046
+ ),
3047
+ "tsconfig.json": JSON.stringify(
3048
+ {
3049
+ compilerOptions: { target: "ES2022", module: "ESNext", strict: true },
3050
+ include: ["src"]
3051
+ },
3052
+ null,
3053
+ 2
3054
+ ),
3055
+ "src/index.ts": `export function hello() {
3056
+ return 'Hello from {{name}}';
3057
+ }
3058
+ `,
3059
+ "src/index.test.ts": `import { hello } from './index';
3060
+ import { describe, it, expect } from 'vitest';
3061
+
3062
+ describe('hello', () => {
3063
+ it('returns greeting', () => {
3064
+ expect(hello()).toBe('Hello from {{name}}');
3065
+ });
3066
+ });
3067
+ `
3068
+ }
3069
+ },
3070
+ "cli-tool": {
3071
+ description: "CLI tool with argparse",
3072
+ files: {
3073
+ "package.json": JSON.stringify(
3074
+ {
3075
+ name: "{{name}}",
3076
+ version: "0.1.1",
3077
+ type: "module",
3078
+ bin: { "{{name}}": "./src/index.js" },
3079
+ scripts: { build: "tsc", start: "node dist/index.js" }
3080
+ },
3081
+ null,
3082
+ 2
3083
+ ),
3084
+ "src/index.ts": `#!/usr/bin/env node
3085
+
3086
+ async function main() {
3087
+ console.log('Hello from {{name}}');
3088
+ }
3089
+
3090
+ main();
3091
+ `
3092
+ }
3093
+ },
3094
+ "react-component": {
3095
+ description: "React component with TypeScript",
3096
+ files: {
3097
+ "{{name}}.tsx": `interface {{Name}}Props {
3098
+ className?: string;
3099
+ }
3100
+
3101
+ export function {{Name}}({ className }: {{Name}}Props) {
3102
+ return (
3103
+ <div className={className}>
3104
+ {{Name}} Component
3105
+ </div>
3106
+ );
3107
+ }
3108
+ `,
3109
+ "{{name}}.test.tsx": `import { render, screen } from '@testing-library/react';
3110
+ import { {{Name}} } from './{{Name}}';
3111
+
3112
+ describe('{{Name}}', () => {
3113
+ it('renders', () => {
3114
+ render(<{{Name}} />);
3115
+ expect(screen.getByText('{{Name}} Component')).toBeInTheDocument();
3116
+ });
3117
+ });
3118
+ `
3119
+ }
3120
+ }
3121
+ };
3122
+ var scaffoldTool = {
3123
+ name: "scaffold",
3124
+ category: "Project",
3125
+ description: "Generate boilerplate code from built-in templates or paths. Creates package.json, source files, tests.",
3126
+ usageHint: "Set `template` (npm-package, cli-tool, react-component) and `name`. `vars` for template variables. `dry_run` preview.",
3127
+ permission: "confirm",
3128
+ mutating: true,
3129
+ timeoutMs: 3e4,
3130
+ inputSchema: {
3131
+ type: "object",
3132
+ properties: {
3133
+ template: {
3134
+ type: "string",
3135
+ description: "Template name (npm-package, cli-tool, react-component) or path to template directory"
3136
+ },
3137
+ name: {
3138
+ type: "string",
3139
+ description: "Project/component name (used in generated files)"
3140
+ },
3141
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
3142
+ vars: {
3143
+ type: "object",
3144
+ additionalProperties: { type: "string" },
3145
+ description: "Template variables for custom templates"
3146
+ },
3147
+ dry_run: {
3148
+ type: "boolean",
3149
+ description: "Preview generated files without creating (default: false)"
3150
+ }
3151
+ },
3152
+ required: ["template", "name"]
3153
+ },
3154
+ async execute(input, ctx) {
3155
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
3156
+ const name = input.name;
3157
+ const vars = { name, ...input.vars };
3158
+ const builtIn = BUILT_IN_TEMPLATES[input.template];
3159
+ if (builtIn) {
3160
+ return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
3161
+ }
3162
+ return {
3163
+ template: input.template,
3164
+ name,
3165
+ files_created: 0,
3166
+ files: [],
3167
+ dry_run: input.dry_run ?? false,
3168
+ output: `Template "${input.template}" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(", ")}`
3169
+ };
3170
+ }
3171
+ };
3172
+ async function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
3173
+ const files = [];
3174
+ let filesCreated = 0;
3175
+ for (const [filePath, content] of Object.entries(templateFiles)) {
3176
+ const resolvedPath = substituteVars(filePath, name, vars);
3177
+ const fullPath = path.join(cwd, resolvedPath);
3178
+ if (!dryRun) {
3179
+ await fs9.mkdir(path.dirname(fullPath), { recursive: true });
3180
+ await fs9.writeFile(fullPath, substituteVars(content, name, vars), "utf8");
3181
+ }
3182
+ files.push(resolvedPath);
3183
+ filesCreated++;
3184
+ }
3185
+ return {
3186
+ template: "built-in",
3187
+ name,
3188
+ files_created: filesCreated,
3189
+ files,
3190
+ dry_run: dryRun,
3191
+ output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
3192
+ };
3193
+ }
3194
+ function substituteVars(content, name, vars) {
3195
+ let result = content;
3196
+ result = result.replace(/\{\{name\}\}/g, name.toLowerCase().replace(/\s+/g, "-"));
3197
+ result = result.replace(
3198
+ /\{\{Name\}\}/g,
3199
+ name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
3200
+ );
3201
+ for (const [k, v] of Object.entries(vars)) {
3202
+ result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
3203
+ }
3204
+ return result;
3205
+ }
3206
+
3207
+ // src/search.ts
3208
+ var DEFAULT_NUM = 10;
3209
+ var MAX_RESULTS = 50;
3210
+ var TIMEOUT_MS4 = 15e3;
3211
+ var searchTool = {
3212
+ name: "search",
3213
+ category: "Search",
3214
+ description: "Search the web for information. Returns title, URL, and snippet for each result.",
3215
+ usageHint: "Set `num_results` (1-50, default 10). Use `source` to pick engine: duckduckgo (default), google, bing.",
3216
+ permission: "confirm",
3217
+ mutating: false,
3218
+ timeoutMs: TIMEOUT_MS4,
3219
+ inputSchema: {
3220
+ type: "object",
3221
+ properties: {
3222
+ query: { type: "string", description: "Search query" },
3223
+ num_results: {
3224
+ type: "integer",
3225
+ description: "Number of results (1-50, default 10)",
3226
+ minimum: 1,
3227
+ maximum: MAX_RESULTS
3228
+ },
3229
+ source: {
3230
+ type: "string",
3231
+ enum: ["duckduckgo", "google", "bing"],
3232
+ description: "Search engine to use (default: duckduckgo)"
3233
+ }
3234
+ },
3235
+ required: ["query"]
3236
+ },
3237
+ async execute(input, ctx, opts) {
3238
+ let final;
3239
+ for await (const ev of searchTool.executeStream(input, ctx, opts)) {
3240
+ if (ev.type === "final") final = ev.output;
3241
+ }
3242
+ if (!final) throw new Error("search: stream ended without final event");
3243
+ return final;
3244
+ },
3245
+ async *executeStream(input, _ctx, opts) {
3246
+ if (!input?.query) throw new Error("search: query is required");
3247
+ const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));
3248
+ const source = input.source ?? "duckduckgo";
3249
+ yield {
3250
+ type: "log",
3251
+ text: `Querying ${source} for "${input.query}"\u2026`,
3252
+ data: { source, query: input.query }
3253
+ };
3254
+ let output;
3255
+ switch (source) {
3256
+ case "duckduckgo":
3257
+ output = await duckduckgoSearch(input.query, num, opts.signal);
3258
+ break;
3259
+ case "google":
3260
+ output = await googleSearch(input.query, num, opts.signal);
3261
+ break;
3262
+ case "bing":
3263
+ output = await bingSearch(input.query, num, opts.signal);
3264
+ break;
3265
+ default:
3266
+ throw new Error(`search: unknown source "${source}"`);
3267
+ }
3268
+ yield {
3269
+ type: "partial_output",
3270
+ text: `${output.results.length} results from ${output.source}`,
3271
+ data: { count: output.results.length }
3272
+ };
3273
+ yield { type: "final", output };
3274
+ }
3275
+ };
3276
+ async function duckduckgoSearch(query2, num, signal) {
3277
+ const encoded = encodeURIComponent(query2);
3278
+ const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
3279
+ const results = await fetchWithTimeout(url, signal, TIMEOUT_MS4).then((r) => r.text()).then((html) => parseDuckDuckGo(html, num)).catch(() => [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }]);
3280
+ return {
3281
+ query: query2,
3282
+ results,
3283
+ source: "duckduckgo",
3284
+ truncated: results.length >= num
3285
+ };
3286
+ }
3287
+ function takeFrom(iter, max) {
3288
+ const out = [];
3289
+ for (const item of iter) {
3290
+ if (out.length >= max) break;
3291
+ out.push(item);
3292
+ }
3293
+ return out;
3294
+ }
3295
+ function parseDuckDuckGo(html, num) {
3296
+ const results = [];
3297
+ const snippetRegex = /<a class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi;
3298
+ const snippet2Regex = /<a class="result-snippet"[^>]*>([^<]+)<\/a>/gi;
3299
+ const linkMatches = takeFrom(
3300
+ [...html.matchAll(snippetRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: m[1], title: stripTags2(m[2]) })),
3301
+ num
3302
+ );
3303
+ const snippetMatches = takeFrom(
3304
+ [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags2(m[1])),
3305
+ num
3306
+ );
3307
+ for (let i = 0; i < linkMatches.length && i < num; i++) {
3308
+ const entry = linkMatches[i];
3309
+ results.push({
3310
+ title: entry?.title ?? "",
3311
+ url: entry?.url ?? "",
3312
+ snippet: snippetMatches[i] ?? ""
3313
+ });
3314
+ }
3315
+ return results;
3316
+ }
3317
+ async function googleSearch(query2, num, signal) {
3318
+ const encoded = encodeURIComponent(query2);
3319
+ const url = `https://www.google.com/search?q=${encoded}&hl=en`;
3320
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS4).then((r) => r.text()).catch(() => "");
3321
+ const results = parseGoogleResults(html, num);
3322
+ return {
3323
+ query: query2,
3324
+ results,
3325
+ source: "google",
3326
+ truncated: results.length >= num
3327
+ };
3328
+ }
3329
+ function parseGoogleResults(html, num) {
3330
+ const results = [];
3331
+ const titleRegex = /<h3[^>]*class="[^"]*DKV84"[^>]*>([^<]+)<\/h3>/gi;
3332
+ const urlRegex = /<cite[^>]*>([^<]+)<\/cite>/gi;
3333
+ const snippetRegex = /<span[^>]*class="[^"]*aXCZ0b[^>]*>([^<]+)<\/span>/gi;
3334
+ const titles = takeFrom(
3335
+ [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags2(m[1])),
3336
+ num
3337
+ );
3338
+ const urls = takeFrom(
3339
+ [...html.matchAll(urlRegex)].filter((m) => m[1]).map((m) => stripTags2(m[1]).replace(/^\*(https?:\/\/[^\s]+).*$/, "$1")).filter((u) => u.startsWith("http")),
3340
+ num
3341
+ );
3342
+ const snippets = takeFrom(
3343
+ [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags2(m[1])),
3344
+ num
3345
+ );
3346
+ for (let i = 0; i < Math.min(titles.length, num); i++) {
3347
+ results.push({
3348
+ title: titles[i] ?? "",
3349
+ url: urls[i] ?? "",
3350
+ snippet: snippets[i] ?? ""
3351
+ });
3352
+ }
3353
+ return results;
3354
+ }
3355
+ async function bingSearch(query2, num, signal) {
3356
+ const encoded = encodeURIComponent(query2);
3357
+ const url = `https://www.bing.com/search?q=${encoded}`;
3358
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS4).then((r) => r.text()).catch(() => "");
3359
+ const results = parseBingResults(html, num);
3360
+ return {
3361
+ query: query2,
3362
+ results,
3363
+ source: "bing",
3364
+ truncated: results.length >= num
3365
+ };
3366
+ }
3367
+ function parseBingResults(html, num) {
3368
+ const results = [];
3369
+ const titleRegex = /<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>\s*<\/h2>/gi;
3370
+ const snippetRegex = /<p[^>]*class="[^"]*b_paractl[^"]*"[^>]*>([^<]+)<\/p>/gi;
3371
+ const entries = takeFrom(
3372
+ [...html.matchAll(titleRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: m[1], title: stripTags2(m[2]) })),
3373
+ num
3374
+ );
3375
+ const snippets = takeFrom(
3376
+ [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags2(m[1])),
3377
+ num
3378
+ );
3379
+ for (let i = 0; i < entries.length; i++) {
3380
+ results.push({
3381
+ title: entries[i]?.title ?? "",
3382
+ url: entries[i]?.url ?? "",
3383
+ snippet: snippets[i] ?? ""
3384
+ });
3385
+ }
3386
+ return results;
3387
+ }
3388
+ async function fetchWithTimeout(url, signal, timeoutMs) {
3389
+ const controller = new AbortController();
3390
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3391
+ const fetchSignal = anySignal(signal, controller.signal);
3392
+ try {
3393
+ const res = await fetch(url, {
3394
+ headers: {
3395
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
3396
+ },
3397
+ signal: fetchSignal
3398
+ });
3399
+ clearTimeout(timer);
3400
+ return res;
3401
+ } catch (e) {
3402
+ clearTimeout(timer);
3403
+ throw e;
3404
+ }
3405
+ }
3406
+ function anySignal(...signals) {
3407
+ const controller = new AbortController();
3408
+ for (const s of signals) {
3409
+ if (s.aborted) {
3410
+ controller.abort();
3411
+ break;
3412
+ }
3413
+ s.addEventListener("abort", () => controller.abort());
3414
+ }
3415
+ return controller.signal;
3416
+ }
3417
+ function stripTags2(html) {
3418
+ return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
3419
+ }
3420
+ var testTool = {
3421
+ name: "test",
3422
+ category: "Code Quality",
3423
+ description: "Run tests with vitest, jest, or mocha. Returns pass/fail counts and output.",
3424
+ usageHint: "Set `files` for specific tests. `watch` enables watch mode. `coverage` generates coverage report. `grep` filters by name.",
3425
+ permission: "confirm",
3426
+ mutating: false,
3427
+ timeoutMs: 12e4,
3428
+ inputSchema: {
3429
+ type: "object",
3430
+ properties: {
3431
+ files: {
3432
+ type: "string",
3433
+ description: 'Test files: single path, comma-separated list, or glob (e.g. "**/*.test.ts")'
3434
+ },
3435
+ runner: {
3436
+ type: "string",
3437
+ enum: ["vitest", "jest", "mocha", "auto"],
3438
+ description: "Test runner (default: auto-detect)"
3439
+ },
3440
+ watch: { type: "boolean", description: "Run in watch mode (default: false)" },
3441
+ coverage: { type: "boolean", description: "Generate coverage report (default: false)" },
3442
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
3443
+ grep: { type: "string", description: "Filter tests by name pattern (default: none)" },
3444
+ timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" }
3445
+ }
3446
+ },
3447
+ async execute(input, ctx, opts) {
3448
+ let final;
3449
+ for await (const ev of testTool.executeStream(input, ctx, opts)) {
3450
+ if (ev.type === "final") final = ev.output;
3451
+ }
3452
+ if (!final) throw new Error("test: stream ended without final event");
3453
+ return final;
3454
+ },
3455
+ async *executeStream(input, ctx, opts) {
3456
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
3457
+ const runner = input.runner ?? "auto";
3458
+ const detected = runner === "auto" ? await detectRunner(cwd) : runner;
3459
+ if (!detected) {
3460
+ yield {
3461
+ type: "final",
3462
+ output: {
3463
+ runner: "none",
3464
+ exit_code: 1,
3465
+ tests_run: 0,
3466
+ passed: 0,
3467
+ failed: 0,
3468
+ duration_ms: 0,
3469
+ output: "No test runner found (vitest.config.ts, jest.config.js, .mocharc.json)",
3470
+ truncated: false
3471
+ }
3472
+ };
3473
+ return;
3474
+ }
3475
+ yield { type: "log", text: `Running ${detected}\u2026`, data: { runner: detected } };
3476
+ const start = Date.now();
3477
+ const args = buildArgs2(detected, input);
3478
+ const result = yield* spawnStream({
3479
+ cmd: detected,
3480
+ args,
3481
+ cwd,
3482
+ signal: opts.signal,
3483
+ maxBytes: 2e5
3484
+ });
3485
+ const duration = Date.now() - start;
3486
+ yield { type: "final", output: parseResult(detected, result, duration) };
3487
+ }
3488
+ };
3489
+ async function detectRunner(cwd) {
3490
+ const { stat: stat9 } = await import('fs/promises');
3491
+ const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
3492
+ for (const f of candidates) {
3493
+ try {
3494
+ await stat9(path.join(cwd, f));
3495
+ if (f.includes("vitest")) return "vitest";
3496
+ if (f.includes("jest")) return "jest";
3497
+ if (f.includes("mocha")) return "mocha";
3498
+ } catch {
3499
+ }
3500
+ }
3501
+ return "vitest";
3502
+ }
3503
+ function buildArgs2(runner, input) {
3504
+ const args = [];
3505
+ const timeout = input.timeout ?? 3e4;
3506
+ switch (runner) {
3507
+ case "vitest":
3508
+ args.push("run", "--reporter=verbose");
3509
+ if (input.watch) {
3510
+ args[1] = "";
3511
+ args.push("watch");
3512
+ }
3513
+ if (input.coverage) args.push("--coverage");
3514
+ if (input.grep) args.push("--testNamePattern", input.grep);
3515
+ args.push("--testTimeout", String(timeout));
3516
+ break;
3517
+ case "jest":
3518
+ args.push("--verbose");
3519
+ if (input.watch) args.push("--watch");
3520
+ if (input.coverage) args.push("--coverage");
3521
+ if (input.grep) args.push("--testPathPattern", input.grep);
3522
+ args.push("--testTimeout", String(timeout));
3523
+ break;
3524
+ case "mocha":
3525
+ args.push("--reporter", "spec");
3526
+ if (input.grep) args.push("--grep", input.grep);
3527
+ args.push("--timeout", String(timeout));
3528
+ break;
3529
+ }
3530
+ if (input.files) {
3531
+ const files = Array.isArray(input.files) ? input.files : input.files.split(",");
3532
+ args.push("--", ...files.map((f) => f.trim()));
3533
+ }
3534
+ return args;
3535
+ }
3536
+ function parseResult(runner, result, duration) {
3537
+ const out = result.stdout + result.stderr;
3538
+ let tests_run = 0;
3539
+ let passed = 0;
3540
+ let failed = 0;
3541
+ if (runner === "vitest") {
3542
+ const passedMatch = out.match(/(\d+) passed/);
3543
+ const failedMatch = out.match(/(\d+) failed/);
3544
+ if (passedMatch?.[1]) passed = Number.parseInt(passedMatch[1], 10);
3545
+ if (failedMatch?.[1]) failed = Number.parseInt(failedMatch[1], 10);
3546
+ tests_run = passed + failed;
3547
+ } else if (runner === "jest") {
3548
+ const suitesMatch = out.match(/Test Suites:\s+(\d+)\s+total/);
3549
+ const passedMatch = out.match(/Tests:\s+(\d+)\s+passed/);
3550
+ const failedMatch = out.match(/Tests:\s+(\d+)\s+failed/);
3551
+ tests_run = Number.parseInt(suitesMatch?.[1] ?? "0", 10);
3552
+ passed = Number.parseInt(passedMatch?.[1] ?? "0", 10);
3553
+ failed = Number.parseInt(failedMatch?.[1] ?? "0", 10);
3554
+ }
3555
+ return {
3556
+ runner,
3557
+ exit_code: result.exitCode,
3558
+ tests_run,
3559
+ passed,
3560
+ failed,
3561
+ duration_ms: duration,
3562
+ output: result.stdout || result.error || "",
3563
+ truncated: result.truncated
3564
+ };
3565
+ }
3566
+
3567
+ // src/todo.ts
3568
+ var todoTool = {
3569
+ name: "todo",
3570
+ category: "Session",
3571
+ description: "Replace the current todo list with a new set of items.",
3572
+ usageHint: "Use for multi-step tasks. Replace the full list on each call. At most ONE task may be in_progress at a time. Items have id, content, status (pending|in_progress|completed), and optional activeForm.",
3573
+ permission: "auto",
3574
+ mutating: false,
3575
+ timeoutMs: 1e3,
3576
+ inputSchema: {
3577
+ type: "object",
3578
+ properties: {
3579
+ todos: {
3580
+ type: "array",
3581
+ items: {
3582
+ type: "object",
3583
+ properties: {
3584
+ id: { type: "string" },
3585
+ content: { type: "string" },
3586
+ status: { type: "string", enum: ["pending", "in_progress", "completed"] },
3587
+ activeForm: { type: "string" }
3588
+ },
3589
+ required: ["id", "content", "status"]
3590
+ }
3591
+ }
3592
+ },
3593
+ required: ["todos"]
3594
+ },
3595
+ async execute(input, ctx) {
3596
+ if (!Array.isArray(input?.todos)) {
3597
+ throw new Error("todo: todos must be an array");
3598
+ }
3599
+ const items = input.todos.filter((t) => Boolean(t?.id && t.content));
3600
+ const inProgress = items.filter((t) => t.status === "in_progress");
3601
+ if (inProgress.length > 1) {
3602
+ let seenInProgress = false;
3603
+ for (const item of items) {
3604
+ if (item.status === "in_progress") {
3605
+ if (seenInProgress) item.status = "pending";
3606
+ seenInProgress = true;
3607
+ }
3608
+ }
3609
+ }
3610
+ ctx.state.replaceTodos(items);
3611
+ return {
3612
+ count: items.length,
3613
+ in_progress: items.filter((t) => t.status === "in_progress").length
3614
+ };
3615
+ }
3616
+ };
3617
+
3618
+ // src/tool-help.ts
3619
+ var toolHelpTool = {
3620
+ name: "tool_help",
3621
+ category: "Meta",
3622
+ description: "Get help and usage information for a specific tool or list all available tools.",
3623
+ usageHint: "Set `tool` for specific help. Omit to list all tools. `format`: short (one-liner), full (schema), markdown (formatted).",
3624
+ permission: "auto",
3625
+ mutating: false,
3626
+ timeoutMs: 5e3,
3627
+ inputSchema: {
3628
+ type: "object",
3629
+ properties: {
3630
+ tool: {
3631
+ type: "string",
3632
+ description: "Tool name to get help for (omit for all tools)"
3633
+ },
3634
+ format: {
3635
+ type: "string",
3636
+ enum: ["short", "full", "markdown"],
3637
+ description: "Output format (default: short)"
3638
+ },
3639
+ include_examples: {
3640
+ type: "boolean",
3641
+ description: "Include usage examples in output (default: false)"
3642
+ }
3643
+ }
3644
+ },
3645
+ async execute(input, ctx) {
3646
+ const format = input.format ?? "short";
3647
+ const includeExamples = input.include_examples ?? false;
3648
+ if (input.tool) {
3649
+ const tool = ctx.tools.find((t) => t.name === input.tool);
3650
+ if (!tool) {
3651
+ return {
3652
+ tool: input.tool,
3653
+ help: `No tool found with name "${input.tool}"`,
3654
+ tools: [],
3655
+ total: 0
3656
+ };
3657
+ }
3658
+ return {
3659
+ tool: tool.name,
3660
+ help: formatToolHelp(tool, format, includeExamples),
3661
+ tools: [
3662
+ {
3663
+ name: tool.name,
3664
+ description: tool.description,
3665
+ usageHint: tool.usageHint ?? "",
3666
+ inputSchema: tool.inputSchema,
3667
+ permission: tool.permission,
3668
+ mutating: tool.mutating
3669
+ }
3670
+ ],
3671
+ total: 1
3672
+ };
3673
+ }
3674
+ const allTools = ctx.tools.map((t) => ({
3675
+ name: t.name,
3676
+ description: t.description,
3677
+ usageHint: t.usageHint ?? "",
3678
+ inputSchema: format === "full" ? t.inputSchema : void 0,
3679
+ permission: t.permission,
3680
+ mutating: t.mutating
3681
+ }));
3682
+ return {
3683
+ help: format === "markdown" ? formatAllToolsMarkdown(allTools) : formatAllToolsShort(allTools),
3684
+ tools: allTools,
3685
+ total: allTools.length
3686
+ };
3687
+ }
3688
+ };
3689
+ function formatToolHelp(tool, format, includeExamples) {
3690
+ const lines = [];
3691
+ if (format === "short") {
3692
+ lines.push(`${tool.name}: ${tool.description}`);
3693
+ if (tool.usageHint) lines.push(`Hint: ${tool.usageHint}`);
3694
+ return lines.join("\n");
3695
+ }
3696
+ if (format === "markdown") {
3697
+ lines.push(`## ${tool.name}`);
3698
+ lines.push("");
3699
+ lines.push(tool.description);
3700
+ lines.push("");
3701
+ lines.push("**Permission:** " + tool.permission);
3702
+ lines.push("**Mutating:** " + (tool.mutating ? "yes" : "no"));
3703
+ if (tool.usageHint) {
3704
+ lines.push("");
3705
+ lines.push("### Usage Hint");
3706
+ lines.push(tool.usageHint);
3707
+ }
3708
+ if (includeExamples && tool.inputSchema) {
3709
+ lines.push("");
3710
+ lines.push("### Input Schema");
3711
+ lines.push("```json");
3712
+ lines.push(JSON.stringify(tool.inputSchema, null, 2));
3713
+ lines.push("```");
3714
+ }
3715
+ return lines.join("\n");
3716
+ }
3717
+ lines.push(`Tool: ${tool.name}`);
3718
+ lines.push(`Description: ${tool.description}`);
3719
+ lines.push(`Permission: ${tool.permission}`);
3720
+ lines.push(`Mutating: ${tool.mutating}`);
3721
+ if (tool.usageHint) lines.push(`Usage: ${tool.usageHint}`);
3722
+ if (format === "full" && tool.inputSchema) {
3723
+ lines.push("Schema: " + JSON.stringify(tool.inputSchema, null, 2));
3724
+ }
3725
+ return lines.join("\n");
3726
+ }
3727
+ function formatAllToolsShort(tools) {
3728
+ return tools.map((t) => ` ${t.name.padEnd(16)} ${t.description}`).join("\n");
3729
+ }
3730
+ function formatAllToolsMarkdown(tools) {
3731
+ const lines = ["## Available Tools", ""];
3732
+ lines.push("| Tool | Description |");
3733
+ lines.push("|------|-------------|");
3734
+ for (const t of tools) {
3735
+ lines.push(`| \`${t.name}\` | ${t.description} |`);
3736
+ }
3737
+ return lines.join("\n");
3738
+ }
3739
+
3740
+ // src/tool-search.ts
3741
+ var toolSearchTool = {
3742
+ name: "tool_search",
3743
+ category: "Meta",
3744
+ description: "Search available tools by name, description, tags, permission level, or mutating flag.",
3745
+ usageHint: "Set `query` for keyword search. `tags` to filter by category. `permission` to filter by required permission. `mutating` to filter by mutating flag.",
3746
+ permission: "auto",
3747
+ mutating: false,
3748
+ timeoutMs: 1e3,
3749
+ inputSchema: {
3750
+ type: "object",
3751
+ properties: {
3752
+ query: {
3753
+ type: "string",
3754
+ description: "Search query for tool name or description"
3755
+ },
3756
+ tags: {
3757
+ type: "array",
3758
+ items: { type: "string" },
3759
+ description: 'Filter by tags (e.g. "filesystem", "network", "dev")'
3760
+ },
3761
+ permission: {
3762
+ type: "string",
3763
+ enum: ["auto", "confirm", "deny"],
3764
+ description: "Filter by required permission level"
3765
+ },
3766
+ mutating: {
3767
+ type: "boolean",
3768
+ description: "Filter by mutating flag (true=filters that modify, false=read-only)"
3769
+ },
3770
+ limit: {
3771
+ type: "integer",
3772
+ description: "Maximum results to return (default: 20)",
3773
+ minimum: 1,
3774
+ maximum: 100
3775
+ }
3776
+ }
3777
+ },
3778
+ async execute(input, ctx) {
3779
+ const limit = Math.min(input.limit ?? 20, 100);
3780
+ const tools = ctx.tools;
3781
+ const query2 = input.query?.toLowerCase() ?? "";
3782
+ const filtered = tools.filter((t) => {
3783
+ if (query2 && !t.name.toLowerCase().includes(query2) && !t.description.toLowerCase().includes(query2)) {
3784
+ return false;
3785
+ }
3786
+ if (input.permission && t.permission !== input.permission) {
3787
+ return false;
3788
+ }
3789
+ if (typeof input.mutating === "boolean" && t.mutating !== input.mutating) {
3790
+ return false;
3791
+ }
3792
+ return true;
3793
+ });
3794
+ const results = filtered.slice(0, limit).map((t) => ({
3795
+ name: t.name,
3796
+ description: t.description,
3797
+ permission: t.permission,
3798
+ mutating: t.mutating
3799
+ }));
3800
+ return {
3801
+ tools: results,
3802
+ total: filtered.length,
3803
+ truncated: filtered.length > limit
3804
+ };
3805
+ }
3806
+ };
3807
+
3808
+ // src/tool-use.ts
3809
+ var toolUseTool = {
3810
+ name: "tool_use",
3811
+ category: "Meta",
3812
+ description: "Execute a specific tool by name with given input. Useful when the agent knows exactly which tool to call.",
3813
+ usageHint: "Set `tool` with exact tool name and `input` with the tool parameters. Returns result or error.",
3814
+ permission: "confirm",
3815
+ mutating: true,
3816
+ timeoutMs: 6e4,
3817
+ inputSchema: {
3818
+ type: "object",
3819
+ properties: {
3820
+ tool: {
3821
+ type: "string",
3822
+ description: "Exact name of the tool to execute"
3823
+ },
3824
+ input: {
3825
+ type: "object",
3826
+ description: "Input parameters for the tool"
3827
+ }
3828
+ },
3829
+ required: ["tool"]
3830
+ },
3831
+ async execute(input, ctx, opts) {
3832
+ const start = Date.now();
3833
+ if (!input?.tool) {
3834
+ return {
3835
+ tool: "unknown",
3836
+ success: false,
3837
+ error: "tool_use: tool name is required",
3838
+ executionMs: 0
3839
+ };
3840
+ }
3841
+ const tool = ctx.tools.find((t) => t.name === input.tool);
3842
+ if (!tool) {
3843
+ return {
3844
+ tool: input.tool,
3845
+ success: false,
3846
+ error: `tool_use: tool "${input.tool}" not found`,
3847
+ executionMs: Date.now() - start
3848
+ };
3849
+ }
3850
+ if (tool.permission === "deny") {
3851
+ return {
3852
+ tool: input.tool,
3853
+ success: false,
3854
+ error: `tool_use: tool "${input.tool}" is denied by policy`,
3855
+ executionMs: Date.now() - start
3856
+ };
3857
+ }
3858
+ try {
3859
+ const result = await tool.execute(input.input, ctx, opts);
3860
+ return {
3861
+ tool: input.tool,
3862
+ success: true,
3863
+ result,
3864
+ executionMs: Date.now() - start
3865
+ };
3866
+ } catch (e) {
3867
+ return {
3868
+ tool: input.tool,
3869
+ success: false,
3870
+ error: e instanceof Error ? e.message : String(e),
3871
+ executionMs: Date.now() - start
3872
+ };
3873
+ }
3874
+ }
3875
+ };
3876
+ var DEFAULT_IGNORE4 = [
3877
+ "node_modules",
3878
+ ".git",
3879
+ "dist",
3880
+ "build",
3881
+ ".next",
3882
+ "coverage",
3883
+ "__pycache__"
3884
+ ];
3885
+ var treeTool = {
3886
+ name: "tree",
3887
+ category: "Filesystem",
3888
+ description: "Display directory structure as an ASCII tree. Shows files and folders with indentation.",
3889
+ usageHint: "Set `path` (default: cwd). `depth` limits nesting (default: 3). `glob` filters files. `exclude` ignores dirs. `show_files` toggles file listing (default: true).",
3890
+ permission: "auto",
3891
+ mutating: false,
3892
+ timeoutMs: 15e3,
3893
+ inputSchema: {
3894
+ type: "object",
3895
+ properties: {
3896
+ path: { type: "string", description: "Root directory (default: cwd)" },
3897
+ depth: {
3898
+ type: "integer",
3899
+ description: "Max nesting depth (default: 3, 0 for unlimited)",
3900
+ minimum: 0,
3901
+ maximum: 20
3902
+ },
3903
+ glob: { type: "string", description: 'Filter files matching glob (e.g. "*.ts")' },
3904
+ exclude: {
3905
+ type: "array",
3906
+ items: { type: "string" },
3907
+ description: "Directory names to exclude"
3908
+ },
3909
+ show_files: {
3910
+ type: "boolean",
3911
+ description: "Show files (default: true, false shows dirs only)"
3912
+ },
3913
+ show_dirs: {
3914
+ type: "boolean",
3915
+ description: "Show directories (default: true)"
3916
+ },
3917
+ show_hidden: {
3918
+ type: "boolean",
3919
+ description: "Show hidden files starting with . (default: false)"
3920
+ }
3921
+ }
3922
+ },
3923
+ async execute(input, ctx, opts) {
3924
+ let final;
3925
+ for await (const ev of treeTool.executeStream(input, ctx, opts)) {
3926
+ if (ev.type === "final") final = ev.output;
3927
+ }
3928
+ if (!final) throw new Error("tree: stream ended without final event");
3929
+ return final;
3930
+ },
3931
+ async *executeStream(input, ctx) {
3932
+ const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
3933
+ const maxDepth = input.depth ?? 3;
3934
+ const showFiles = input.show_files ?? true;
3935
+ const showDirs = input.show_dirs ?? true;
3936
+ const showHidden = input.show_hidden ?? false;
3937
+ const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE4, ...input.exclude ?? []]);
3938
+ const filterGlob = input.glob;
3939
+ const lines = [basePath];
3940
+ const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
3941
+ const queue = [];
3942
+ const FLUSH_EVERY = 200;
3943
+ let lastEmittedTotal = 0;
3944
+ const tickProgress = () => {
3945
+ const seen = totals.totalFiles.value + totals.totalDirs.value;
3946
+ if (seen - lastEmittedTotal >= FLUSH_EVERY) {
3947
+ queue.push({
3948
+ type: "metric",
3949
+ text: `${seen} entries`,
3950
+ data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value }
3951
+ });
3952
+ lastEmittedTotal = seen;
3953
+ }
3954
+ };
3955
+ const walkPromise = walkDir(basePath, 0, {
3956
+ maxDepth,
3957
+ exclude,
3958
+ showFiles,
3959
+ showDirs,
3960
+ showHidden,
3961
+ filterGlob,
3962
+ lines,
3963
+ prefix: "",
3964
+ isLast: true,
3965
+ totalFiles: totals.totalFiles,
3966
+ totalDirs: totals.totalDirs,
3967
+ onProgress: tickProgress
3968
+ });
3969
+ let walkDone = false;
3970
+ walkPromise.finally(() => {
3971
+ walkDone = true;
3972
+ });
3973
+ while (!walkDone || queue.length > 0) {
3974
+ if (queue.length > 0) {
3975
+ yield queue.shift();
3976
+ } else {
3977
+ let pollTimer;
3978
+ const poll = new Promise((r) => {
3979
+ pollTimer = setTimeout(r, 50);
3980
+ });
3981
+ try {
3982
+ await Promise.race([walkPromise, poll]).catch(() => void 0);
3983
+ } finally {
3984
+ if (pollTimer) clearTimeout(pollTimer);
3985
+ }
3986
+ }
3987
+ }
3988
+ await walkPromise;
3989
+ yield {
3990
+ type: "final",
3991
+ output: {
3992
+ tree: lines.join("\n"),
3993
+ total_files: totals.totalFiles.value,
3994
+ total_dirs: totals.totalDirs.value,
3995
+ truncated: false,
3996
+ path: basePath
3997
+ }
3998
+ };
3999
+ }
4000
+ };
4001
+ async function walkDir(dir, depth, opts) {
4002
+ const entries = await fs9.readdir(dir, { withFileTypes: true }).catch(() => []);
4003
+ const filtered = entries.filter((e) => {
4004
+ if (!opts.showHidden && e.name.startsWith(".")) return false;
4005
+ if (opts.exclude.has(e.name)) return false;
4006
+ return true;
4007
+ });
4008
+ if (depth > 0) {
4009
+ const dirCount = filtered.filter((e) => e.isDirectory()).length;
4010
+ const fileCount = filtered.filter((e) => e.isFile()).length;
4011
+ opts.totalDirs.value += dirCount;
4012
+ opts.totalFiles.value += fileCount;
4013
+ opts.onProgress?.();
4014
+ }
4015
+ const items = filtered.sort((a, b) => {
4016
+ if (a.isDirectory() && !b.isDirectory()) return -1;
4017
+ if (!a.isDirectory() && b.isDirectory()) return 1;
4018
+ return a.name.localeCompare(b.name);
4019
+ });
4020
+ for (let i = 0; i < items.length; i++) {
4021
+ const entry = items[i];
4022
+ if (!entry) continue;
4023
+ const isLast = i === items.length - 1;
4024
+ const connector = opts.isLast ? " " : "\u2502 ";
4025
+ const branch = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4026
+ const displayName = entry.name + (entry.isDirectory() ? "/" : "");
4027
+ if (!opts.showDirs && entry.isDirectory()) continue;
4028
+ if (!opts.showFiles && entry.isFile()) continue;
4029
+ opts.lines.push(opts.prefix + branch + displayName);
4030
+ if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
4031
+ const childPrefix = opts.prefix + connector;
4032
+ await walkDir(path.join(dir, entry.name), depth + 1, {
4033
+ ...opts,
4034
+ prefix: childPrefix,
4035
+ isLast
4036
+ });
4037
+ }
4038
+ }
4039
+ }
4040
+ var typecheckTool = {
4041
+ name: "typecheck",
4042
+ category: "Code Quality",
4043
+ description: "Run TypeScript type checking with `tsc --noEmit`. Checks for type errors without compiling.",
4044
+ usageHint: "Set `project` for tsconfig path (default: nearest). `strict` enables strictest flags. `all` checks all projects in workspace.",
4045
+ permission: "confirm",
4046
+ mutating: false,
4047
+ timeoutMs: 12e4,
4048
+ inputSchema: {
4049
+ type: "object",
4050
+ properties: {
4051
+ project: { type: "string", description: "Path to tsconfig.json (default: auto-detect)" },
4052
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
4053
+ strict: {
4054
+ type: "boolean",
4055
+ description: "Add --strict flag for maximum type checking (default: false)"
4056
+ },
4057
+ all: {
4058
+ type: "boolean",
4059
+ description: "Type-check all projects (pnpm -r) (default: false)"
4060
+ }
4061
+ }
4062
+ },
4063
+ async execute(input, ctx, opts) {
4064
+ let final;
4065
+ for await (const ev of typecheckTool.executeStream(input, ctx, opts)) {
4066
+ if (ev.type === "final") final = ev.output;
4067
+ }
4068
+ if (!final) throw new Error("typecheck: stream ended without final event");
4069
+ return final;
4070
+ },
4071
+ async *executeStream(input, ctx, opts) {
4072
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
4073
+ let args;
4074
+ let project;
4075
+ if (input.all) {
4076
+ args = ["--noEmit"];
4077
+ project = "workspace";
4078
+ } else {
4079
+ const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
4080
+ args = ["--noEmit"];
4081
+ if (input.strict) args.push("--strict");
4082
+ if (tsconfig) args.push("--project", tsconfig);
4083
+ project = tsconfig ?? "default";
4084
+ }
4085
+ yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
4086
+ const result = yield* spawnStream({
4087
+ cmd: "npx",
4088
+ args: ["tsc", ...args],
4089
+ cwd,
4090
+ signal: opts.signal,
4091
+ maxBytes: 2e5
4092
+ });
4093
+ const errors = (result.stdout.match(/error TS/g) || []).length;
4094
+ const warnings = (result.stdout.match(/warning/g) || []).length;
4095
+ yield {
4096
+ type: "final",
4097
+ output: {
4098
+ project,
4099
+ exit_code: result.exitCode,
4100
+ errors,
4101
+ warnings,
4102
+ output: result.stdout || result.stderr || result.error || "",
4103
+ truncated: result.truncated
4104
+ }
4105
+ };
4106
+ }
4107
+ };
4108
+ async function findTsConfig(cwd) {
4109
+ const { stat: stat9 } = await import('fs/promises');
4110
+ const candidates = ["tsconfig.json", "tsconfig.base.json"];
4111
+ for (const f of candidates) {
4112
+ try {
4113
+ const s = await stat9(path.join(cwd, f));
4114
+ if (s.isFile()) return path.join(cwd, f);
4115
+ } catch {
4116
+ }
4117
+ }
4118
+ return null;
4119
+ }
4120
+ var writeTool = {
4121
+ name: "write",
4122
+ category: "Filesystem",
4123
+ description: "Write or overwrite a file. For existing files, prefer `edit` over `write`.",
4124
+ usageHint: "Use `write` for new files or full replacements. For partial edits use `edit`. Existing files must have been `read` first in this session.",
4125
+ permission: "confirm",
4126
+ mutating: true,
4127
+ timeoutMs: 5e3,
4128
+ inputSchema: {
4129
+ type: "object",
4130
+ properties: {
4131
+ path: { type: "string" },
4132
+ content: { type: "string" }
4133
+ },
4134
+ required: ["path", "content"]
4135
+ },
4136
+ async execute(input, ctx) {
4137
+ if (!input?.path) throw new Error("write: path is required");
4138
+ if (input.content === void 0) throw new Error("write: content is required");
4139
+ const absPath = safeResolve(input.path, ctx);
4140
+ let existed = false;
4141
+ let prev = "";
4142
+ try {
4143
+ const stat10 = await fs9.stat(absPath);
4144
+ existed = stat10.isFile();
4145
+ if (existed) {
4146
+ if (!ctx.hasRead(absPath)) {
4147
+ throw new Error(
4148
+ `write: file "${input.path}" exists but was not read in this session. Read it first.`
4149
+ );
4150
+ }
4151
+ prev = await fs9.readFile(absPath, "utf8");
4152
+ }
4153
+ } catch (err) {
4154
+ if (err.code !== "ENOENT") {
4155
+ throw err;
4156
+ }
4157
+ }
4158
+ await atomicWrite(absPath, input.content);
4159
+ const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
4160
+ + (new file, ${input.content.split("\n").length} lines)`;
4161
+ const stat9 = await fs9.stat(absPath);
4162
+ ctx.recordRead(absPath, stat9.mtimeMs);
4163
+ return {
4164
+ path: absPath,
4165
+ bytes_written: Buffer.byteLength(input.content, "utf8"),
4166
+ created: !existed,
4167
+ diff
4168
+ };
4169
+ }
4170
+ };
4171
+
4172
+ // src/builtin.ts
4173
+ var builtinTools = [
4174
+ readTool,
4175
+ writeTool,
4176
+ editTool,
4177
+ replaceTool,
4178
+ globTool,
4179
+ grepTool,
4180
+ bashTool,
4181
+ execTool,
4182
+ fetchTool,
4183
+ searchTool,
4184
+ todoTool,
4185
+ planTool,
4186
+ gitTool,
4187
+ patchTool,
4188
+ jsonTool,
4189
+ diffTool,
4190
+ treeTool,
4191
+ lintTool,
4192
+ formatTool,
4193
+ typecheckTool,
4194
+ testTool,
4195
+ installTool,
4196
+ auditTool,
4197
+ outdatedTool,
4198
+ logsTool,
4199
+ documentTool,
4200
+ scaffoldTool,
4201
+ toolSearchTool,
4202
+ toolUseTool,
4203
+ batchToolUseTool,
4204
+ toolHelpTool
4205
+ ];
4206
+
4207
+ // src/pack.ts
4208
+ var builtinToolsPack = {
4209
+ name: "builtin-tools",
4210
+ description: "WrongStack built-in filesystem, execution, network, lifecycle, and agent-control tools.",
4211
+ tools: builtinTools
4212
+ };
4213
+
4214
+ export { builtinToolsPack };
4215
+ //# sourceMappingURL=pack.js.map
4216
+ //# sourceMappingURL=pack.js.map