@wrongstack/tools 0.1.4 → 0.1.8

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 (51) hide show
  1. package/README.md +127 -0
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +103 -5
  4. package/dist/bash.js.map +1 -1
  5. package/dist/builtin.js +550 -258
  6. package/dist/builtin.js.map +1 -1
  7. package/dist/diff.js +5 -9
  8. package/dist/diff.js.map +1 -1
  9. package/dist/document.js +0 -1
  10. package/dist/document.js.map +1 -1
  11. package/dist/edit.js +2 -2
  12. package/dist/edit.js.map +1 -1
  13. package/dist/exec.d.ts +0 -1
  14. package/dist/exec.js +105 -44
  15. package/dist/exec.js.map +1 -1
  16. package/dist/fetch.js +110 -25
  17. package/dist/fetch.js.map +1 -1
  18. package/dist/format.js.map +1 -1
  19. package/dist/git.d.ts +0 -1
  20. package/dist/git.js +9 -9
  21. package/dist/git.js.map +1 -1
  22. package/dist/glob.js +0 -1
  23. package/dist/glob.js.map +1 -1
  24. package/dist/grep.js +58 -3
  25. package/dist/grep.js.map +1 -1
  26. package/dist/index.js +549 -257
  27. package/dist/index.js.map +1 -1
  28. package/dist/install.js.map +1 -1
  29. package/dist/lint.js.map +1 -1
  30. package/dist/logs.js +61 -6
  31. package/dist/logs.js.map +1 -1
  32. package/dist/outdated.js.map +1 -1
  33. package/dist/patch.js +68 -29
  34. package/dist/patch.js.map +1 -1
  35. package/dist/read.js +0 -1
  36. package/dist/read.js.map +1 -1
  37. package/dist/replace.js +59 -9
  38. package/dist/replace.js.map +1 -1
  39. package/dist/scaffold.js +5 -6
  40. package/dist/scaffold.js.map +1 -1
  41. package/dist/test.js.map +1 -1
  42. package/dist/todo.js +1 -1
  43. package/dist/todo.js.map +1 -1
  44. package/dist/tool-use.js +0 -8
  45. package/dist/tool-use.js.map +1 -1
  46. package/dist/tree.js +9 -5
  47. package/dist/tree.js.map +1 -1
  48. package/dist/typecheck.js.map +1 -1
  49. package/dist/write.js +0 -1
  50. package/dist/write.js.map +1 -1
  51. package/package.json +7 -4
package/dist/read.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/read.ts"],"names":["stat"],"mappings":"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","file":"read.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 type { Tool } from '@wrongstack/core';\nimport { safeResolve, isBinaryBuffer } from './_util.js';\n\ninterface ReadInput {\n path: string;\n offset?: number;\n limit?: number;\n}\n\ninterface ReadOutput {\n text: string;\n total_lines: number;\n encoding: string;\n truncated: boolean;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport const readTool: Tool<ReadInput, ReadOutput> = {\n name: 'read',\n description: 'Read the contents of a file. Lines are 1-indexed and prefixed with line numbers.',\n usageHint:\n 'Read a file before editing it. Returns lines numbered like ` 1→content`. Use `offset` and `limit` for large files (default reads up to 2000 lines).',\n permission: 'auto',\n mutating: false,\n maxOutputBytes: 262_144,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'File path (absolute or relative to cwd)' },\n offset: { type: 'integer', description: '1-based line number to start from' },\n limit: { type: 'integer', description: 'Max lines to read (default 2000)' },\n },\n required: ['path'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('read: path is required');\n const absPath = safeResolve(input.path, ctx);\n\n const stat = await fs.stat(absPath);\n if (!stat.isFile()) throw new Error(`read: \"${input.path}\" is not a regular file`);\n if (stat.size > MAX_BYTES) {\n throw new Error(`read: file too large (${stat.size} bytes, limit ${MAX_BYTES})`);\n }\n\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) {\n throw new Error(`read: \"${input.path}\" appears to be binary`);\n }\n\n const text = buf.toString('utf8');\n const allLines = text.split(/\\r\\n|\\r|\\n/);\n const total = allLines.length;\n const offset = Math.max(1, input.offset ?? 1);\n const limit = Math.max(1, Math.min(input.limit ?? 2000, 5000));\n const slice = allLines.slice(offset - 1, offset - 1 + limit);\n const truncated = offset - 1 + slice.length < total;\n\n const width = String(offset + slice.length - 1).length;\n const numbered = slice\n .map((line, i) => `${String(offset + i).padStart(width, ' ')}→${line}`)\n .join('\\n');\n\n ctx.recordRead(absPath, stat.mtimeMs);\n\n return {\n text: numbered,\n total_lines: total,\n encoding: 'utf8',\n truncated,\n };\n },\n};\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/read.ts"],"names":["stat"],"mappings":"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","file":"read.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\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\n","import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve, isBinaryBuffer } from './_util.js';\n\ninterface ReadInput {\n path: string;\n offset?: number;\n limit?: number;\n}\n\ninterface ReadOutput {\n text: string;\n total_lines: number;\n encoding: string;\n truncated: boolean;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport const readTool: Tool<ReadInput, ReadOutput> = {\n name: 'read',\n description: 'Read the contents of a file. Lines are 1-indexed and prefixed with line numbers.',\n usageHint:\n 'Read a file before editing it. Returns lines numbered like ` 1→content`. Use `offset` and `limit` for large files (default reads up to 2000 lines).',\n permission: 'auto',\n mutating: false,\n maxOutputBytes: 262_144,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'File path (absolute or relative to cwd)' },\n offset: { type: 'integer', description: '1-based line number to start from' },\n limit: { type: 'integer', description: 'Max lines to read (default 2000)' },\n },\n required: ['path'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('read: path is required');\n const absPath = safeResolve(input.path, ctx);\n\n const stat = await fs.stat(absPath);\n if (!stat.isFile()) throw new Error(`read: \"${input.path}\" is not a regular file`);\n if (stat.size > MAX_BYTES) {\n throw new Error(`read: file too large (${stat.size} bytes, limit ${MAX_BYTES})`);\n }\n\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) {\n throw new Error(`read: \"${input.path}\" appears to be binary`);\n }\n\n const text = buf.toString('utf8');\n const allLines = text.split(/\\r\\n|\\r|\\n/);\n const total = allLines.length;\n const offset = Math.max(1, input.offset ?? 1);\n const limit = Math.max(1, Math.min(input.limit ?? 2000, 5000));\n const slice = allLines.slice(offset - 1, offset - 1 + limit);\n const truncated = offset - 1 + slice.length < total;\n\n const width = String(offset + slice.length - 1).length;\n const numbered = slice\n .map((line, i) => `${String(offset + i).padStart(width, ' ')}→${line}`)\n .join('\\n');\n\n ctx.recordRead(absPath, stat.mtimeMs);\n\n return {\n text: numbered,\n total_lines: total,\n encoding: 'utf8',\n truncated,\n };\n },\n};\n"]}
package/dist/replace.js CHANGED
@@ -27,6 +27,42 @@ function isBinaryBuffer(buf) {
27
27
  return false;
28
28
  }
29
29
 
30
+ // src/_regex.ts
31
+ var MAX_PATTERN_LEN = 512;
32
+ var DANGEROUS_PATTERNS = [
33
+ /(\([^)]*[+*][^)]*\))[+*]/,
34
+ // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
35
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
36
+ // same, with non-capturing group
37
+ ];
38
+ function compileUserRegex(pattern, flags) {
39
+ if (typeof pattern !== "string") {
40
+ return { ok: false, reason: "pattern must be a string" };
41
+ }
42
+ if (pattern.length === 0) {
43
+ return { ok: false, reason: "pattern is empty" };
44
+ }
45
+ if (pattern.length > MAX_PATTERN_LEN) {
46
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
47
+ }
48
+ for (const rx of DANGEROUS_PATTERNS) {
49
+ if (rx.test(pattern)) {
50
+ return {
51
+ ok: false,
52
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
53
+ };
54
+ }
55
+ }
56
+ try {
57
+ return { ok: true, regex: new RegExp(pattern, flags) };
58
+ } catch (err) {
59
+ return {
60
+ ok: false,
61
+ reason: err instanceof Error ? err.message : "invalid regex"
62
+ };
63
+ }
64
+ }
65
+
30
66
  // src/replace.ts
31
67
  var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
32
68
  var replaceTool = {
@@ -55,23 +91,38 @@ var replaceTool = {
55
91
  if (!input?.pattern) throw new Error("replace: pattern is required");
56
92
  if (input.replacement === void 0) throw new Error("replace: replacement is required");
57
93
  if (!input?.files) throw new Error("replace: files is required");
58
- const re = new RegExp(input.pattern, "g");
94
+ const replaceAll = input.replace_all ?? true;
95
+ const compiled = compileUserRegex(input.pattern, replaceAll ? "g" : "");
96
+ if (!compiled.ok) {
97
+ throw new Error(`replace: ${compiled.reason}`);
98
+ }
99
+ const re = compiled.regex;
59
100
  const globRe = input.glob ? compileGlob(input.glob) : null;
60
101
  const dryRun = input.dry_run ?? false;
61
- const replaceAll = input.replace_all ?? true;
62
102
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
63
103
  const fileList = await resolveFiles(filesInput, ctx, globRe);
64
104
  const results = [];
65
105
  let totalReplacements = 0;
66
106
  for (const absPath of fileList) {
67
- const stat2 = await fs.stat(absPath).catch((err) => {
107
+ const lstat2 = await fs.lstat(absPath).catch((err) => {
68
108
  if (err.code === "ENOENT") return null;
69
109
  throw err;
70
110
  });
111
+ if (!lstat2 || !lstat2.isFile()) continue;
112
+ if (lstat2.isSymbolicLink()) continue;
113
+ let realPath;
114
+ try {
115
+ realPath = await fs.realpath(absPath);
116
+ } catch {
117
+ continue;
118
+ }
119
+ const rel = path.relative(ctx.projectRoot, realPath);
120
+ if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
121
+ const stat2 = await fs.stat(realPath).catch(() => null);
71
122
  if (!stat2 || !stat2.isFile()) continue;
72
123
  let content;
73
124
  try {
74
- const buf = await fs.readFile(absPath);
125
+ const buf = await fs.readFile(realPath);
75
126
  if (isBinaryBuffer(buf)) continue;
76
127
  content = buf.toString("utf8");
77
128
  } catch {
@@ -82,13 +133,12 @@ var replaceTool = {
82
133
  re.lastIndex = 0;
83
134
  const matches = [...contentLf.matchAll(re)];
84
135
  if (matches.length === 0) continue;
85
- const newContentLf = replaceAll ? contentLf.replace(re, input.replacement) : contentLf.replace(re, input.replacement);
136
+ const newContentLf = contentLf.replace(re, input.replacement);
86
137
  re.lastIndex = 0;
87
- const actualCount = replaceAll ? matches.length : 1;
88
- totalReplacements += actualCount;
138
+ totalReplacements += matches.length;
89
139
  if (!dryRun) {
90
140
  const newContent = toStyle(newContentLf, style);
91
- await atomicWrite(absPath, newContent, { mode: stat2.mode & 511 });
141
+ await atomicWrite(realPath, newContent, { mode: stat2.mode & 511 });
92
142
  }
93
143
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), { fromFile: absPath, toFile: absPath }) : void 0;
94
144
  results.push({
@@ -123,7 +173,7 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
123
173
  return resolved;
124
174
  }
125
175
  async function globFiles(pattern, base, extraGlob) {
126
- const { spawn: spawn3 } = await import('child_process');
176
+ const { spawn: spawn2 } = await import('child_process');
127
177
  const rgAvailable = await checkRg();
128
178
  if (rgAvailable) {
129
179
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/replace.ts"],"names":["stat","spawn","resolve","path2"],"mappings":"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","file":"replace.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 {\n atomicWrite,\n detectNewlineStyle,\n toStyle,\n normalizeToLf,\n compileGlob,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Tool, Context } from '@wrongstack/core';\nimport { isBinaryBuffer, safeResolve } from './_util.js';\n\ninterface ReplaceInput {\n pattern: string;\n replacement: string;\n files: string | string[];\n glob?: string;\n replace_all?: boolean;\n dry_run?: boolean;\n}\n\ninterface ReplaceOutput {\n files_modified: number;\n total_replacements: number;\n results: { path: string; replacements: number; diff?: string }[];\n dry_run: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];\n\nexport const replaceTool: Tool<ReplaceInput, ReplaceOutput> = {\n name: 'replace',\n description:\n 'Batch replace a pattern across multiple files matched by glob. Returns diff for each modified file.',\n usageHint:\n 'Use `glob` for broad patterns (e.g. \"**/*.ts\"). Set `dry_run: true` to preview without modifying. `files` can be a single path, comma-separated list, or glob pattern.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: { type: 'string', description: 'Regex pattern to match' },\n replacement: { type: 'string', description: 'Replacement string' },\n files: {\n type: 'string',\n description: 'File(s) to target: single path, comma-separated list, or glob pattern',\n },\n glob: { type: 'string', description: 'Additional glob filter (e.g. \"*.ts\")' },\n replace_all: { type: 'boolean', description: 'Replace all occurrences in each file (default: true)' },\n dry_run: { type: 'boolean', description: 'Preview changes without writing' },\n },\n required: ['pattern', 'replacement', 'files'],\n },\n async execute(input: ReplaceInput, ctx: Context) {\n if (!input?.pattern) throw new Error('replace: pattern is required');\n if (input.replacement === undefined) throw new Error('replace: replacement is required');\n if (!input?.files) throw new Error('replace: files is required');\n\n const re = new RegExp(input.pattern, 'g');\n const globRe = input.glob ? compileGlob(input.glob) : null;\n const dryRun = input.dry_run ?? false;\n const replaceAll = input.replace_all ?? true;\n\n const filesInput = Array.isArray(input.files) ? input.files.join(',') : input.files;\n const fileList = await resolveFiles(filesInput, ctx, globRe);\n\n const results: ReplaceOutput['results'] = [];\n let totalReplacements = 0;\n\n for (const absPath of fileList) {\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n });\n if (!stat || !stat.isFile()) continue;\n\n let content: string;\n try {\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) continue;\n content = buf.toString('utf8');\n } catch {\n continue;\n }\n\n const style = detectNewlineStyle(content);\n const contentLf = normalizeToLf(content);\n re.lastIndex = 0;\n const matches = [...contentLf.matchAll(re)];\n if (matches.length === 0) continue;\n\n const newContentLf = replaceAll\n ? contentLf.replace(re, input.replacement)\n : contentLf.replace(re, input.replacement);\n re.lastIndex = 0;\n\n // Only count replacements that were actually performed.\n const actualCount = replaceAll ? matches.length : 1;\n totalReplacements += actualCount;\n\n if (!dryRun) {\n const newContent = toStyle(newContentLf, style);\n await atomicWrite(absPath, newContent, { mode: stat.mode & 0o777 });\n }\n\n const diff = dryRun || matches.length > 0\n ? unifiedDiff(content, toStyle(newContentLf, style), { fromFile: absPath, toFile: absPath })\n : undefined;\n\n results.push({\n path: absPath,\n replacements: matches.length,\n diff,\n });\n }\n\n return {\n files_modified: results.length,\n total_replacements: totalReplacements,\n results,\n dry_run: dryRun,\n };\n },\n};\n\nasync function resolveFiles(\n filesInput: string,\n ctx: Context,\n extraGlob?: RegExp | null,\n): Promise<string[]> {\n const base = ctx.cwd;\n const normalized = filesInput.trim();\n\n if (normalized.startsWith('**/') || normalized.startsWith('*') || normalized.includes('**')) {\n return await globFiles(normalized, base, extraGlob);\n }\n\n const parts = normalized.split(',').map((s) => s.trim()).filter(Boolean);\n const resolved: string[] = [];\n\n for (const p of parts) {\n const absPath = safeResolve(p, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (stat?.isFile()) {\n resolved.push(absPath);\n }\n }\n\n return resolved;\n}\n\nasync function globFiles(pattern: string, base: string, extraGlob?: RegExp | null): Promise<string[]> {\n const { spawn } = await import('node:child_process');\n const results: string[] = [];\n\n const rgAvailable = await checkRg();\n if (rgAvailable) {\n try {\n const { promise } = spawnRgFind(pattern, base);\n return await promise;\n } catch {\n // fall through\n }\n }\n\n return await globNative(pattern, base, extraGlob);\n}\n\nfunction checkRg(): Promise<boolean> {\n return new Promise((resolve) => {\n try {\n const p = spawn('rg', ['--version'], { stdio: 'ignore' });\n p.on('error', () => resolve(false));\n p.on('close', (code) => resolve(code === 0));\n } catch {\n resolve(false);\n }\n });\n}\n\nfunction spawnRgFind(pattern: string, base: string): { promise: Promise<string[]> } {\n const args = ['--files', '--glob', pattern, base];\n const child = spawn('rg', args, { stdio: ['ignore', 'pipe', 'pipe'] });\n let buf = '';\n child.stdout?.on('data', (chunk: Buffer) => { buf += chunk.toString(); });\n return {\n promise: new Promise((resolve, reject) => {\n child.on('error', reject);\n child.on('close', () => {\n resolve(buf.split('\\n').filter(Boolean));\n });\n }),\n };\n}\n\nasync function globNative(pattern: string, base: string, extraGlob?: RegExp | null): Promise<string[]> {\n const results: string[] = [];\n const globRe = compileGlob(pattern);\n\n const walk = async (dir: string): Promise<void> => {\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (DEFAULT_IGNORE.includes(e.name)) continue;\n const full = path.join(dir, e.name);\n if (e.isDirectory()) {\n await walk(full);\n } else if (e.isFile()) {\n const name = e.name;\n if (globRe.test(name) || globRe.test(full)) {\n if (extraGlob && !extraGlob.test(name) && !extraGlob.test(full)) continue;\n results.push(full);\n }\n globRe.lastIndex = 0;\n if (extraGlob) extraGlob.lastIndex = 0;\n }\n }\n };\n\n await walk(base);\n return results;\n}"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/_regex.ts","../src/replace.ts"],"names":["lstat","path2","stat","spawn","resolve"],"mappings":"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","file":"replace.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\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\n","/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 512;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n /(\\([^)]*[+*][^)]*\\))[+*]/, // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/, // same, with non-capturing group\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(\n pattern: string,\n flags: string,\n): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason: 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport {\n atomicWrite,\n detectNewlineStyle,\n toStyle,\n normalizeToLf,\n compileGlob,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Tool, Context } from '@wrongstack/core';\nimport { isBinaryBuffer, safeResolve } from './_util.js';\nimport { compileUserRegex } from './_regex.js';\n\ninterface ReplaceInput {\n pattern: string;\n replacement: string;\n files: string | string[];\n glob?: string;\n replace_all?: boolean;\n dry_run?: boolean;\n}\n\ninterface ReplaceOutput {\n files_modified: number;\n total_replacements: number;\n results: { path: string; replacements: number; diff?: string }[];\n dry_run: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];\n\nexport const replaceTool: Tool<ReplaceInput, ReplaceOutput> = {\n name: 'replace',\n description:\n 'Batch replace a pattern across multiple files matched by glob. Returns diff for each modified file.',\n usageHint:\n 'Use `glob` for broad patterns (e.g. \"**/*.ts\"). Set `dry_run: true` to preview without modifying. `files` can be a single path, comma-separated list, or glob pattern.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: { type: 'string', description: 'Regex pattern to match' },\n replacement: { type: 'string', description: 'Replacement string' },\n files: {\n type: 'string',\n description: 'File(s) to target: single path, comma-separated list, or glob pattern',\n },\n glob: { type: 'string', description: 'Additional glob filter (e.g. \"*.ts\")' },\n replace_all: { type: 'boolean', description: 'Replace all occurrences in each file (default: true)' },\n dry_run: { type: 'boolean', description: 'Preview changes without writing' },\n },\n required: ['pattern', 'replacement', 'files'],\n },\n async execute(input: ReplaceInput, ctx: Context) {\n if (!input?.pattern) throw new Error('replace: pattern is required');\n if (input.replacement === undefined) throw new Error('replace: replacement is required');\n if (!input?.files) throw new Error('replace: files is required');\n\n const replaceAll = input.replace_all ?? true;\n // L-8 (audit) fix: when replace_all is false we previously still used the\n // 'g' flag, so both branches replaced every occurrence. Build flags from\n // replaceAll for correct semantics.\n const compiled = compileUserRegex(input.pattern, replaceAll ? 'g' : '');\n if (!compiled.ok) {\n throw new Error(`replace: ${compiled.reason}`);\n }\n const re = compiled.regex;\n const globRe = input.glob ? compileGlob(input.glob) : null;\n const dryRun = input.dry_run ?? false;\n\n const filesInput = Array.isArray(input.files) ? input.files.join(',') : input.files;\n const fileList = await resolveFiles(filesInput, ctx, globRe);\n\n const results: ReplaceOutput['results'] = [];\n let totalReplacements = 0;\n\n for (const absPath of fileList) {\n // Use lstat to detect symlinks. resolveFiles already applies\n // safeResolve, but a symlink with a target outside the project\n // root would still pass that string check — explicitly skip it\n // so we never read or write through a link.\n const lstat = await fs.lstat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n });\n if (!lstat || !lstat.isFile()) continue;\n if (lstat.isSymbolicLink()) continue;\n\n // Cross-check via realpath: if the resolved target lives outside the\n // project root (e.g. a bind mount or a parent-dir traversal we missed),\n // skip rather than rewrite through it.\n let realPath: string;\n try {\n realPath = await fs.realpath(absPath);\n } catch {\n continue;\n }\n const rel = path.relative(ctx.projectRoot, realPath);\n if (rel.startsWith('..') || path.isAbsolute(rel)) continue;\n\n // Now stat the real target so we use its mode for atomicWrite.\n const stat = await fs.stat(realPath).catch(() => null);\n if (!stat || !stat.isFile()) continue;\n\n let content: string;\n try {\n const buf = await fs.readFile(realPath);\n if (isBinaryBuffer(buf)) continue;\n content = buf.toString('utf8');\n } catch {\n continue;\n }\n\n const style = detectNewlineStyle(content);\n const contentLf = normalizeToLf(content);\n re.lastIndex = 0;\n const matches = [...contentLf.matchAll(re)];\n if (matches.length === 0) continue;\n\n // The 'g' flag on `re` is set iff replaceAll, so a single replace()\n // call covers both modes: with 'g' it replaces every match, without\n // it replaces only the first. matchAll already returns the correct\n // count for both modes — replaceAll=true → all matches, false → at\n // most one — so totalReplacements is accurate either way.\n const newContentLf = contentLf.replace(re, input.replacement);\n re.lastIndex = 0;\n totalReplacements += matches.length;\n\n if (!dryRun) {\n const newContent = toStyle(newContentLf, style);\n // Write to the real path (already validated inside project root)\n // so atomicWrite's temp-and-rename can't be redirected through a\n // freshly-planted symlink at absPath.\n await atomicWrite(realPath, newContent, { mode: stat.mode & 0o777 });\n }\n\n const diff = dryRun || matches.length > 0\n ? unifiedDiff(content, toStyle(newContentLf, style), { fromFile: absPath, toFile: absPath })\n : undefined;\n\n results.push({\n path: absPath,\n replacements: matches.length,\n diff,\n });\n }\n\n return {\n files_modified: results.length,\n total_replacements: totalReplacements,\n results,\n dry_run: dryRun,\n };\n },\n};\n\nasync function resolveFiles(\n filesInput: string,\n ctx: Context,\n extraGlob?: RegExp | null,\n): Promise<string[]> {\n const base = ctx.cwd;\n const normalized = filesInput.trim();\n\n if (normalized.startsWith('**/') || normalized.startsWith('*') || normalized.includes('**')) {\n return await globFiles(normalized, base, extraGlob);\n }\n\n const parts = normalized.split(',').map((s) => s.trim()).filter(Boolean);\n const resolved: string[] = [];\n\n for (const p of parts) {\n const absPath = safeResolve(p, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (stat?.isFile()) {\n resolved.push(absPath);\n }\n }\n\n return resolved;\n}\n\nasync function globFiles(pattern: string, base: string, extraGlob?: RegExp | null): Promise<string[]> {\n const { spawn } = await import('node:child_process');\n const results: string[] = [];\n\n const rgAvailable = await checkRg();\n if (rgAvailable) {\n try {\n const { promise } = spawnRgFind(pattern, base);\n return await promise;\n } catch {\n // fall through\n }\n }\n\n return await globNative(pattern, base, extraGlob);\n}\n\nfunction checkRg(): Promise<boolean> {\n return new Promise((resolve) => {\n try {\n const p = spawn('rg', ['--version'], { stdio: 'ignore' });\n p.on('error', () => resolve(false));\n p.on('close', (code) => resolve(code === 0));\n } catch {\n resolve(false);\n }\n });\n}\n\nfunction spawnRgFind(pattern: string, base: string): { promise: Promise<string[]> } {\n const args = ['--files', '--glob', pattern, base];\n const child = spawn('rg', args, { stdio: ['ignore', 'pipe', 'pipe'] });\n let buf = '';\n child.stdout?.on('data', (chunk: Buffer) => { buf += chunk.toString(); });\n return {\n promise: new Promise((resolve, reject) => {\n child.on('error', reject);\n child.on('close', () => {\n resolve(buf.split('\\n').filter(Boolean));\n });\n }),\n };\n}\n\nasync function globNative(pattern: string, base: string, extraGlob?: RegExp | null): Promise<string[]> {\n const results: string[] = [];\n const globRe = compileGlob(pattern);\n\n const walk = async (dir: string): Promise<void> => {\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (DEFAULT_IGNORE.includes(e.name)) continue;\n const full = path.join(dir, e.name);\n if (e.isDirectory()) {\n await walk(full);\n } else if (e.isFile()) {\n const name = e.name;\n if (globRe.test(name) || globRe.test(full)) {\n if (extraGlob && !extraGlob.test(name) && !extraGlob.test(full)) continue;\n results.push(full);\n }\n globRe.lastIndex = 0;\n if (extraGlob) extraGlob.lastIndex = 0;\n }\n }\n };\n\n await walk(base);\n return results;\n}"]}
package/dist/scaffold.js CHANGED
@@ -1,6 +1,5 @@
1
+ import * as fs from 'fs/promises';
1
2
  import * as path from 'path';
2
- import * as fsSync from 'fs';
3
- import 'child_process';
4
3
 
5
4
  // src/scaffold.ts
6
5
  function resolvePath(input, ctx) {
@@ -136,7 +135,7 @@ var scaffoldTool = {
136
135
  const vars = { name, ...input.vars };
137
136
  const builtIn = BUILT_IN_TEMPLATES[input.template];
138
137
  if (builtIn) {
139
- return handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
138
+ return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
140
139
  }
141
140
  return {
142
141
  template: input.template,
@@ -148,15 +147,15 @@ var scaffoldTool = {
148
147
  };
149
148
  }
150
149
  };
151
- function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
150
+ async function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
152
151
  const files = [];
153
152
  let filesCreated = 0;
154
153
  for (const [filePath, content] of Object.entries(templateFiles)) {
155
154
  const resolvedPath = substituteVars(filePath, name, vars);
156
155
  const fullPath = path.join(cwd, resolvedPath);
157
156
  if (!dryRun) {
158
- fsSync.mkdirSync(path.dirname(fullPath), { recursive: true });
159
- fsSync.writeFileSync(fullPath, substituteVars(content, name, vars), "utf8");
157
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
158
+ await fs.writeFile(fullPath, substituteVars(content, name, vars), "utf8");
160
159
  }
161
160
  files.push(resolvedPath);
162
161
  filesCreated++;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/scaffold.ts"],"names":["path2"],"mappings":"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","file":"scaffold.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 * as fsSync from 'node:fs';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface ScaffoldInput {\n template: string;\n name: string;\n cwd?: string;\n vars?: Record<string, string>;\n dry_run?: boolean;\n}\n\ninterface ScaffoldOutput {\n template: string;\n name: string;\n files_created: number;\n files: string[];\n dry_run: boolean;\n output: string;\n}\n\nconst BUILT_IN_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = {\n 'npm-package': {\n description: 'Basic npm package with ESM',\n files: {\n 'package.json': JSON.stringify({\n name: '{{name}}',\n version: '0.1.1',\n type: 'module',\n main: './dist/index.js',\n scripts: { build: 'tsc', test: 'vitest run' },\n devDependencies: { typescript: '^5.0.0' },\n }, null, 2),\n 'tsconfig.json': JSON.stringify({\n compilerOptions: { target: 'ES2022', module: 'ESNext', strict: true },\n include: ['src'],\n }, null, 2),\n 'src/index.ts': `export function hello() {\\n return 'Hello from {{name}}';\\n}\\n`,\n 'src/index.test.ts': `import { hello } from './index';\\nimport { describe, it, expect } from 'vitest';\\n\\ndescribe('hello', () => {\\n it('returns greeting', () => {\\n expect(hello()).toBe('Hello from {{name}}');\\n });\\n});\\n`,\n },\n },\n 'cli-tool': {\n description: 'CLI tool with argparse',\n files: {\n 'package.json': JSON.stringify({\n name: '{{name}}',\n version: '0.1.1',\n type: 'module',\n bin: { '{{name}}': './src/index.js' },\n scripts: { build: 'tsc', start: 'node dist/index.js' },\n }, null, 2),\n 'src/index.ts': `#!/usr/bin/env node\\n\\nasync function main() {\\n console.log('Hello from {{name}}');\\n}\\n\\nmain();\\n`,\n },\n },\n 'react-component': {\n description: 'React component with TypeScript',\n files: {\n '{{name}}.tsx': `interface {{Name}}Props {\\n className?: string;\\n}\\n\\nexport function {{Name}}({ className }: {{Name}}Props) {\\n return (\\n <div className={className}>\\n {{Name}} Component\\n </div>\\n );\\n}\\n`,\n '{{name}}.test.tsx': `import { render, screen } from '@testing-library/react';\\nimport { {{Name}} } from './{{Name}}';\\n\\ndescribe('{{Name}}', () => {\\n it('renders', () => {\\n render(<{{Name}} />);\\n expect(screen.getByText('{{Name}} Component')).toBeInTheDocument();\\n });\\n});\\n`,\n },\n },\n};\n\nexport const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput> = {\n name: 'scaffold',\n description:\n 'Generate boilerplate code from built-in templates or paths. Creates package.json, source files, tests.',\n usageHint:\n 'Set `template` (npm-package, cli-tool, react-component) and `name`. `vars` for template variables. `dry_run` preview.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n template: {\n type: 'string',\n description: 'Template name (npm-package, cli-tool, react-component) or path to template directory',\n },\n name: {\n type: 'string',\n description: 'Project/component name (used in generated files)',\n },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n vars: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'Template variables for custom templates',\n },\n dry_run: {\n type: 'boolean',\n description: 'Preview generated files without creating (default: false)',\n },\n },\n required: ['template', 'name'],\n },\n async execute(input, ctx) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const name = input.name;\n const vars = { name, ...input.vars };\n\n const builtIn = BUILT_IN_TEMPLATES[input.template];\n if (builtIn) {\n return handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);\n }\n\n return {\n template: input.template,\n name,\n files_created: 0,\n files: [],\n dry_run: input.dry_run ?? false,\n output: `Template \"${input.template}\" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(', ')}`,\n };\n },\n};\n\nfunction handleBuiltIn(\n name: string,\n templateFiles: Record<string, string>,\n cwd: string,\n dryRun: boolean,\n vars: Record<string, string>,\n): ScaffoldOutput {\n const files: string[] = [];\n let filesCreated = 0;\n\n for (const [filePath, content] of Object.entries(templateFiles)) {\n const resolvedPath = substituteVars(filePath, name, vars);\n const fullPath = path.join(cwd, resolvedPath);\n\n if (!dryRun) {\n fsSync.mkdirSync(path.dirname(fullPath), { recursive: true });\n fsSync.writeFileSync(fullPath, substituteVars(content, name, vars), 'utf8');\n }\n files.push(resolvedPath);\n filesCreated++;\n }\n\n return {\n template: 'built-in',\n name,\n files_created: filesCreated,\n files,\n dry_run: dryRun,\n output: dryRun\n ? `Would create ${filesCreated} files: ${files.join(', ')}`\n : `Created ${filesCreated} files: ${files.join(', ')}`,\n };\n}\n\nfunction substituteVars(content: string, name: string, vars: Record<string, string>): string {\n let result = content;\n result = result.replace(/\\{\\{name\\}\\}/g, name.toLowerCase().replace(/\\s+/g, '-'));\n result = result.replace(/\\{\\{Name\\}\\}/g, name.replace(/(?:^|[-_\\s]+)([a-z])/g, (_, c) => c.toUpperCase()));\n for (const [k, v] of Object.entries(vars)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), v);\n }\n return result;\n}"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/scaffold.ts"],"names":["path2"],"mappings":"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","file":"scaffold.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\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\n","import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface ScaffoldInput {\n template: string;\n name: string;\n cwd?: string;\n vars?: Record<string, string>;\n dry_run?: boolean;\n}\n\ninterface ScaffoldOutput {\n template: string;\n name: string;\n files_created: number;\n files: string[];\n dry_run: boolean;\n output: string;\n}\n\nconst BUILT_IN_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = {\n 'npm-package': {\n description: 'Basic npm package with ESM',\n files: {\n 'package.json': JSON.stringify({\n name: '{{name}}',\n version: '0.1.1',\n type: 'module',\n main: './dist/index.js',\n scripts: { build: 'tsc', test: 'vitest run' },\n devDependencies: { typescript: '^5.0.0' },\n }, null, 2),\n 'tsconfig.json': JSON.stringify({\n compilerOptions: { target: 'ES2022', module: 'ESNext', strict: true },\n include: ['src'],\n }, null, 2),\n 'src/index.ts': `export function hello() {\\n return 'Hello from {{name}}';\\n}\\n`,\n 'src/index.test.ts': `import { hello } from './index';\\nimport { describe, it, expect } from 'vitest';\\n\\ndescribe('hello', () => {\\n it('returns greeting', () => {\\n expect(hello()).toBe('Hello from {{name}}');\\n });\\n});\\n`,\n },\n },\n 'cli-tool': {\n description: 'CLI tool with argparse',\n files: {\n 'package.json': JSON.stringify({\n name: '{{name}}',\n version: '0.1.1',\n type: 'module',\n bin: { '{{name}}': './src/index.js' },\n scripts: { build: 'tsc', start: 'node dist/index.js' },\n }, null, 2),\n 'src/index.ts': `#!/usr/bin/env node\\n\\nasync function main() {\\n console.log('Hello from {{name}}');\\n}\\n\\nmain();\\n`,\n },\n },\n 'react-component': {\n description: 'React component with TypeScript',\n files: {\n '{{name}}.tsx': `interface {{Name}}Props {\\n className?: string;\\n}\\n\\nexport function {{Name}}({ className }: {{Name}}Props) {\\n return (\\n <div className={className}>\\n {{Name}} Component\\n </div>\\n );\\n}\\n`,\n '{{name}}.test.tsx': `import { render, screen } from '@testing-library/react';\\nimport { {{Name}} } from './{{Name}}';\\n\\ndescribe('{{Name}}', () => {\\n it('renders', () => {\\n render(<{{Name}} />);\\n expect(screen.getByText('{{Name}} Component')).toBeInTheDocument();\\n });\\n});\\n`,\n },\n },\n};\n\nexport const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput> = {\n name: 'scaffold',\n description:\n 'Generate boilerplate code from built-in templates or paths. Creates package.json, source files, tests.',\n usageHint:\n 'Set `template` (npm-package, cli-tool, react-component) and `name`. `vars` for template variables. `dry_run` preview.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n template: {\n type: 'string',\n description: 'Template name (npm-package, cli-tool, react-component) or path to template directory',\n },\n name: {\n type: 'string',\n description: 'Project/component name (used in generated files)',\n },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n vars: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'Template variables for custom templates',\n },\n dry_run: {\n type: 'boolean',\n description: 'Preview generated files without creating (default: false)',\n },\n },\n required: ['template', 'name'],\n },\n async execute(input, ctx) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const name = input.name;\n const vars = { name, ...input.vars };\n\n const builtIn = BUILT_IN_TEMPLATES[input.template];\n if (builtIn) {\n return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);\n }\n\n return {\n template: input.template,\n name,\n files_created: 0,\n files: [],\n dry_run: input.dry_run ?? false,\n output: `Template \"${input.template}\" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(', ')}`,\n };\n },\n};\n\nasync function handleBuiltIn(\n name: string,\n templateFiles: Record<string, string>,\n cwd: string,\n dryRun: boolean,\n vars: Record<string, string>,\n): Promise<ScaffoldOutput> {\n const files: string[] = [];\n let filesCreated = 0;\n\n for (const [filePath, content] of Object.entries(templateFiles)) {\n const resolvedPath = substituteVars(filePath, name, vars);\n const fullPath = path.join(cwd, resolvedPath);\n\n if (!dryRun) {\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\n await fs.writeFile(fullPath, substituteVars(content, name, vars), 'utf8');\n }\n files.push(resolvedPath);\n filesCreated++;\n }\n\n return {\n template: 'built-in',\n name,\n files_created: filesCreated,\n files,\n dry_run: dryRun,\n output: dryRun\n ? `Would create ${filesCreated} files: ${files.join(', ')}`\n : `Created ${filesCreated} files: ${files.join(', ')}`,\n };\n}\n\nfunction substituteVars(content: string, name: string, vars: Record<string, string>): string {\n let result = content;\n result = result.replace(/\\{\\{name\\}\\}/g, name.toLowerCase().replace(/\\s+/g, '-'));\n result = result.replace(/\\{\\{Name\\}\\}/g, name.replace(/(?:^|[-_\\s]+)([a-z])/g, (_, c) => c.toUpperCase()));\n for (const [k, v] of Object.entries(vars)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), v);\n }\n return result;\n}"]}
package/dist/test.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/test.ts"],"names":["resolve","path2"],"mappings":"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","file":"test.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 TestInput {\r\n files?: string | string[];\r\n runner?: 'vitest' | 'jest' | 'mocha' | 'auto';\r\n watch?: boolean;\r\n coverage?: boolean;\r\n cwd?: string;\r\n grep?: string;\r\n timeout?: number;\r\n}\r\n\r\ninterface TestOutput {\r\n runner: string;\r\n exit_code: number;\r\n tests_run: number;\r\n passed: number;\r\n failed: number;\r\n duration_ms: number;\r\n output: string;\r\n truncated: boolean;\r\n}\r\n\r\nexport const testTool: Tool<TestInput, TestOutput> = {\r\n name: 'test',\r\n description:\r\n 'Run tests with vitest, jest, or mocha. Returns pass/fail counts and output.',\r\n usageHint:\r\n 'Set `files` for specific tests. `watch` enables watch mode. `coverage` generates coverage report. `grep` filters by name.',\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 files: {\r\n type: 'string',\r\n description: 'Test files: single path, comma-separated list, or glob (e.g. \"**/*.test.ts\")',\r\n },\r\n runner: {\r\n type: 'string',\r\n enum: ['vitest', 'jest', 'mocha', 'auto'],\r\n description: 'Test runner (default: auto-detect)',\r\n },\r\n watch: { type: 'boolean', description: 'Run in watch mode (default: false)' },\r\n coverage: { type: 'boolean', description: 'Generate coverage report (default: false)' },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n grep: { type: 'string', description: 'Filter tests by name pattern (default: none)' },\r\n timeout: { type: 'integer', description: 'Test timeout in ms (default: 30000)' },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: TestOutput | undefined;\r\n for await (const ev of testTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('test: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<TestOutput>> {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const runner = input.runner ?? 'auto';\r\n\r\n const detected = runner === 'auto' ? await detectRunner(cwd) : runner;\r\n if (!detected) {\r\n yield {\r\n type: 'final',\r\n output: {\r\n runner: 'none',\r\n exit_code: 1,\r\n tests_run: 0,\r\n passed: 0,\r\n failed: 0,\r\n duration_ms: 0,\r\n output: 'No test runner found (vitest.config.ts, jest.config.js, .mocharc.json)',\r\n truncated: false,\r\n },\r\n };\r\n return;\r\n }\r\n\r\n yield { type: 'log', text: `Running ${detected}…`, data: { runner: detected } };\r\n\r\n const start = Date.now();\r\n const args = buildArgs(detected, input);\r\n\r\n const result = yield* spawnStream({\r\n cmd: detected,\r\n args,\r\n cwd,\r\n signal: opts.signal,\r\n maxBytes: 200_000,\r\n });\r\n const duration = Date.now() - start;\r\n\r\n yield { type: 'final', output: parseResult(detected, result, duration) };\r\n },\r\n};\r\n\r\nasync function detectRunner(cwd: string): Promise<string | null> {\r\n const { stat } = await import('node:fs/promises');\r\n const candidates = ['vitest.config.ts', 'jest.config.js', '.mocharc.json'];\r\n for (const f of candidates) {\r\n try {\r\n await stat(path.join(cwd, f));\r\n if (f.includes('vitest')) return 'vitest';\r\n if (f.includes('jest')) return 'jest';\r\n if (f.includes('mocha')) return 'mocha';\r\n } catch {\r\n // continue\r\n }\r\n }\r\n return 'vitest';\r\n}\r\n\r\nfunction buildArgs(runner: string, input: TestInput): string[] {\r\n const args: string[] = [];\r\n const timeout = input.timeout ?? 30000;\r\n\r\n switch (runner) {\r\n case 'vitest':\r\n args.push('run', '--reporter=verbose');\r\n if (input.watch) { args[1] = ''; args.push('watch'); }\r\n if (input.coverage) args.push('--coverage');\r\n if (input.grep) args.push('--testNamePattern', input.grep);\r\n args.push('--testTimeout', String(timeout));\r\n break;\r\n case 'jest':\r\n args.push('--verbose');\r\n if (input.watch) args.push('--watch');\r\n if (input.coverage) args.push('--coverage');\r\n if (input.grep) args.push('--testPathPattern', input.grep);\r\n args.push('--testTimeout', String(timeout));\r\n break;\r\n case 'mocha':\r\n args.push('--reporter', 'spec');\r\n if (input.grep) args.push('--grep', input.grep);\r\n args.push('--timeout', String(timeout));\r\n break;\r\n }\r\n\r\n if (input.files) {\r\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\r\n args.push('--', ...files.map((f) => f.trim()));\r\n }\r\n\r\n return args;\r\n}\r\n\r\nfunction parseResult(\r\n runner: string,\r\n result: { stdout: string; stderr: string; exitCode: number; truncated: boolean; error?: string },\r\n duration: number,\r\n): TestOutput {\r\n const out = result.stdout + result.stderr;\r\n\r\n let tests_run = 0;\r\n let passed = 0;\r\n let failed = 0;\r\n\r\n if (runner === 'vitest') {\r\n const passedMatch = out.match(/(\\d+) passed/);\r\n const failedMatch = out.match(/(\\d+) failed/);\r\n if (passedMatch?.[1]) passed = Number.parseInt(passedMatch[1], 10);\r\n if (failedMatch?.[1]) failed = Number.parseInt(failedMatch[1], 10);\r\n tests_run = passed + failed;\r\n } else if (runner === 'jest') {\r\n const suitesMatch = out.match(/Test Suites:\\s+(\\d+)\\s+total/);\r\n const passedMatch = out.match(/Tests:\\s+(\\d+)\\s+passed/);\r\n const failedMatch = out.match(/Tests:\\s+(\\d+)\\s+failed/);\r\n tests_run = Number.parseInt(suitesMatch?.[1] ?? '0', 10);\r\n passed = Number.parseInt(passedMatch?.[1] ?? '0', 10);\r\n failed = Number.parseInt(failedMatch?.[1] ?? '0', 10);\r\n }\r\n\r\n return {\r\n runner,\r\n exit_code: result.exitCode,\r\n tests_run,\r\n passed,\r\n failed,\r\n duration_ms: duration,\r\n output: result.stdout || result.error || '',\r\n truncated: result.truncated,\r\n };\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/_spawn-stream.ts","../src/test.ts"],"names":["resolve","path2"],"mappings":"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","file":"test.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\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\n","import { spawn } from 'node:child_process';\nimport type { ToolProgressEvent } from '@wrongstack/core';\n\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number;\n}\n\n/**\n * Spawn a child process and yield `partial_output` progress events as\n * stdout/stderr arrive (batched by byte threshold), then return the full\n * buffered result. Shared between install/lint/format/typecheck/test/audit\n * so the TUI live tail sees consistent progress regardless of which tool\n * is running.\n */\nexport async function* spawnStream(\n opts: SpawnStreamOptions,\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\n const max = opts.maxBytes ?? 200_000;\n const flushAt = opts.flushBytes ?? 4 * 1024;\n let stdout = '';\n let stderr = '';\n let pending = '';\n let error: string | undefined;\n\n const child = spawn(opts.cmd, opts.args, {\n cwd: opts.cwd,\n signal: opts.signal,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\n const queue: Chunk[] = [];\n let waiter: (() => void) | undefined;\n const wake = () => {\n if (waiter) {\n const w = waiter;\n waiter = undefined;\n w();\n }\n };\n\n child.stdout?.on('data', (c) => {\n const s = c.toString();\n if (stdout.length < max) stdout += s;\n queue.push({ kind: 'out', data: s });\n wake();\n });\n child.stderr?.on('data', (c) => {\n const s = c.toString();\n if (stderr.length < max) stderr += s;\n queue.push({ kind: 'err', data: s });\n wake();\n });\n child.on('error', (e) => {\n error = e.message;\n queue.push({ kind: 'error', data: e.message });\n wake();\n });\n child.on('close', (code) => {\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\n wake();\n });\n\n let exitCode = 0;\n let spawnFailed = false;\n for (;;) {\n while (queue.length === 0) {\n await new Promise<void>((resolve) => {\n waiter = resolve;\n });\n }\n const chunk = queue.shift()!;\n if (chunk.kind === 'close') {\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\n // rather than the negative platform code Node fabricates.\n if (!spawnFailed) exitCode = chunk.code ?? 0;\n break;\n }\n if (chunk.kind === 'error') {\n spawnFailed = true;\n exitCode = 1;\n // close usually follows\n continue;\n }\n pending += chunk.data;\n if (pending.length >= flushAt) {\n yield { type: 'partial_output', text: pending };\n pending = '';\n }\n }\n if (pending.length > 0) {\n yield { type: 'partial_output', text: pending };\n }\n\n return {\n stdout,\n stderr,\n exitCode,\n truncated: stdout.length >= max || stderr.length >= max,\n error,\n };\n}\n","import * as path from 'node:path';\r\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\nimport { spawnStream } from './_spawn-stream.js';\n\r\ninterface TestInput {\r\n files?: string | string[];\r\n runner?: 'vitest' | 'jest' | 'mocha' | 'auto';\r\n watch?: boolean;\r\n coverage?: boolean;\r\n cwd?: string;\r\n grep?: string;\r\n timeout?: number;\r\n}\r\n\r\ninterface TestOutput {\r\n runner: string;\r\n exit_code: number;\r\n tests_run: number;\r\n passed: number;\r\n failed: number;\r\n duration_ms: number;\r\n output: string;\r\n truncated: boolean;\r\n}\r\n\r\nexport const testTool: Tool<TestInput, TestOutput> = {\r\n name: 'test',\r\n description:\r\n 'Run tests with vitest, jest, or mocha. Returns pass/fail counts and output.',\r\n usageHint:\r\n 'Set `files` for specific tests. `watch` enables watch mode. `coverage` generates coverage report. `grep` filters by name.',\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 files: {\r\n type: 'string',\r\n description: 'Test files: single path, comma-separated list, or glob (e.g. \"**/*.test.ts\")',\r\n },\r\n runner: {\r\n type: 'string',\r\n enum: ['vitest', 'jest', 'mocha', 'auto'],\r\n description: 'Test runner (default: auto-detect)',\r\n },\r\n watch: { type: 'boolean', description: 'Run in watch mode (default: false)' },\r\n coverage: { type: 'boolean', description: 'Generate coverage report (default: false)' },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n grep: { type: 'string', description: 'Filter tests by name pattern (default: none)' },\r\n timeout: { type: 'integer', description: 'Test timeout in ms (default: 30000)' },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: TestOutput | undefined;\r\n for await (const ev of testTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('test: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<TestOutput>> {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const runner = input.runner ?? 'auto';\r\n\r\n const detected = runner === 'auto' ? await detectRunner(cwd) : runner;\r\n if (!detected) {\r\n yield {\r\n type: 'final',\r\n output: {\r\n runner: 'none',\r\n exit_code: 1,\r\n tests_run: 0,\r\n passed: 0,\r\n failed: 0,\r\n duration_ms: 0,\r\n output: 'No test runner found (vitest.config.ts, jest.config.js, .mocharc.json)',\r\n truncated: false,\r\n },\r\n };\r\n return;\r\n }\r\n\r\n yield { type: 'log', text: `Running ${detected}…`, data: { runner: detected } };\r\n\r\n const start = Date.now();\r\n const args = buildArgs(detected, input);\r\n\r\n const result = yield* spawnStream({\r\n cmd: detected,\r\n args,\r\n cwd,\r\n signal: opts.signal,\r\n maxBytes: 200_000,\r\n });\r\n const duration = Date.now() - start;\r\n\r\n yield { type: 'final', output: parseResult(detected, result, duration) };\r\n },\r\n};\r\n\r\nasync function detectRunner(cwd: string): Promise<string | null> {\r\n const { stat } = await import('node:fs/promises');\r\n const candidates = ['vitest.config.ts', 'jest.config.js', '.mocharc.json'];\r\n for (const f of candidates) {\r\n try {\r\n await stat(path.join(cwd, f));\r\n if (f.includes('vitest')) return 'vitest';\r\n if (f.includes('jest')) return 'jest';\r\n if (f.includes('mocha')) return 'mocha';\r\n } catch {\r\n // continue\r\n }\r\n }\r\n return 'vitest';\r\n}\r\n\r\nfunction buildArgs(runner: string, input: TestInput): string[] {\r\n const args: string[] = [];\r\n const timeout = input.timeout ?? 30000;\r\n\r\n switch (runner) {\r\n case 'vitest':\r\n args.push('run', '--reporter=verbose');\r\n if (input.watch) { args[1] = ''; args.push('watch'); }\r\n if (input.coverage) args.push('--coverage');\r\n if (input.grep) args.push('--testNamePattern', input.grep);\r\n args.push('--testTimeout', String(timeout));\r\n break;\r\n case 'jest':\r\n args.push('--verbose');\r\n if (input.watch) args.push('--watch');\r\n if (input.coverage) args.push('--coverage');\r\n if (input.grep) args.push('--testPathPattern', input.grep);\r\n args.push('--testTimeout', String(timeout));\r\n break;\r\n case 'mocha':\r\n args.push('--reporter', 'spec');\r\n if (input.grep) args.push('--grep', input.grep);\r\n args.push('--timeout', String(timeout));\r\n break;\r\n }\r\n\r\n if (input.files) {\r\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\r\n args.push('--', ...files.map((f) => f.trim()));\r\n }\r\n\r\n return args;\r\n}\r\n\r\nfunction parseResult(\r\n runner: string,\r\n result: { stdout: string; stderr: string; exitCode: number; truncated: boolean; error?: string },\r\n duration: number,\r\n): TestOutput {\r\n const out = result.stdout + result.stderr;\r\n\r\n let tests_run = 0;\r\n let passed = 0;\r\n let failed = 0;\r\n\r\n if (runner === 'vitest') {\r\n const passedMatch = out.match(/(\\d+) passed/);\r\n const failedMatch = out.match(/(\\d+) failed/);\r\n if (passedMatch?.[1]) passed = Number.parseInt(passedMatch[1], 10);\r\n if (failedMatch?.[1]) failed = Number.parseInt(failedMatch[1], 10);\r\n tests_run = passed + failed;\r\n } else if (runner === 'jest') {\r\n const suitesMatch = out.match(/Test Suites:\\s+(\\d+)\\s+total/);\r\n const passedMatch = out.match(/Tests:\\s+(\\d+)\\s+passed/);\r\n const failedMatch = out.match(/Tests:\\s+(\\d+)\\s+failed/);\r\n tests_run = Number.parseInt(suitesMatch?.[1] ?? '0', 10);\r\n passed = Number.parseInt(passedMatch?.[1] ?? '0', 10);\r\n failed = Number.parseInt(failedMatch?.[1] ?? '0', 10);\r\n }\r\n\r\n return {\r\n runner,\r\n exit_code: result.exitCode,\r\n tests_run,\r\n passed,\r\n failed,\r\n duration_ms: duration,\r\n output: result.stdout || result.error || '',\r\n truncated: result.truncated,\r\n };\r\n}\r\n"]}
package/dist/todo.js CHANGED
@@ -40,7 +40,7 @@ var todoTool = {
40
40
  }
41
41
  }
42
42
  }
43
- ctx.todos = items;
43
+ ctx.state.replaceTodos(items);
44
44
  return {
45
45
  count: items.length,
46
46
  in_progress: items.filter((t) => t.status === "in_progress").length
package/dist/todo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/todo.ts"],"names":[],"mappings":";AAWO,IAAM,QAAA,GAAwC;AAAA,EACnD,IAAA,EAAM,MAAA;AAAA,EACN,WAAA,EAAa,wDAAA;AAAA,EACb,SAAA,EACE,uMAAA;AAAA,EACF,UAAA,EAAY,MAAA;AAAA,EACZ,QAAA,EAAU,KAAA;AAAA,EACV,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,OAAA;AAAA,QACN,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,QAAA;AAAA,UACN,UAAA,EAAY;AAAA,YACV,EAAA,EAAI,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,YACrB,OAAA,EAAS,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,YAC1B,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,MAAM,CAAC,SAAA,EAAW,aAAA,EAAe,WAAW,CAAA,EAAE;AAAA,YACxE,UAAA,EAAY,EAAE,IAAA,EAAM,QAAA;AAAS,WAC/B;AAAA,UACA,QAAA,EAAU,CAAC,IAAA,EAAM,SAAA,EAAW,QAAQ;AAAA;AACtC;AACF,KACF;AAAA,IACA,QAAA,EAAU,CAAC,OAAO;AAAA,GACpB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK;AACxB,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAA,EAAO,KAAK,CAAA,EAAG;AAChC,MAAA,MAAM,IAAI,MAAM,8BAA8B,CAAA;AAAA,IAChD;AACA,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAqB,OAAA,CAAQ,CAAA,EAAG,EAAA,IAAM,CAAA,CAAE,OAAO,CAAC,CAAA;AAClF,IAAA,MAAM,aAAa,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,aAAa,CAAA;AACjE,IAAA,IAAI,UAAA,CAAW,SAAS,CAAA,EAAG;AAEzB,MAAA,IAAI,cAAA,GAAiB,KAAA;AACrB,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,IAAI,IAAA,CAAK,WAAW,aAAA,EAAe;AACjC,UAAA,IAAI,cAAA,OAAqB,MAAA,GAAS,SAAA;AAClC,UAAA,cAAA,GAAiB,IAAA;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AACA,IAAA,GAAA,CAAI,KAAA,GAAQ,KAAA;AACZ,IAAA,OAAO;AAAA,MACL,OAAO,KAAA,CAAM,MAAA;AAAA,MACb,WAAA,EAAa,MAAM,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,MAAA,KAAW,aAAa,CAAA,CAAE;AAAA,KAC/D;AAAA,EACF;AACF","file":"todo.js","sourcesContent":["import type { Tool, TodoItem } from '@wrongstack/core';\n\ninterface TodoInput {\n todos: TodoItem[];\n}\n\ninterface TodoOutput {\n count: number;\n in_progress: number;\n}\n\nexport const todoTool: Tool<TodoInput, TodoOutput> = {\n name: 'todo',\n description: 'Replace the current todo list with a new set of items.',\n usageHint:\n 'Use for multi-step tasks. Replace the full list on each call. At most ONE task may be in_progress at a time. Items have id, content, status (pending|in_progress|completed), and optional activeForm.',\n permission: 'auto',\n mutating: false,\n timeoutMs: 1_000,\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n content: { type: 'string' },\n status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },\n activeForm: { type: 'string' },\n },\n required: ['id', 'content', 'status'],\n },\n },\n },\n required: ['todos'],\n },\n async execute(input, ctx) {\n if (!Array.isArray(input?.todos)) {\n throw new Error('todo: todos must be an array');\n }\n const items = input.todos.filter((t): t is TodoItem => Boolean(t?.id && t.content));\n const inProgress = items.filter((t) => t.status === 'in_progress');\n if (inProgress.length > 1) {\n // Keep only the first as in_progress, mark rest pending\n let seenInProgress = false;\n for (const item of items) {\n if (item.status === 'in_progress') {\n if (seenInProgress) item.status = 'pending';\n seenInProgress = true;\n }\n }\n }\n ctx.todos = items;\n return {\n count: items.length,\n in_progress: items.filter((t) => t.status === 'in_progress').length,\n };\n },\n};\n"]}
1
+ {"version":3,"sources":["../src/todo.ts"],"names":[],"mappings":";AAWO,IAAM,QAAA,GAAwC;AAAA,EACnD,IAAA,EAAM,MAAA;AAAA,EACN,WAAA,EAAa,wDAAA;AAAA,EACb,SAAA,EACE,uMAAA;AAAA,EACF,UAAA,EAAY,MAAA;AAAA,EACZ,QAAA,EAAU,KAAA;AAAA,EACV,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,OAAA;AAAA,QACN,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,QAAA;AAAA,UACN,UAAA,EAAY;AAAA,YACV,EAAA,EAAI,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,YACrB,OAAA,EAAS,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,YAC1B,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,MAAM,CAAC,SAAA,EAAW,aAAA,EAAe,WAAW,CAAA,EAAE;AAAA,YACxE,UAAA,EAAY,EAAE,IAAA,EAAM,QAAA;AAAS,WAC/B;AAAA,UACA,QAAA,EAAU,CAAC,IAAA,EAAM,SAAA,EAAW,QAAQ;AAAA;AACtC;AACF,KACF;AAAA,IACA,QAAA,EAAU,CAAC,OAAO;AAAA,GACpB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK;AACxB,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAA,EAAO,KAAK,CAAA,EAAG;AAChC,MAAA,MAAM,IAAI,MAAM,8BAA8B,CAAA;AAAA,IAChD;AACA,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAqB,OAAA,CAAQ,CAAA,EAAG,EAAA,IAAM,CAAA,CAAE,OAAO,CAAC,CAAA;AAClF,IAAA,MAAM,aAAa,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,aAAa,CAAA;AACjE,IAAA,IAAI,UAAA,CAAW,SAAS,CAAA,EAAG;AAEzB,MAAA,IAAI,cAAA,GAAiB,KAAA;AACrB,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,IAAI,IAAA,CAAK,WAAW,aAAA,EAAe;AACjC,UAAA,IAAI,cAAA,OAAqB,MAAA,GAAS,SAAA;AAClC,UAAA,cAAA,GAAiB,IAAA;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AACA,IAAA,GAAA,CAAI,KAAA,CAAM,aAAa,KAAK,CAAA;AAC5B,IAAA,OAAO;AAAA,MACL,OAAO,KAAA,CAAM,MAAA;AAAA,MACb,WAAA,EAAa,MAAM,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,MAAA,KAAW,aAAa,CAAA,CAAE;AAAA,KAC/D;AAAA,EACF;AACF","file":"todo.js","sourcesContent":["import type { Tool, TodoItem } from '@wrongstack/core';\n\ninterface TodoInput {\n todos: TodoItem[];\n}\n\ninterface TodoOutput {\n count: number;\n in_progress: number;\n}\n\nexport const todoTool: Tool<TodoInput, TodoOutput> = {\n name: 'todo',\n description: 'Replace the current todo list with a new set of items.',\n usageHint:\n 'Use for multi-step tasks. Replace the full list on each call. At most ONE task may be in_progress at a time. Items have id, content, status (pending|in_progress|completed), and optional activeForm.',\n permission: 'auto',\n mutating: false,\n timeoutMs: 1_000,\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n content: { type: 'string' },\n status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },\n activeForm: { type: 'string' },\n },\n required: ['id', 'content', 'status'],\n },\n },\n },\n required: ['todos'],\n },\n async execute(input, ctx) {\n if (!Array.isArray(input?.todos)) {\n throw new Error('todo: todos must be an array');\n }\n const items = input.todos.filter((t): t is TodoItem => Boolean(t?.id && t.content));\n const inProgress = items.filter((t) => t.status === 'in_progress');\n if (inProgress.length > 1) {\n // Keep only the first as in_progress, mark rest pending\n let seenInProgress = false;\n for (const item of items) {\n if (item.status === 'in_progress') {\n if (seenInProgress) item.status = 'pending';\n seenInProgress = true;\n }\n }\n }\n ctx.state.replaceTodos(items);\n return {\n count: items.length,\n in_progress: items.filter((t) => t.status === 'in_progress').length,\n };\n },\n};\n"]}
package/dist/tool-use.js CHANGED
@@ -47,14 +47,6 @@ var toolUseTool = {
47
47
  executionMs: Date.now() - start
48
48
  };
49
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
50
  try {
59
51
  const result = await tool.execute(input.input, ctx, opts);
60
52
  return {
@@ -1 +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};"]}
1
+ {"version":3,"sources":["../src/tool-use.ts"],"names":[],"mappings":";AAeO,IAAM,WAAA,GAAiD;AAAA,EAC5D,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EACE,2GAAA;AAAA,EACF,SAAA,EACE,gGAAA;AAAA,EACF,UAAA,EAAY,SAAA;AAAA,EACZ,QAAA,EAAU,KAAA;AAAA,EACV,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,IAAA,EAAM;AAAA,QACJ,IAAA,EAAM,QAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACf;AAAA,MACA,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,QAAA;AAAA,QACN,WAAA,EAAa;AAAA;AACf,KACF;AAAA,IACA,QAAA,EAAU,CAAC,MAAM;AAAA,GACnB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM;AAC9B,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,EAAI;AAEvB,IAAA,IAAI,CAAC,OAAO,IAAA,EAAM;AAChB,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,SAAA;AAAA,QACN,OAAA,EAAS,KAAA;AAAA,QACT,KAAA,EAAO,iCAAA;AAAA,QACP,WAAA,EAAa;AAAA,OACf;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,IAAI,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAY,CAAA,CAAE,IAAA,KAAS,KAAA,CAAM,IAAI,CAAA;AAC9D,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO;AAAA,QACL,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,OAAA,EAAS,KAAA;AAAA,QACT,KAAA,EAAO,CAAA,gBAAA,EAAmB,KAAA,CAAM,IAAI,CAAA,WAAA,CAAA;AAAA,QACpC,WAAA,EAAa,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC5B;AAAA,IACF;AAKA,IAAA,IAAI,IAAA,CAAK,eAAe,MAAA,EAAQ;AAC9B,MAAA,OAAO;AAAA,QACL,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,OAAA,EAAS,KAAA;AAAA,QACT,KAAA,EAAO,CAAA,gBAAA,EAAmB,KAAA,CAAM,IAAI,CAAA,qBAAA,CAAA;AAAA,QACpC,WAAA,EAAa,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC5B;AAAA,IACF;AAWA,IAAA,IAAI;AACF,MAAA,MAAM,SAAS,MAAM,IAAA,CAAK,QAAQ,KAAA,CAAM,KAAA,EAAO,KAAK,IAAI,CAAA;AACxD,MAAA,OAAO;AAAA,QACL,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,OAAA,EAAS,IAAA;AAAA,QACT,MAAA;AAAA,QACA,WAAA,EAAa,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC5B;AAAA,IACF,SAAS,CAAA,EAAG;AACV,MAAA,OAAO;AAAA,QACL,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,OAAA,EAAS,KAAA;AAAA,QACT,OAAO,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,OAAO,CAAC,CAAA;AAAA,QAChD,WAAA,EAAa,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC5B;AAAA,IACF;AAAA,EACF;AACF","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 // `deny` is a hard policy gate — bypassing it through a meta-tool\n // would defeat the whole point of the permission system. Keep this\n // check even though the outer `tool_use` already requires `confirm`.\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 // Note: inner `permission === 'confirm'` is intentionally NOT short-\n // circuited here. The outer `tool_use` itself has `permission: 'confirm'`,\n // so the user already saw the full args (including which inner tool will\n // run, and with what input) before approving the meta-call. Duplicating\n // the check inside execute() turned every confirm-tool dispatch through\n // `tool_use` into a hard failure — the model would see \"requires\n // confirmation\" with no way to proceed, even after the user said yes.\n // `batch_tool_use` already follows this same model.\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.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
- import 'child_process';
4
3
 
5
4
  // src/tree.ts
6
5
  function resolvePath(input, ctx) {
@@ -112,10 +111,15 @@ var treeTool = {
112
111
  if (queue.length > 0) {
113
112
  yield queue.shift();
114
113
  } else {
115
- await Promise.race([
116
- walkPromise,
117
- new Promise((r) => setTimeout(r, 50))
118
- ]).catch(() => void 0);
114
+ let pollTimer;
115
+ const poll = new Promise((r) => {
116
+ pollTimer = setTimeout(r, 50);
117
+ });
118
+ try {
119
+ await Promise.race([walkPromise, poll]).catch(() => void 0);
120
+ } finally {
121
+ if (pollTimer) clearTimeout(pollTimer);
122
+ }
119
123
  }
120
124
  }
121
125
  await walkPromise;