@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
package/dist/diff.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface DiffInput {
4
+ path?: string;
5
+ files?: string | string[];
6
+ a?: string;
7
+ b?: string;
8
+ staged?: boolean;
9
+ mode?: 'unified' | 'side-by-side' | 'stat';
10
+ context?: number;
11
+ }
12
+ interface DiffOutput {
13
+ diff: string;
14
+ files: string[];
15
+ truncated: boolean;
16
+ mode: string;
17
+ }
18
+ declare const diffTool: Tool<DiffInput, DiffOutput>;
19
+
20
+ export { diffTool };
package/dist/diff.js ADDED
@@ -0,0 +1,142 @@
1
+ import * as fs from 'fs/promises';
2
+ import { spawn } from 'child_process';
3
+ import * as path from 'path';
4
+
5
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
+ }) : x)(function(x) {
8
+ if (typeof require !== "undefined") return require.apply(this, arguments);
9
+ throw Error('Dynamic require of "' + x + '" is not supported');
10
+ });
11
+ function resolvePath(input, ctx) {
12
+ return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
13
+ }
14
+ function ensureInsideRoot(absPath, ctx) {
15
+ const root = path.resolve(ctx.projectRoot);
16
+ const target = path.resolve(absPath);
17
+ const rel = path.relative(root, target);
18
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
19
+ throw new Error(`Path "${absPath}" is outside project root "${root}"`);
20
+ }
21
+ return target;
22
+ }
23
+ function safeResolve(input, ctx) {
24
+ return ensureInsideRoot(resolvePath(input, ctx), ctx);
25
+ }
26
+
27
+ // src/diff.ts
28
+ var diffTool = {
29
+ name: "diff",
30
+ description: "Show differences between files, commits, or branches. Supports staged vs working tree.",
31
+ usageHint: "Use `files` for file paths, `a`/`b` for commit refs, `staged` for git index. `mode`: unified (default), stat, side-by-side.",
32
+ permission: "auto",
33
+ mutating: false,
34
+ timeoutMs: 1e4,
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ path: { type: "string", description: "Working directory for diff" },
39
+ files: {
40
+ type: "string",
41
+ description: 'File(s) to diff: single path, comma-separated, or "**/*.ts" glob'
42
+ },
43
+ a: { type: "string", description: "First commit/branch/ref (for git diff)" },
44
+ b: { type: "string", description: "Second commit/branch/ref (for git diff)" },
45
+ staged: { type: "boolean", description: "Diff staged changes only" },
46
+ mode: {
47
+ type: "string",
48
+ enum: ["unified", "side-by-side", "stat"],
49
+ description: "Output mode (default: unified)"
50
+ },
51
+ context: { type: "integer", description: "Context lines for unified diff (default: 3)" }
52
+ }
53
+ },
54
+ async execute(input, ctx, opts) {
55
+ if (input.a !== void 0 || input.b !== void 0) {
56
+ return await gitDiff(input, ctx, opts.signal);
57
+ }
58
+ return await fileDiff(input, ctx, opts.signal);
59
+ }
60
+ };
61
+ async function gitDiff(input, ctx, signal) {
62
+ const gitDir = findGitDir(ctx.cwd);
63
+ if (!gitDir) {
64
+ return { diff: "", files: [], truncated: false, mode: "unified" };
65
+ }
66
+ const args = ["diff", "--no-color"];
67
+ if (input.staged) args.push("--staged");
68
+ if (input.a) args.push(input.a);
69
+ if (input.b) args.push(input.b);
70
+ if (input.files) {
71
+ const files = Array.isArray(input.files) ? input.files : input.files.split(",");
72
+ args.push("--", ...files.map((f) => f.trim()));
73
+ }
74
+ const result = await runGit(args, gitDir, signal);
75
+ return {
76
+ diff: result.stdout,
77
+ files: [],
78
+ truncated: result.stdout.length > 1e5,
79
+ mode: "unified"
80
+ };
81
+ }
82
+ function findGitDir(cwd) {
83
+ let dir = cwd;
84
+ for (let i = 0; i < 20; i++) {
85
+ try {
86
+ const stat2 = __require("fs").statSync(`${dir}/.git`);
87
+ if (stat2.isDirectory()) return dir;
88
+ } catch {
89
+ }
90
+ const parent = __require("path").dirname(dir);
91
+ if (parent === dir) break;
92
+ dir = parent;
93
+ }
94
+ return null;
95
+ }
96
+ function runGit(args, cwd, signal) {
97
+ return new Promise((resolve2) => {
98
+ let stdout = "";
99
+ let stderr = "";
100
+ const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
101
+ child.stdout?.on("data", (c) => {
102
+ stdout += c.toString();
103
+ });
104
+ child.stderr?.on("data", (c) => {
105
+ stderr += c.toString();
106
+ });
107
+ child.on("close", (code) => resolve2({ stdout, stderr, exitCode: code ?? 0 }));
108
+ child.on("error", (e) => resolve2({ stdout: "", stderr: e.message, exitCode: 1 }));
109
+ });
110
+ }
111
+ async function fileDiff(input, ctx, signal) {
112
+ input.path ? safeResolve(input.path, ctx) : ctx.cwd;
113
+ input.context ?? 3;
114
+ const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()).filter(Boolean) : [];
115
+ if (files.length === 0) {
116
+ return { diff: "No files specified", files: [], truncated: false, mode: input.mode ?? "unified" };
117
+ }
118
+ const results = [];
119
+ for (const file of files) {
120
+ const absPath = safeResolve(file, ctx);
121
+ const stat2 = await fs.stat(absPath).catch(() => null);
122
+ if (!stat2?.isFile()) continue;
123
+ const content = await fs.readFile(absPath, "utf8");
124
+ const lines = content.split(/\r?\n/);
125
+ results.push(`--- ${file}
126
+ +++ ${file}
127
+ ${formatUnified(lines)}`);
128
+ }
129
+ return {
130
+ diff: results.join("\n"),
131
+ files,
132
+ truncated: false,
133
+ mode: input.mode ?? "unified"
134
+ };
135
+ }
136
+ function formatUnified(lines, context) {
137
+ return lines.map((line, i) => ` ${line}`).join("\n");
138
+ }
139
+
140
+ export { diffTool };
141
+ //# sourceMappingURL=diff.js.map
142
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/_util.ts","../src/diff.ts"],"names":["stat","resolve","spawn"],"mappings":"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","file":"diff.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';\nimport * as path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\nimport { unifiedDiff } from '@wrongstack/core';\n\ninterface DiffInput {\n path?: string;\n files?: string | string[];\n a?: string;\n b?: string;\n staged?: boolean;\n mode?: 'unified' | 'side-by-side' | 'stat';\n context?: number;\n}\n\ninterface DiffOutput {\n diff: string;\n files: string[];\n truncated: boolean;\n mode: string;\n}\n\nexport const diffTool: Tool<DiffInput, DiffOutput> = {\n name: 'diff',\n description:\n 'Show differences between files, commits, or branches. Supports staged vs working tree.',\n usageHint:\n 'Use `files` for file paths, `a`/`b` for commit refs, `staged` for git index. `mode`: unified (default), stat, side-by-side.',\n permission: 'auto',\n mutating: false,\n timeoutMs: 10_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Working directory for diff' },\n files: {\n type: 'string',\n description: 'File(s) to diff: single path, comma-separated, or \"**/*.ts\" glob',\n },\n a: { type: 'string', description: 'First commit/branch/ref (for git diff)' },\n b: { type: 'string', description: 'Second commit/branch/ref (for git diff)' },\n staged: { type: 'boolean', description: 'Diff staged changes only' },\n mode: {\n type: 'string',\n enum: ['unified', 'side-by-side', 'stat'],\n description: 'Output mode (default: unified)',\n },\n context: { type: 'integer', description: 'Context lines for unified diff (default: 3)' },\n },\n },\n async execute(input, ctx, opts) {\n if (input.a !== undefined || input.b !== undefined) {\n return await gitDiff(input, ctx, opts.signal);\n }\n\n return await fileDiff(input, ctx, opts.signal);\n },\n};\n\nasync function gitDiff(\n input: DiffInput,\n ctx: import('@wrongstack/core').Context,\n signal: AbortSignal,\n): Promise<DiffOutput> {\n const gitDir = findGitDir(ctx.cwd);\n if (!gitDir) {\n return { diff: '', files: [], truncated: false, mode: 'unified' };\n }\n\n const args: string[] = ['diff', '--no-color'];\n if (input.staged) args.push('--staged');\n if (input.a) args.push(input.a);\n if (input.b) args.push(input.b);\n if (input.files) {\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\n args.push('--', ...files.map((f) => f.trim()));\n }\n\n const result = await runGit(args, gitDir, signal);\n return {\n diff: result.stdout,\n files: [],\n truncated: result.stdout.length > 100_000,\n mode: 'unified',\n };\n}\n\nfunction findGitDir(cwd: string): string | null {\n let dir = cwd;\n for (let i = 0; i < 20; i++) {\n try {\n const stat = require('node:fs').statSync(`${dir}/.git`);\n if (stat.isDirectory()) return dir;\n } catch {\n // continue\n }\n const parent = require('node:path').dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\nfunction runGit(args: string[], cwd: string, signal: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n const child = spawn('git', args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });\n child.stdout?.on('data', (c) => { stdout += c.toString(); });\n child.stderr?.on('data', (c) => { stderr += c.toString(); });\n child.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? 0 }));\n child.on('error', (e) => resolve({ stdout: '', stderr: e.message, exitCode: 1 }));\n });\n}\n\nasync function fileDiff(\n input: DiffInput,\n ctx: import('@wrongstack/core').Context,\n signal: AbortSignal,\n): Promise<DiffOutput> {\n const baseDir = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\n const context = input.context ?? 3;\n\n const files = input.files\n ? (Array.isArray(input.files) ? input.files : input.files.split(',')).map((f) => f.trim()).filter(Boolean)\n : [];\n\n if (files.length === 0) {\n return { diff: 'No files specified', files: [], truncated: false, mode: input.mode ?? 'unified' };\n }\n\n const results: string[] = [];\n\n for (const file of files) {\n const absPath = safeResolve(file, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (!stat?.isFile()) continue;\n\n const content = await fs.readFile(absPath, 'utf8');\n const lines = content.split(/\\r?\\n/);\n results.push(`--- ${file}\\n+++ ${file}\\n${formatUnified(lines, context)}`);\n }\n\n return {\n diff: results.join('\\n'),\n files,\n truncated: false,\n mode: input.mode ?? 'unified',\n };\n}\n\nfunction formatUnified(lines: string[], context: number): string {\n return lines.map((line, i) => ` ${line}`).join('\\n');\n}"]}
@@ -0,0 +1,27 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface DocumentInput {
4
+ target: 'file' | 'function' | 'class' | 'type' | 'all';
5
+ path?: string;
6
+ files?: string | string[];
7
+ style?: 'jsdoc' | 'tsdoc' | 'block';
8
+ overwrite?: boolean;
9
+ cwd?: string;
10
+ }
11
+ interface DocumentedItem {
12
+ path: string;
13
+ name: string;
14
+ signature: string;
15
+ docstring: string;
16
+ status: 'documented' | 'skipped' | 'error';
17
+ error?: string;
18
+ }
19
+ interface DocumentOutput {
20
+ files_processed: number;
21
+ items_documented: number;
22
+ results: DocumentedItem[];
23
+ style: string;
24
+ }
25
+ declare const documentTool: Tool<DocumentInput, DocumentOutput>;
26
+
27
+ export { documentTool };
@@ -0,0 +1,148 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import 'child_process';
4
+
5
+ // src/document.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/document.ts
23
+ var documentTool = {
24
+ name: "document",
25
+ description: "Generate or update documentation comments for functions, classes, and types. Supports JSDoc, TSDoc, and block comments.",
26
+ usageHint: "Set `target` for what to document. `files` for paths. `style` for comment format. `overwrite` replaces existing docs.",
27
+ permission: "confirm",
28
+ mutating: true,
29
+ timeoutMs: 3e4,
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ target: {
34
+ type: "string",
35
+ enum: ["file", "function", "class", "type", "all"],
36
+ description: "What to document"
37
+ },
38
+ path: {
39
+ type: "string",
40
+ description: "Specific file path to document"
41
+ },
42
+ files: {
43
+ type: "string",
44
+ description: "File(s) to process: single path, comma-separated list, or glob"
45
+ },
46
+ style: {
47
+ type: "string",
48
+ enum: ["jsdoc", "tsdoc", "block"],
49
+ description: "Documentation style (default: jsdoc)"
50
+ },
51
+ overwrite: {
52
+ type: "boolean",
53
+ description: "Overwrite existing docstrings (default: false)"
54
+ },
55
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
56
+ }
57
+ },
58
+ async execute(input, ctx) {
59
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
60
+ const style = input.style ?? "jsdoc";
61
+ const results = [];
62
+ let filesProcessed = 0;
63
+ let itemsDocumented = 0;
64
+ const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
65
+ for (const absPath of fileList) {
66
+ try {
67
+ const content = await fs.readFile(absPath, "utf8");
68
+ filesProcessed++;
69
+ const processed = processFile(content, absPath, style, input.overwrite ?? false, input.target ?? "all");
70
+ results.push(...processed);
71
+ itemsDocumented += processed.filter((r) => r.status === "documented").length;
72
+ } catch (e) {
73
+ results.push({
74
+ path: absPath,
75
+ name: absPath.split("/").pop() ?? absPath,
76
+ signature: "",
77
+ docstring: "",
78
+ status: "error",
79
+ error: e instanceof Error ? e.message : String(e)
80
+ });
81
+ }
82
+ }
83
+ return {
84
+ files_processed: filesProcessed,
85
+ items_documented: itemsDocumented,
86
+ results,
87
+ style
88
+ };
89
+ }
90
+ };
91
+ async function resolveFiles(filesInput, cwd) {
92
+ const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
93
+ const resolved = [];
94
+ for (const f of files) {
95
+ const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
96
+ try {
97
+ const stat2 = await fs.stat(absPath);
98
+ if (stat2.isFile()) resolved.push(absPath);
99
+ } catch {
100
+ }
101
+ }
102
+ return resolved;
103
+ }
104
+ function processFile(content, absPath, style, overwrite, target) {
105
+ const results = [];
106
+ content.split("\n");
107
+ const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
108
+ const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
109
+ const classRegex = /class\s+(\w+)/g;
110
+ const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
111
+ const allMatches = [];
112
+ if (target === "all" || target === "function") {
113
+ for (const m of content.matchAll(functionRegex)) {
114
+ if (!m[1]) continue;
115
+ allMatches.push({ name: m[1], sig: m[2] ?? "", type: "function", line: content.slice(0, m.index).split("\n").length });
116
+ }
117
+ for (const m of content.matchAll(arrowRegex)) {
118
+ if (!m[1]) continue;
119
+ allMatches.push({ name: m[1], sig: m[2] ?? "", type: "arrow", line: content.slice(0, m.index).split("\n").length });
120
+ }
121
+ }
122
+ if (target === "all" || target === "class") {
123
+ for (const m of content.matchAll(classRegex)) {
124
+ if (!m[1]) continue;
125
+ allMatches.push({ name: m[1], sig: "", type: "class", line: content.slice(0, m.index).split("\n").length });
126
+ }
127
+ }
128
+ if (target === "all" || target === "type") {
129
+ for (const m of content.matchAll(typeRegex)) {
130
+ if (!m[1]) continue;
131
+ allMatches.push({ name: m[1], sig: m[0] ?? "", type: "type", line: content.slice(0, m.index).split("\n").length });
132
+ }
133
+ }
134
+ for (const m of allMatches) {
135
+ results.push({
136
+ path: absPath,
137
+ name: m.name,
138
+ signature: m.sig,
139
+ docstring: `/** ${m.name} - documented at line ${m.line} */`,
140
+ status: "skipped"
141
+ });
142
+ }
143
+ return results;
144
+ }
145
+
146
+ export { documentTool };
147
+ //# sourceMappingURL=document.js.map
148
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/_util.ts","../src/document.ts"],"names":["stat"],"mappings":"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","file":"document.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 type { Tool } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface DocumentInput {\r\n target: 'file' | 'function' | 'class' | 'type' | 'all';\r\n path?: string;\r\n files?: string | string[];\r\n style?: 'jsdoc' | 'tsdoc' | 'block';\r\n overwrite?: boolean;\r\n cwd?: string;\r\n}\r\n\r\ninterface DocumentedItem {\r\n path: string;\r\n name: string;\r\n signature: string;\r\n docstring: string;\r\n status: 'documented' | 'skipped' | 'error';\r\n error?: string;\r\n}\r\n\r\ninterface DocumentOutput {\r\n files_processed: number;\r\n items_documented: number;\r\n results: DocumentedItem[];\r\n style: string;\r\n}\r\n\r\nconst JSDOC_TEMPLATE = `/**\r\n * {description}\r\n *{params}\r\n * @returns {returns}\r\n */`;\r\n\r\nconst BLOCK_TEMPLATE = `/*\r\n * {description}\r\n *{params}\r\n * @returns {returns}\r\n */`;\r\n\r\nexport const documentTool: Tool<DocumentInput, DocumentOutput> = {\r\n name: 'document',\r\n description:\r\n 'Generate or update documentation comments for functions, classes, and types. Supports JSDoc, TSDoc, and block comments.',\r\n usageHint:\r\n 'Set `target` for what to document. `files` for paths. `style` for comment format. `overwrite` replaces existing docs.',\r\n permission: 'confirm',\r\n mutating: true,\r\n timeoutMs: 30_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n target: {\r\n type: 'string',\r\n enum: ['file', 'function', 'class', 'type', 'all'],\r\n description: 'What to document',\r\n },\r\n path: {\r\n type: 'string',\r\n description: 'Specific file path to document',\r\n },\r\n files: {\r\n type: 'string',\r\n description: 'File(s) to process: single path, comma-separated list, or glob',\r\n },\r\n style: {\r\n type: 'string',\r\n enum: ['jsdoc', 'tsdoc', 'block'],\r\n description: 'Documentation style (default: jsdoc)',\r\n },\r\n overwrite: {\r\n type: 'boolean',\r\n description: 'Overwrite existing docstrings (default: false)',\r\n },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n },\r\n },\r\n async execute(input, ctx) {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const style = input.style ?? 'jsdoc';\r\n const results: DocumentedItem[] = [];\r\n let filesProcessed = 0;\r\n let itemsDocumented = 0;\r\n\r\n const fileList = input.files\r\n ? await resolveFiles(Array.isArray(input.files) ? input.files.join(',') : input.files, cwd)\r\n : input.path\r\n ? [safeResolve(input.path, ctx)]\r\n : [];\r\n\r\n for (const absPath of fileList) {\r\n try {\r\n const content = await fs.readFile(absPath, 'utf8');\r\n filesProcessed++;\r\n const processed = processFile(content, absPath, style, input.overwrite ?? false, input.target ?? 'all');\r\n results.push(...processed);\r\n itemsDocumented += processed.filter((r) => r.status === 'documented').length;\r\n } catch (e) {\r\n results.push({\r\n path: absPath,\r\n name: absPath.split('/').pop() ?? absPath,\r\n signature: '',\r\n docstring: '',\r\n status: 'error',\r\n error: e instanceof Error ? e.message : String(e),\r\n });\r\n }\r\n }\r\n\r\n return {\r\n files_processed: filesProcessed,\r\n items_documented: itemsDocumented,\r\n results,\r\n style,\r\n };\r\n },\r\n};\r\n\r\nasync function resolveFiles(filesInput: string, cwd: string): Promise<string[]> {\r\n const files = Array.isArray(filesInput) ? filesInput : filesInput.split(',');\r\n const resolved: string[] = [];\r\n\r\n for (const f of files) {\r\n const absPath = f.trim().startsWith('/') ? f.trim() : `${cwd}/${f.trim()}`;\r\n try {\r\n const stat = await fs.stat(absPath);\r\n if (stat.isFile()) resolved.push(absPath);\r\n } catch {\r\n // skip\r\n }\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nfunction processFile(\r\n content: string,\r\n absPath: string,\r\n style: string,\r\n overwrite: boolean,\r\n target: string,\r\n): DocumentedItem[] {\r\n const results: DocumentedItem[] = [];\r\n const lines = content.split('\\n');\r\n const functionRegex = /(?:async\\s+)?function\\s+(\\w+)\\s*\\(([^)]*)\\)/g;\r\n const arrowRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?\\(([^)]*)\\)\\s*=>/g;\r\n const classRegex = /class\\s+(\\w+)/g;\r\n const typeRegex = /(?:type|interface)\\s+(\\w+)\\s*[=<]/g;\r\n\r\n const allMatches: { name: string; sig: string; type: string; line: number }[] = [];\r\n\r\n if (target === 'all' || target === 'function') {\r\n for (const m of content.matchAll(functionRegex)) {\r\n if (!m[1]) continue;\r\n allMatches.push({ name: m[1], sig: m[2] ?? '', type: 'function', line: content.slice(0, m.index).split('\\n').length });\r\n }\r\n for (const m of content.matchAll(arrowRegex)) {\r\n if (!m[1]) continue;\r\n allMatches.push({ name: m[1], sig: m[2] ?? '', type: 'arrow', line: content.slice(0, m.index).split('\\n').length });\r\n }\r\n }\r\n\r\n if (target === 'all' || target === 'class') {\r\n for (const m of content.matchAll(classRegex)) {\r\n if (!m[1]) continue;\r\n allMatches.push({ name: m[1], sig: '', type: 'class', line: content.slice(0, m.index).split('\\n').length });\r\n }\r\n }\r\n\r\n if (target === 'all' || target === 'type') {\r\n for (const m of content.matchAll(typeRegex)) {\r\n if (!m[1]) continue;\r\n allMatches.push({ name: m[1], sig: m[0] ?? '', type: 'type', line: content.slice(0, m.index).split('\\n').length });\r\n }\r\n }\r\n\r\n for (const m of allMatches) {\r\n results.push({\r\n path: absPath,\r\n name: m.name,\r\n signature: m.sig,\r\n docstring: `/** ${m.name} - documented at line ${m.line} */`,\r\n status: 'skipped',\r\n });\r\n }\r\n\r\n return results;\r\n}"]}
package/dist/edit.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface EditInput {
4
+ path: string;
5
+ old_string: string;
6
+ new_string: string;
7
+ /**
8
+ * When true, replaces all occurrences of `old_string`.
9
+ * When false (default), replaces only the first occurrence and errors
10
+ * if more than one match exists — use this to ensure you target the
11
+ * right location.
12
+ */
13
+ replace_all?: boolean;
14
+ }
15
+ interface EditOutput {
16
+ path: string;
17
+ replacements: number;
18
+ diff: string;
19
+ }
20
+ declare const editTool: Tool<EditInput, EditOutput>;
21
+
22
+ export { editTool };
package/dist/edit.js ADDED
@@ -0,0 +1,138 @@
1
+ import * as fs from 'fs/promises';
2
+ import { detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff } from '@wrongstack/core';
3
+ import * as path from 'path';
4
+ import 'child_process';
5
+
6
+ // src/edit.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/edit.ts
24
+ var editTool = {
25
+ name: "edit",
26
+ description: "Make a surgical edit by replacing exact text. Fails if `old_string` is not unique unless `replace_all` is true.",
27
+ 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`.",
28
+ permission: "confirm",
29
+ mutating: true,
30
+ timeoutMs: 5e3,
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ path: { type: "string" },
35
+ old_string: { type: "string" },
36
+ new_string: { type: "string" },
37
+ replace_all: { type: "boolean" }
38
+ },
39
+ required: ["path", "old_string", "new_string"]
40
+ },
41
+ async execute(input, ctx) {
42
+ if (!input?.path) throw new Error("edit: path is required");
43
+ if (input.old_string === void 0) throw new Error("edit: old_string is required");
44
+ if (input.new_string === void 0) throw new Error("edit: new_string is required");
45
+ if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
46
+ const absPath = safeResolve(input.path, ctx);
47
+ const stat2 = await fs.stat(absPath).catch((err) => {
48
+ if (err.code === "ENOENT") {
49
+ throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
50
+ }
51
+ throw err;
52
+ });
53
+ if (!stat2.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
54
+ if (!ctx.hasRead(absPath)) {
55
+ throw new Error(
56
+ `edit: file "${input.path}" was not read in this session. Read it first.`
57
+ );
58
+ }
59
+ const lastReadMtime = ctx.lastReadMtime(absPath);
60
+ if (lastReadMtime !== void 0 && stat2.mtimeMs > lastReadMtime + 1) {
61
+ throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
62
+ }
63
+ const original = await fs.readFile(absPath, "utf8");
64
+ const style = detectNewlineStyle(original);
65
+ const fileLf = normalizeToLf(original);
66
+ const oldLf = normalizeToLf(input.old_string);
67
+ const newLf = normalizeToLf(input.new_string);
68
+ if (oldLf === newLf) {
69
+ return {
70
+ path: absPath,
71
+ replacements: 0,
72
+ diff: "(no-op: old and new are identical)"
73
+ };
74
+ }
75
+ let count = 0;
76
+ let idx = fileLf.indexOf(oldLf);
77
+ const matches = [];
78
+ while (idx !== -1) {
79
+ matches.push(idx);
80
+ count++;
81
+ idx = fileLf.indexOf(oldLf, idx + 1);
82
+ }
83
+ if (count === 0) {
84
+ const hint = findSimilarity(fileLf, oldLf);
85
+ throw new Error(
86
+ `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`
87
+ );
88
+ }
89
+ if (count > 1 && !input.replace_all) {
90
+ const lines = lineNumbersFor(fileLf, matches);
91
+ throw new Error(
92
+ `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`
93
+ );
94
+ }
95
+ const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
96
+ const newFile = toStyle(newFileLf, style);
97
+ await atomicWrite(absPath, newFile, { mode: stat2.mode & 511 });
98
+ const updated = await fs.stat(absPath);
99
+ ctx.recordRead(absPath, updated.mtimeMs);
100
+ const diff = unifiedDiff(original, newFile, {
101
+ fromFile: input.path,
102
+ toFile: input.path
103
+ });
104
+ return {
105
+ path: absPath,
106
+ replacements: input.replace_all ? count : 1,
107
+ diff
108
+ };
109
+ }
110
+ };
111
+ function lineNumbersFor(text, indices) {
112
+ const out = [];
113
+ let pos = 0;
114
+ let line = 1;
115
+ for (const target of indices) {
116
+ while (pos < target) {
117
+ if (text.charCodeAt(pos) === 10) line++;
118
+ pos++;
119
+ }
120
+ out.push(line);
121
+ }
122
+ return out;
123
+ }
124
+ function findSimilarity(haystack, needle) {
125
+ if (needle.length < 20) return void 0;
126
+ const probe = needle.slice(0, Math.min(40, needle.length));
127
+ const idx = haystack.indexOf(probe);
128
+ if (idx === -1) return void 0;
129
+ let line = 1;
130
+ for (let i = 0; i < idx; i++) {
131
+ if (haystack.charCodeAt(i) === 10) line++;
132
+ }
133
+ return line;
134
+ }
135
+
136
+ export { editTool };
137
+ //# sourceMappingURL=edit.js.map
138
+ //# sourceMappingURL=edit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/_util.ts","../src/edit.ts"],"names":["stat"],"mappings":"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","file":"edit.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';\nimport {\n atomicWrite,\n detectNewlineStyle,\n toStyle,\n normalizeToLf,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface EditInput {\n path: string;\n old_string: string;\n new_string: string;\n /**\n * When true, replaces all occurrences of `old_string`.\n * When false (default), replaces only the first occurrence and errors\n * if more than one match exists — use this to ensure you target the\n * right location.\n */\n replace_all?: boolean;\n}\n\ninterface EditOutput {\n path: string;\n replacements: number;\n diff: string;\n}\n\nexport const editTool: Tool<EditInput, EditOutput> = {\n name: 'edit',\n description:\n 'Make a surgical edit by replacing exact text. Fails if `old_string` is not unique unless `replace_all` is true.',\n usageHint:\n '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`.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n replace_all: { type: 'boolean' },\n },\n required: ['path', 'old_string', 'new_string'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('edit: path is required');\n if (input.old_string === undefined) throw new Error('edit: old_string is required');\n if (input.new_string === undefined) throw new Error('edit: new_string is required');\n if (input.old_string === '') throw new Error('edit: old_string cannot be empty');\n\n const absPath = safeResolve(input.path, ctx);\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new Error(`edit: file \"${input.path}\" does not exist. Use \\`write\\` instead.`);\n }\n throw err;\n });\n if (!stat.isFile()) throw new Error(`edit: \"${input.path}\" is not a regular file`);\n\n // Read-before-write invariant\n if (!ctx.hasRead(absPath)) {\n throw new Error(\n `edit: file \"${input.path}\" was not read in this session. Read it first.`,\n );\n }\n // Stale-read detection\n const lastReadMtime = ctx.lastReadMtime(absPath);\n if (lastReadMtime !== undefined && stat.mtimeMs > lastReadMtime + 1) {\n throw new Error(`edit: file \"${input.path}\" was modified externally. Re-read it first.`);\n }\n\n const original = await fs.readFile(absPath, 'utf8');\n const style = detectNewlineStyle(original);\n const fileLf = normalizeToLf(original);\n const oldLf = normalizeToLf(input.old_string);\n const newLf = normalizeToLf(input.new_string);\n\n if (oldLf === newLf) {\n return {\n path: absPath,\n replacements: 0,\n diff: '(no-op: old and new are identical)',\n };\n }\n\n let count = 0;\n let idx = fileLf.indexOf(oldLf);\n const matches: number[] = [];\n while (idx !== -1) {\n matches.push(idx);\n count++;\n idx = fileLf.indexOf(oldLf, idx + 1);\n }\n\n if (count === 0) {\n const hint = findSimilarity(fileLf, oldLf);\n throw new Error(\n `edit: no match for old_string in \"${input.path}\".${\n hint ? ` Nearest match near line ${hint}.` : ''\n }`,\n );\n }\n\n if (count > 1 && !input.replace_all) {\n const lines = lineNumbersFor(fileLf, matches);\n throw new Error(\n `edit: old_string matched ${count} times in \"${input.path}\" (lines: ${lines.join(', ')}). ` +\n `Add more context to make it unique, or set replace_all: true.`,\n );\n }\n\n const newFileLf = input.replace_all\n ? fileLf.split(oldLf).join(newLf)\n : fileLf.replace(oldLf, newLf);\n const newFile = toStyle(newFileLf, style);\n\n await atomicWrite(absPath, newFile, { mode: stat.mode & 0o777 });\n const updated = await fs.stat(absPath);\n ctx.recordRead(absPath, updated.mtimeMs);\n\n const diff = unifiedDiff(original, newFile, {\n fromFile: input.path,\n toFile: input.path,\n });\n\n return {\n path: absPath,\n replacements: input.replace_all ? count : 1,\n diff,\n };\n },\n};\n\nfunction lineNumbersFor(text: string, indices: number[]): number[] {\n const out: number[] = [];\n let pos = 0;\n let line = 1;\n for (const target of indices) {\n while (pos < target) {\n if (text.charCodeAt(pos) === 0x0a) line++;\n pos++;\n }\n out.push(line);\n }\n return out;\n}\n\nfunction findSimilarity(haystack: string, needle: string): number | undefined {\n if (needle.length < 20) return undefined;\n const probe = needle.slice(0, Math.min(40, needle.length));\n const idx = haystack.indexOf(probe);\n if (idx === -1) return undefined;\n let line = 1;\n for (let i = 0; i < idx; i++) {\n if (haystack.charCodeAt(i) === 0x0a) line++;\n }\n return line;\n}\n"]}
package/dist/exec.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface ExecInput {
4
+ command: string;
5
+ args?: string[];
6
+ cwd?: string;
7
+ timeout?: number;
8
+ allow_unknown?: boolean;
9
+ }
10
+ interface ExecOutput {
11
+ command: string;
12
+ args: string[];
13
+ stdout: string;
14
+ stderr: string;
15
+ exitCode: number;
16
+ truncated: boolean;
17
+ allowed: boolean;
18
+ }
19
+ declare const execTool: Tool<ExecInput, ExecOutput>;
20
+
21
+ export { execTool };