@wrongstack/tools 0.1.1 → 0.1.3

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 (103) hide show
  1. package/dist/audit.d.ts +25 -0
  2. package/dist/audit.js +209 -0
  3. package/dist/audit.js.map +1 -0
  4. package/dist/bash.d.ts +16 -0
  5. package/dist/bash.js +180 -0
  6. package/dist/bash.js.map +1 -0
  7. package/dist/batch-tool-use.d.ts +26 -0
  8. package/dist/batch-tool-use.js +106 -0
  9. package/dist/batch-tool-use.js.map +1 -0
  10. package/dist/builtin.d.ts +5 -0
  11. package/dist/builtin.js +3735 -0
  12. package/dist/builtin.js.map +1 -0
  13. package/dist/diff.d.ts +20 -0
  14. package/dist/diff.js +142 -0
  15. package/dist/diff.js.map +1 -0
  16. package/dist/document.d.ts +27 -0
  17. package/dist/document.js +148 -0
  18. package/dist/document.js.map +1 -0
  19. package/dist/edit.d.ts +22 -0
  20. package/dist/edit.js +138 -0
  21. package/dist/edit.js.map +1 -0
  22. package/dist/exec.d.ts +21 -0
  23. package/dist/exec.js +159 -0
  24. package/dist/exec.js.map +1 -0
  25. package/dist/fetch.d.ts +15 -0
  26. package/dist/fetch.js +213 -0
  27. package/dist/fetch.js.map +1 -0
  28. package/dist/format.d.ts +18 -0
  29. package/dist/format.js +194 -0
  30. package/dist/format.js.map +1 -0
  31. package/dist/git.d.ts +27 -0
  32. package/dist/git.js +174 -0
  33. package/dist/git.js.map +1 -0
  34. package/dist/glob.d.ts +14 -0
  35. package/dist/glob.js +101 -0
  36. package/dist/glob.js.map +1 -0
  37. package/dist/grep.d.ts +20 -0
  38. package/dist/grep.js +264 -0
  39. package/dist/grep.js.map +1 -0
  40. package/dist/index.d.ts +34 -563
  41. package/dist/index.js +717 -442
  42. package/dist/index.js.map +1 -1
  43. package/dist/install.d.ts +19 -0
  44. package/dist/install.js +186 -0
  45. package/dist/install.js.map +1 -0
  46. package/dist/json.d.ts +20 -0
  47. package/dist/json.js +124 -0
  48. package/dist/json.js.map +1 -0
  49. package/dist/lint.d.ts +20 -0
  50. package/dist/lint.js +191 -0
  51. package/dist/lint.js.map +1 -0
  52. package/dist/logs.d.ts +27 -0
  53. package/dist/logs.js +180 -0
  54. package/dist/logs.js.map +1 -0
  55. package/dist/memory.d.ts +22 -0
  56. package/dist/memory.js +53 -0
  57. package/dist/memory.js.map +1 -0
  58. package/dist/mode.d.ts +20 -0
  59. package/dist/mode.js +81 -0
  60. package/dist/mode.js.map +1 -0
  61. package/dist/outdated.d.ts +26 -0
  62. package/dist/outdated.js +138 -0
  63. package/dist/outdated.js.map +1 -0
  64. package/dist/patch.d.ts +18 -0
  65. package/dist/patch.js +101 -0
  66. package/dist/patch.js.map +1 -0
  67. package/dist/read.d.ts +16 -0
  68. package/dist/read.js +81 -0
  69. package/dist/read.js.map +1 -0
  70. package/dist/replace.d.ts +23 -0
  71. package/dist/replace.js +196 -0
  72. package/dist/replace.js.map +1 -0
  73. package/dist/scaffold.d.ts +20 -0
  74. package/dist/scaffold.js +185 -0
  75. package/dist/scaffold.js.map +1 -0
  76. package/dist/search.d.ts +20 -0
  77. package/dist/search.js +212 -0
  78. package/dist/search.js.map +1 -0
  79. package/dist/test.d.ts +24 -0
  80. package/dist/test.js +247 -0
  81. package/dist/test.js.map +1 -0
  82. package/dist/todo.d.ts +12 -0
  83. package/dist/todo.js +53 -0
  84. package/dist/todo.js.map +1 -0
  85. package/dist/tool-help.d.ts +23 -0
  86. package/dist/tool-help.js +122 -0
  87. package/dist/tool-help.js.map +1 -0
  88. package/dist/tool-search.d.ts +22 -0
  89. package/dist/tool-search.js +70 -0
  90. package/dist/tool-search.js.map +1 -0
  91. package/dist/tool-use.d.ts +16 -0
  92. package/dist/tool-use.js +79 -0
  93. package/dist/tool-use.js.map +1 -0
  94. package/dist/tree.d.ts +21 -0
  95. package/dist/tree.js +176 -0
  96. package/dist/tree.js.map +1 -0
  97. package/dist/typecheck.d.ts +19 -0
  98. package/dist/typecheck.js +181 -0
  99. package/dist/typecheck.js.map +1 -0
  100. package/dist/write.d.ts +15 -0
  101. package/dist/write.js +77 -0
  102. package/dist/write.js.map +1 -0
  103. package/package.json +137 -4
@@ -0,0 +1,79 @@
1
+ // src/tool-use.ts
2
+ var toolUseTool = {
3
+ name: "tool_use",
4
+ description: "Execute a specific tool by name with given input. Useful when the agent knows exactly which tool to call.",
5
+ usageHint: "Set `tool` with exact tool name and `input` with the tool parameters. Returns result or error.",
6
+ permission: "confirm",
7
+ mutating: false,
8
+ timeoutMs: 6e4,
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: {
12
+ tool: {
13
+ type: "string",
14
+ description: "Exact name of the tool to execute"
15
+ },
16
+ input: {
17
+ type: "object",
18
+ description: "Input parameters for the tool"
19
+ }
20
+ },
21
+ required: ["tool"]
22
+ },
23
+ async execute(input, ctx, opts) {
24
+ const start = Date.now();
25
+ if (!input?.tool) {
26
+ return {
27
+ tool: "unknown",
28
+ success: false,
29
+ error: "tool_use: tool name is required",
30
+ executionMs: 0
31
+ };
32
+ }
33
+ const tool = ctx.tools.find((t) => t.name === input.tool);
34
+ if (!tool) {
35
+ return {
36
+ tool: input.tool,
37
+ success: false,
38
+ error: `tool_use: tool "${input.tool}" not found`,
39
+ executionMs: Date.now() - start
40
+ };
41
+ }
42
+ if (tool.permission === "deny") {
43
+ return {
44
+ tool: input.tool,
45
+ success: false,
46
+ error: `tool_use: tool "${input.tool}" is denied by policy`,
47
+ executionMs: Date.now() - start
48
+ };
49
+ }
50
+ if (tool.permission === "confirm" && input.input) {
51
+ return {
52
+ tool: input.tool,
53
+ success: false,
54
+ error: `tool_use: tool "${input.tool}" requires confirmation`,
55
+ executionMs: Date.now() - start
56
+ };
57
+ }
58
+ try {
59
+ const result = await tool.execute(input.input, ctx, opts);
60
+ return {
61
+ tool: input.tool,
62
+ success: true,
63
+ result,
64
+ executionMs: Date.now() - start
65
+ };
66
+ } catch (e) {
67
+ return {
68
+ tool: input.tool,
69
+ success: false,
70
+ error: e instanceof Error ? e.message : String(e),
71
+ executionMs: Date.now() - start
72
+ };
73
+ }
74
+ }
75
+ };
76
+
77
+ export { toolUseTool };
78
+ //# sourceMappingURL=tool-use.js.map
79
+ //# sourceMappingURL=tool-use.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tool-use.ts"],"names":[],"mappings":"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","file":"tool-use.js","sourcesContent":["import type { Tool } from '@wrongstack/core';\n\ninterface ToolUseInput {\n tool: string;\n input: Record<string, unknown>;\n}\n\ninterface ToolUseOutput {\n tool: string;\n success: boolean;\n result?: unknown;\n error?: string;\n executionMs: number;\n}\n\nexport const toolUseTool: Tool<ToolUseInput, ToolUseOutput> = {\n name: 'tool_use',\n description:\n 'Execute a specific tool by name with given input. Useful when the agent knows exactly which tool to call.',\n usageHint:\n 'Set `tool` with exact tool name and `input` with the tool parameters. Returns result or error.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 60_000,\n inputSchema: {\n type: 'object',\n properties: {\n tool: {\n type: 'string',\n description: 'Exact name of the tool to execute',\n },\n input: {\n type: 'object',\n description: 'Input parameters for the tool',\n },\n },\n required: ['tool'],\n },\n async execute(input, ctx, opts) {\n const start = Date.now();\n\n if (!input?.tool) {\n return {\n tool: 'unknown',\n success: false,\n error: 'tool_use: tool name is required',\n executionMs: 0,\n };\n }\n\n const tool = ctx.tools.find((t: Tool) => t.name === input.tool);\n if (!tool) {\n return {\n tool: input.tool,\n success: false,\n error: `tool_use: tool \"${input.tool}\" not found`,\n executionMs: Date.now() - start,\n };\n }\n\n if (tool.permission === 'deny') {\n return {\n tool: input.tool,\n success: false,\n error: `tool_use: tool \"${input.tool}\" is denied by policy`,\n executionMs: Date.now() - start,\n };\n }\n\n if (tool.permission === 'confirm' && input.input) {\n return {\n tool: input.tool,\n success: false,\n error: `tool_use: tool \"${input.tool}\" requires confirmation`,\n executionMs: Date.now() - start,\n };\n }\n\n try {\n const result = await tool.execute(input.input, ctx, opts);\n return {\n tool: input.tool,\n success: true,\n result,\n executionMs: Date.now() - start,\n };\n } catch (e) {\n return {\n tool: input.tool,\n success: false,\n error: e instanceof Error ? e.message : String(e),\n executionMs: Date.now() - start,\n };\n }\n },\n};"]}
package/dist/tree.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface TreeInput {
4
+ path?: string;
5
+ depth?: number;
6
+ glob?: string;
7
+ exclude?: string[];
8
+ show_files?: boolean;
9
+ show_dirs?: boolean;
10
+ show_hidden?: boolean;
11
+ }
12
+ interface TreeOutput {
13
+ tree: string;
14
+ total_files: number;
15
+ total_dirs: number;
16
+ truncated: boolean;
17
+ path: string;
18
+ }
19
+ declare const treeTool: Tool<TreeInput, TreeOutput>;
20
+
21
+ export { treeTool };
package/dist/tree.js ADDED
@@ -0,0 +1,176 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import 'child_process';
4
+
5
+ // src/tree.ts
6
+ function resolvePath(input, ctx) {
7
+ return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
8
+ }
9
+ function ensureInsideRoot(absPath, ctx) {
10
+ const root = path.resolve(ctx.projectRoot);
11
+ const target = path.resolve(absPath);
12
+ const rel = path.relative(root, target);
13
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
14
+ throw new Error(`Path "${absPath}" is outside project root "${root}"`);
15
+ }
16
+ return target;
17
+ }
18
+ function safeResolve(input, ctx) {
19
+ return ensureInsideRoot(resolvePath(input, ctx), ctx);
20
+ }
21
+
22
+ // src/tree.ts
23
+ var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage", "__pycache__"];
24
+ var treeTool = {
25
+ name: "tree",
26
+ description: "Display directory structure as an ASCII tree. Shows files and folders with indentation.",
27
+ usageHint: "Set `path` (default: cwd). `depth` limits nesting (default: 3). `glob` filters files. `exclude` ignores dirs. `show_files` toggles file listing (default: true).",
28
+ permission: "auto",
29
+ mutating: false,
30
+ timeoutMs: 15e3,
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ path: { type: "string", description: "Root directory (default: cwd)" },
35
+ depth: {
36
+ type: "integer",
37
+ description: "Max nesting depth (default: 3, 0 for unlimited)",
38
+ minimum: 0,
39
+ maximum: 20
40
+ },
41
+ glob: { type: "string", description: 'Filter files matching glob (e.g. "*.ts")' },
42
+ exclude: {
43
+ type: "array",
44
+ items: { type: "string" },
45
+ description: "Directory names to exclude"
46
+ },
47
+ show_files: {
48
+ type: "boolean",
49
+ description: "Show files (default: true, false shows dirs only)"
50
+ },
51
+ show_dirs: {
52
+ type: "boolean",
53
+ description: "Show directories (default: true)"
54
+ },
55
+ show_hidden: {
56
+ type: "boolean",
57
+ description: "Show hidden files starting with . (default: false)"
58
+ }
59
+ }
60
+ },
61
+ async execute(input, ctx, opts) {
62
+ let final;
63
+ for await (const ev of treeTool.executeStream(input, ctx, opts)) {
64
+ if (ev.type === "final") final = ev.output;
65
+ }
66
+ if (!final) throw new Error("tree: stream ended without final event");
67
+ return final;
68
+ },
69
+ async *executeStream(input, ctx) {
70
+ const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
71
+ const maxDepth = input.depth ?? 3;
72
+ const showFiles = input.show_files ?? true;
73
+ const showDirs = input.show_dirs ?? true;
74
+ const showHidden = input.show_hidden ?? false;
75
+ const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...input.exclude ?? []]);
76
+ const filterGlob = input.glob;
77
+ const lines = [basePath];
78
+ const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
79
+ const queue = [];
80
+ const FLUSH_EVERY = 200;
81
+ let lastEmittedTotal = 0;
82
+ const tickProgress = () => {
83
+ const seen = totals.totalFiles.value + totals.totalDirs.value;
84
+ if (seen - lastEmittedTotal >= FLUSH_EVERY) {
85
+ queue.push({
86
+ type: "metric",
87
+ text: `${seen} entries`,
88
+ data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value }
89
+ });
90
+ lastEmittedTotal = seen;
91
+ }
92
+ };
93
+ const walkPromise = walkDir(basePath, 0, {
94
+ maxDepth,
95
+ exclude,
96
+ showFiles,
97
+ showDirs,
98
+ showHidden,
99
+ filterGlob,
100
+ lines,
101
+ prefix: "",
102
+ isLast: true,
103
+ totalFiles: totals.totalFiles,
104
+ totalDirs: totals.totalDirs,
105
+ onProgress: tickProgress
106
+ });
107
+ let walkDone = false;
108
+ walkPromise.finally(() => {
109
+ walkDone = true;
110
+ });
111
+ while (!walkDone || queue.length > 0) {
112
+ if (queue.length > 0) {
113
+ yield queue.shift();
114
+ } else {
115
+ await Promise.race([
116
+ walkPromise,
117
+ new Promise((r) => setTimeout(r, 50))
118
+ ]).catch(() => void 0);
119
+ }
120
+ }
121
+ await walkPromise;
122
+ yield {
123
+ type: "final",
124
+ output: {
125
+ tree: lines.join("\n"),
126
+ total_files: totals.totalFiles.value,
127
+ total_dirs: totals.totalDirs.value,
128
+ truncated: false,
129
+ path: basePath
130
+ }
131
+ };
132
+ }
133
+ };
134
+ async function walkDir(dir, depth, opts) {
135
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
136
+ const filtered = entries.filter((e) => {
137
+ if (!opts.showHidden && e.name.startsWith(".")) return false;
138
+ if (opts.exclude.has(e.name)) return false;
139
+ return true;
140
+ });
141
+ if (depth > 0) {
142
+ const dirCount = filtered.filter((e) => e.isDirectory()).length;
143
+ const fileCount = filtered.filter((e) => e.isFile()).length;
144
+ opts.totalDirs.value += dirCount;
145
+ opts.totalFiles.value += fileCount;
146
+ opts.onProgress?.();
147
+ }
148
+ const items = filtered.sort((a, b) => {
149
+ if (a.isDirectory() && !b.isDirectory()) return -1;
150
+ if (!a.isDirectory() && b.isDirectory()) return 1;
151
+ return a.name.localeCompare(b.name);
152
+ });
153
+ for (let i = 0; i < items.length; i++) {
154
+ const entry = items[i];
155
+ if (!entry) continue;
156
+ const isLast = i === items.length - 1;
157
+ const connector = opts.isLast ? " " : "\u2502 ";
158
+ const branch = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
159
+ const displayName = entry.name + (entry.isDirectory() ? "/" : "");
160
+ if (!opts.showDirs && entry.isDirectory()) continue;
161
+ if (!opts.showFiles && entry.isFile()) continue;
162
+ opts.lines.push(opts.prefix + branch + displayName);
163
+ if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
164
+ const childPrefix = opts.prefix + connector;
165
+ await walkDir(path.join(dir, entry.name), depth + 1, {
166
+ ...opts,
167
+ prefix: childPrefix,
168
+ isLast
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ export { treeTool };
175
+ //# sourceMappingURL=tree.js.map
176
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/_util.ts","../src/tree.ts"],"names":["path2"],"mappings":"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","file":"tree.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport type { Tool, ToolProgressEvent, ToolStreamEvent } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\r\n\r\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '__pycache__'];\r\n\r\ninterface TreeInput {\r\n path?: string;\r\n depth?: number;\r\n glob?: string;\r\n exclude?: string[];\r\n show_files?: boolean;\r\n show_dirs?: boolean;\r\n show_hidden?: boolean;\r\n}\r\n\r\ninterface TreeOutput {\r\n tree: string;\r\n total_files: number;\r\n total_dirs: number;\r\n truncated: boolean;\r\n path: string;\r\n}\r\n\r\nexport const treeTool: Tool<TreeInput, TreeOutput> = {\r\n name: 'tree',\r\n description:\r\n 'Display directory structure as an ASCII tree. Shows files and folders with indentation.',\r\n usageHint:\r\n 'Set `path` (default: cwd). `depth` limits nesting (default: 3). `glob` filters files. `exclude` ignores dirs. `show_files` toggles file listing (default: true).',\r\n permission: 'auto',\r\n mutating: false,\r\n timeoutMs: 15_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n path: { type: 'string', description: 'Root directory (default: cwd)' },\r\n depth: {\r\n type: 'integer',\r\n description: 'Max nesting depth (default: 3, 0 for unlimited)',\r\n minimum: 0,\r\n maximum: 20,\r\n },\r\n glob: { type: 'string', description: 'Filter files matching glob (e.g. \"*.ts\")' },\r\n exclude: {\r\n type: 'array',\r\n items: { type: 'string' },\r\n description: 'Directory names to exclude',\r\n },\r\n show_files: {\r\n type: 'boolean',\r\n description: 'Show files (default: true, false shows dirs only)',\r\n },\r\n show_dirs: {\r\n type: 'boolean',\r\n description: 'Show directories (default: true)',\r\n },\r\n show_hidden: {\r\n type: 'boolean',\r\n description: 'Show hidden files starting with . (default: false)',\r\n },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: TreeOutput | undefined;\r\n for await (const ev of treeTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('tree: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx): AsyncGenerator<ToolStreamEvent<TreeOutput>> {\r\n const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\r\n const maxDepth = input.depth ?? 3;\r\n const showFiles = input.show_files ?? true;\r\n const showDirs = input.show_dirs ?? true;\r\n const showHidden = input.show_hidden ?? false;\r\n const exclude = new Set([...DEFAULT_IGNORE, ...(input.exclude ?? [])]);\r\n const filterGlob = input.glob;\r\n\r\n const lines: string[] = [basePath];\r\n const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };\r\n\r\n // Walker pushes progress into an async queue; the generator drains it.\r\n const queue: ToolProgressEvent[] = [];\r\n const FLUSH_EVERY = 200; // emit metric every 200 entries seen\r\n let lastEmittedTotal = 0;\r\n\r\n const tickProgress = () => {\r\n const seen = totals.totalFiles.value + totals.totalDirs.value;\r\n if (seen - lastEmittedTotal >= FLUSH_EVERY) {\r\n queue.push({\r\n type: 'metric',\r\n text: `${seen} entries`,\r\n data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value },\r\n });\r\n lastEmittedTotal = seen;\r\n }\r\n };\r\n\r\n const walkPromise = walkDir(basePath, 0, {\r\n maxDepth,\r\n exclude,\r\n showFiles,\r\n showDirs,\r\n showHidden,\r\n filterGlob,\r\n lines,\r\n prefix: '',\r\n isLast: true,\r\n totalFiles: totals.totalFiles,\r\n totalDirs: totals.totalDirs,\r\n onProgress: tickProgress,\r\n });\r\n\r\n // Race the walk against periodic flushes — yield metrics while it runs.\r\n let walkDone = false;\r\n walkPromise.finally(() => {\r\n walkDone = true;\r\n });\r\n\r\n while (!walkDone || queue.length > 0) {\r\n if (queue.length > 0) {\r\n yield queue.shift()!;\r\n } else {\r\n await Promise.race([\r\n walkPromise,\r\n new Promise<void>((r) => setTimeout(r, 50)),\r\n ]).catch(() => undefined);\r\n }\r\n }\r\n await walkPromise; // surface any error\r\n\r\n yield {\r\n type: 'final',\r\n output: {\r\n tree: lines.join('\\n'),\r\n total_files: totals.totalFiles.value,\r\n total_dirs: totals.totalDirs.value,\r\n truncated: false,\r\n path: basePath,\r\n },\r\n };\r\n },\r\n};\r\n\r\ninterface WalkOptions {\r\n maxDepth: number;\r\n exclude: Set<string>;\r\n showFiles: boolean;\r\n showDirs: boolean;\r\n showHidden: boolean;\r\n filterGlob?: string;\r\n lines: string[];\r\n prefix: string;\r\n isLast: boolean;\r\n totalFiles: { value: number };\r\n totalDirs: { value: number };\r\n onProgress?: () => void;\r\n}\r\n\r\nasync function walkDir(\r\n dir: string,\r\n depth: number,\r\n opts: WalkOptions,\r\n): Promise<void> {\r\n const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [] as import('node:fs').Dirent[]);\r\n\r\n const filtered = entries.filter((e) => {\r\n if (!opts.showHidden && e.name.startsWith('.')) return false;\r\n if (opts.exclude.has(e.name)) return false;\r\n return true;\r\n });\r\n\r\n if (depth > 0) {\r\n const dirCount = filtered.filter((e) => e.isDirectory()).length;\r\n const fileCount = filtered.filter((e) => e.isFile()).length;\r\n opts.totalDirs.value += dirCount;\r\n opts.totalFiles.value += fileCount;\r\n opts.onProgress?.();\r\n }\r\n\r\n const items = filtered.sort((a, b) => {\r\n if (a.isDirectory() && !b.isDirectory()) return -1;\r\n if (!a.isDirectory() && b.isDirectory()) return 1;\r\n return a.name.localeCompare(b.name);\r\n });\r\n\r\n for (let i = 0; i < items.length; i++) {\r\n const entry = items[i];\r\n if (!entry) continue;\r\n const isLast = i === items.length - 1;\r\n const connector = opts.isLast ? ' ' : '│ ';\r\n const branch = isLast ? '└── ' : '├── ';\r\n const displayName = entry.name + (entry.isDirectory() ? '/' : '');\r\n\r\n if (!opts.showDirs && entry.isDirectory()) continue;\r\n if (!opts.showFiles && entry.isFile()) continue;\r\n\r\n opts.lines.push(opts.prefix + branch + displayName);\r\n\r\n if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {\r\n const childPrefix = opts.prefix + connector;\r\n await walkDir(path.join(dir, entry.name), depth + 1, {\r\n ...opts,\r\n prefix: childPrefix,\r\n isLast,\r\n });\r\n }\r\n }\r\n}"]}
@@ -0,0 +1,19 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface TypecheckInput {
4
+ project?: string;
5
+ cwd?: string;
6
+ strict?: boolean;
7
+ all?: boolean;
8
+ }
9
+ interface TypecheckOutput {
10
+ project: string;
11
+ exit_code: number;
12
+ errors: number;
13
+ warnings: number;
14
+ output: string;
15
+ truncated: boolean;
16
+ }
17
+ declare const typecheckTool: Tool<TypecheckInput, TypecheckOutput>;
18
+
19
+ export { typecheckTool };
@@ -0,0 +1,181 @@
1
+ import * as path from 'path';
2
+ import { spawn } from 'child_process';
3
+
4
+ // src/typecheck.ts
5
+ function resolvePath(input, ctx) {
6
+ return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
7
+ }
8
+ function ensureInsideRoot(absPath, ctx) {
9
+ const root = path.resolve(ctx.projectRoot);
10
+ const target = path.resolve(absPath);
11
+ const rel = path.relative(root, target);
12
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
13
+ throw new Error(`Path "${absPath}" is outside project root "${root}"`);
14
+ }
15
+ return target;
16
+ }
17
+ function safeResolve(input, ctx) {
18
+ return ensureInsideRoot(resolvePath(input, ctx), ctx);
19
+ }
20
+ async function* spawnStream(opts) {
21
+ const max = opts.maxBytes;
22
+ const flushAt = opts.flushBytes ?? 4 * 1024;
23
+ let stdout = "";
24
+ let stderr = "";
25
+ let pending = "";
26
+ let error;
27
+ const child = spawn(opts.cmd, opts.args, {
28
+ cwd: opts.cwd,
29
+ signal: opts.signal,
30
+ stdio: ["ignore", "pipe", "pipe"]
31
+ });
32
+ const queue = [];
33
+ let waiter;
34
+ const wake = () => {
35
+ if (waiter) {
36
+ const w = waiter;
37
+ waiter = void 0;
38
+ w();
39
+ }
40
+ };
41
+ child.stdout?.on("data", (c) => {
42
+ const s = c.toString();
43
+ if (stdout.length < max) stdout += s;
44
+ queue.push({ kind: "out", data: s });
45
+ wake();
46
+ });
47
+ child.stderr?.on("data", (c) => {
48
+ const s = c.toString();
49
+ if (stderr.length < max) stderr += s;
50
+ queue.push({ kind: "err", data: s });
51
+ wake();
52
+ });
53
+ child.on("error", (e) => {
54
+ error = e.message;
55
+ queue.push({ kind: "error", data: e.message });
56
+ wake();
57
+ });
58
+ child.on("close", (code) => {
59
+ queue.push({ kind: "close", data: "", code: code ?? 0 });
60
+ wake();
61
+ });
62
+ let exitCode = 0;
63
+ let spawnFailed = false;
64
+ for (; ; ) {
65
+ while (queue.length === 0) {
66
+ await new Promise((resolve2) => {
67
+ waiter = resolve2;
68
+ });
69
+ }
70
+ const chunk = queue.shift();
71
+ if (chunk.kind === "close") {
72
+ if (!spawnFailed) exitCode = chunk.code ?? 0;
73
+ break;
74
+ }
75
+ if (chunk.kind === "error") {
76
+ spawnFailed = true;
77
+ exitCode = 1;
78
+ continue;
79
+ }
80
+ pending += chunk.data;
81
+ if (pending.length >= flushAt) {
82
+ yield { type: "partial_output", text: pending };
83
+ pending = "";
84
+ }
85
+ }
86
+ if (pending.length > 0) {
87
+ yield { type: "partial_output", text: pending };
88
+ }
89
+ return {
90
+ stdout,
91
+ stderr,
92
+ exitCode,
93
+ truncated: stdout.length >= max || stderr.length >= max,
94
+ error
95
+ };
96
+ }
97
+
98
+ // src/typecheck.ts
99
+ var typecheckTool = {
100
+ name: "typecheck",
101
+ description: "Run TypeScript type checking with `tsc --noEmit`. Checks for type errors without compiling.",
102
+ usageHint: "Set `project` for tsconfig path (default: nearest). `strict` enables strictest flags. `all` checks all projects in workspace.",
103
+ permission: "confirm",
104
+ mutating: false,
105
+ timeoutMs: 12e4,
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ project: { type: "string", description: "Path to tsconfig.json (default: auto-detect)" },
110
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
111
+ strict: {
112
+ type: "boolean",
113
+ description: "Add --strict flag for maximum type checking (default: false)"
114
+ },
115
+ all: {
116
+ type: "boolean",
117
+ description: "Type-check all projects (pnpm -r) (default: false)"
118
+ }
119
+ }
120
+ },
121
+ async execute(input, ctx, opts) {
122
+ let final;
123
+ for await (const ev of typecheckTool.executeStream(input, ctx, opts)) {
124
+ if (ev.type === "final") final = ev.output;
125
+ }
126
+ if (!final) throw new Error("typecheck: stream ended without final event");
127
+ return final;
128
+ },
129
+ async *executeStream(input, ctx, opts) {
130
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
131
+ let args;
132
+ let project;
133
+ if (input.all) {
134
+ args = ["--noEmit"];
135
+ project = "workspace";
136
+ } else {
137
+ const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
138
+ args = ["--noEmit"];
139
+ if (input.strict) args.push("--strict");
140
+ if (tsconfig) args.push("--project", tsconfig);
141
+ project = tsconfig ?? "default";
142
+ }
143
+ yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
144
+ const result = yield* spawnStream({
145
+ cmd: "npx",
146
+ args: ["tsc", ...args],
147
+ cwd,
148
+ signal: opts.signal,
149
+ maxBytes: 2e5
150
+ });
151
+ const errors = (result.stdout.match(/error TS/g) || []).length;
152
+ const warnings = (result.stdout.match(/warning/g) || []).length;
153
+ yield {
154
+ type: "final",
155
+ output: {
156
+ project,
157
+ exit_code: result.exitCode,
158
+ errors,
159
+ warnings,
160
+ output: result.stdout || result.stderr || result.error || "",
161
+ truncated: result.truncated
162
+ }
163
+ };
164
+ }
165
+ };
166
+ async function findTsConfig(cwd) {
167
+ const { stat } = await import('fs/promises');
168
+ const candidates = ["tsconfig.json", "tsconfig.base.json"];
169
+ for (const f of candidates) {
170
+ try {
171
+ const s = await stat(path.join(cwd, f));
172
+ if (s.isFile()) return path.join(cwd, f);
173
+ } catch {
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+
179
+ export { typecheckTool };
180
+ //# sourceMappingURL=typecheck.js.map
181
+ //# sourceMappingURL=typecheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/_util.ts","../src/typecheck.ts"],"names":["resolve","path2"],"mappings":"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","file":"typecheck.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as path from 'node:path';\r\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { safeResolve, spawnStream } from './_util.js';\r\n\r\ninterface TypecheckInput {\r\n project?: string;\r\n cwd?: string;\r\n strict?: boolean;\r\n all?: boolean;\r\n}\r\n\r\ninterface TypecheckOutput {\r\n project: string;\r\n exit_code: number;\r\n errors: number;\r\n warnings: number;\r\n output: string;\r\n truncated: boolean;\r\n}\r\n\r\nexport const typecheckTool: Tool<TypecheckInput, TypecheckOutput> = {\r\n name: 'typecheck',\r\n description:\r\n 'Run TypeScript type checking with `tsc --noEmit`. Checks for type errors without compiling.',\r\n usageHint:\r\n 'Set `project` for tsconfig path (default: nearest). `strict` enables strictest flags. `all` checks all projects in workspace.',\r\n permission: 'confirm',\r\n mutating: false,\r\n timeoutMs: 120_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n project: { type: 'string', description: 'Path to tsconfig.json (default: auto-detect)' },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n strict: {\r\n type: 'boolean',\r\n description: 'Add --strict flag for maximum type checking (default: false)',\r\n },\r\n all: {\r\n type: 'boolean',\r\n description: 'Type-check all projects (pnpm -r) (default: false)',\r\n },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: TypecheckOutput | undefined;\r\n for await (const ev of typecheckTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('typecheck: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<TypecheckOutput>> {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n\r\n let args: string[];\r\n let project: string;\r\n if (input.all) {\r\n args = ['--noEmit'];\r\n project = 'workspace';\r\n } else {\r\n const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);\r\n args = ['--noEmit'];\r\n if (input.strict) args.push('--strict');\r\n if (tsconfig) args.push('--project', tsconfig);\r\n project = tsconfig ?? 'default';\r\n }\r\n\r\n yield { type: 'log', text: `tsc ${args.join(' ')}`, data: { project } };\r\n\r\n const result = yield* spawnStream({\r\n cmd: 'npx',\r\n args: ['tsc', ...args],\r\n cwd,\r\n signal: opts.signal,\r\n maxBytes: 200_000,\r\n });\r\n\r\n const errors = (result.stdout.match(/error TS/g) || []).length;\r\n const warnings = (result.stdout.match(/warning/g) || []).length;\r\n\r\n yield {\r\n type: 'final',\r\n output: {\r\n project,\r\n exit_code: result.exitCode,\r\n errors,\r\n warnings,\r\n output: result.stdout || result.stderr || result.error || '',\r\n truncated: result.truncated,\r\n },\r\n };\r\n },\r\n};\r\n\r\nasync function findTsConfig(cwd: string): Promise<string | null> {\r\n const { stat } = await import('node:fs/promises');\r\n const candidates = ['tsconfig.json', 'tsconfig.base.json'];\r\n for (const f of candidates) {\r\n try {\r\n const s = await stat(path.join(cwd, f));\r\n if (s.isFile()) return path.join(cwd, f);\r\n } catch {\r\n // continue\r\n }\r\n }\r\n return null;\r\n}\r\n"]}
@@ -0,0 +1,15 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface WriteInput {
4
+ path: string;
5
+ content: string;
6
+ }
7
+ interface WriteOutput {
8
+ path: string;
9
+ bytes_written: number;
10
+ created: boolean;
11
+ diff?: string;
12
+ }
13
+ declare const writeTool: Tool<WriteInput, WriteOutput>;
14
+
15
+ export { writeTool };
package/dist/write.js ADDED
@@ -0,0 +1,77 @@
1
+ import * as fs from 'fs/promises';
2
+ import { atomicWrite, unifiedDiff } from '@wrongstack/core';
3
+ import * as path from 'path';
4
+ import 'child_process';
5
+
6
+ // src/write.ts
7
+ function resolvePath(input, ctx) {
8
+ return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
9
+ }
10
+ function ensureInsideRoot(absPath, ctx) {
11
+ const root = path.resolve(ctx.projectRoot);
12
+ const target = path.resolve(absPath);
13
+ const rel = path.relative(root, target);
14
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
15
+ throw new Error(`Path "${absPath}" is outside project root "${root}"`);
16
+ }
17
+ return target;
18
+ }
19
+ function safeResolve(input, ctx) {
20
+ return ensureInsideRoot(resolvePath(input, ctx), ctx);
21
+ }
22
+
23
+ // src/write.ts
24
+ var writeTool = {
25
+ name: "write",
26
+ description: "Write or overwrite a file. For existing files, prefer `edit` over `write`.",
27
+ usageHint: "Use `write` for new files or full replacements. For partial edits use `edit`. Existing files must have been `read` first in this session.",
28
+ permission: "confirm",
29
+ mutating: true,
30
+ timeoutMs: 5e3,
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ path: { type: "string" },
35
+ content: { type: "string" }
36
+ },
37
+ required: ["path", "content"]
38
+ },
39
+ async execute(input, ctx) {
40
+ if (!input?.path) throw new Error("write: path is required");
41
+ if (input.content === void 0) throw new Error("write: content is required");
42
+ const absPath = safeResolve(input.path, ctx);
43
+ let existed = false;
44
+ let prev = "";
45
+ try {
46
+ const stat3 = await fs.stat(absPath);
47
+ existed = stat3.isFile();
48
+ if (existed) {
49
+ if (!ctx.hasRead(absPath)) {
50
+ throw new Error(
51
+ `write: file "${input.path}" exists but was not read in this session. Read it first.`
52
+ );
53
+ }
54
+ prev = await fs.readFile(absPath, "utf8");
55
+ }
56
+ } catch (err) {
57
+ if (err.code !== "ENOENT") {
58
+ throw err;
59
+ }
60
+ }
61
+ await atomicWrite(absPath, input.content);
62
+ const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
63
+ + (new file, ${input.content.split("\n").length} lines)`;
64
+ const stat2 = await fs.stat(absPath);
65
+ ctx.recordRead(absPath, stat2.mtimeMs);
66
+ return {
67
+ path: absPath,
68
+ bytes_written: Buffer.byteLength(input.content, "utf8"),
69
+ created: !existed,
70
+ diff
71
+ };
72
+ }
73
+ };
74
+
75
+ export { writeTool };
76
+ //# sourceMappingURL=write.js.map
77
+ //# sourceMappingURL=write.js.map