@wrongstack/tools 0.89.1 → 0.89.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/audit.js +1 -7
  2. package/dist/audit.js.map +1 -1
  3. package/dist/{background-indexer-DYm1FUxK.d.ts → background-indexer-C2014mH0.d.ts} +1 -1
  4. package/dist/bash.js +6 -18
  5. package/dist/bash.js.map +1 -1
  6. package/dist/builtin.js +45 -133
  7. package/dist/builtin.js.map +1 -1
  8. package/dist/circuit-breaker.js +1 -1
  9. package/dist/circuit-breaker.js.map +1 -1
  10. package/dist/codebase-index/index.d.ts +2 -21
  11. package/dist/codebase-index/index.js +25 -69
  12. package/dist/codebase-index/index.js.map +1 -1
  13. package/dist/diff.js.map +1 -1
  14. package/dist/document.js +1 -1
  15. package/dist/document.js.map +1 -1
  16. package/dist/edit.js.map +1 -1
  17. package/dist/exec.js +3 -15
  18. package/dist/exec.js.map +1 -1
  19. package/dist/fetch.js.map +1 -1
  20. package/dist/format.js +4 -16
  21. package/dist/format.js.map +1 -1
  22. package/dist/git.js +1 -7
  23. package/dist/git.js.map +1 -1
  24. package/dist/glob.js.map +1 -1
  25. package/dist/grep.js +1 -7
  26. package/dist/grep.js.map +1 -1
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +159 -136
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js +4 -16
  31. package/dist/install.js.map +1 -1
  32. package/dist/lint.js +4 -16
  33. package/dist/lint.js.map +1 -1
  34. package/dist/logs.js.map +1 -1
  35. package/dist/memory.d.ts +29 -1
  36. package/dist/memory.js +115 -4
  37. package/dist/memory.js.map +1 -1
  38. package/dist/outdated.js.map +1 -1
  39. package/dist/pack.js +45 -133
  40. package/dist/pack.js.map +1 -1
  41. package/dist/patch.js.map +1 -1
  42. package/dist/process-registry.js +2 -7
  43. package/dist/process-registry.js.map +1 -1
  44. package/dist/read.js +1 -1
  45. package/dist/read.js.map +1 -1
  46. package/dist/replace.js +1 -7
  47. package/dist/replace.js.map +1 -1
  48. package/dist/scaffold.js.map +1 -1
  49. package/dist/search.js +2 -7
  50. package/dist/search.js.map +1 -1
  51. package/dist/test.js +4 -16
  52. package/dist/test.js.map +1 -1
  53. package/dist/tree.js +1 -7
  54. package/dist/tree.js.map +1 -1
  55. package/dist/typecheck.js +4 -16
  56. package/dist/typecheck.js.map +1 -1
  57. package/dist/write.js.map +1 -1
  58. package/package.json +2 -2
@@ -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 fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n\n\nfunction expectDefined<T>(value: T | null | undefined): T {\n if (value === null || value === undefined) {\n throw new Error('Expected value to be defined');\n }\n return value;\n}\n\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(expectDefined(lines[i]), `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(expectDefined(lines[k]));\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { atomicWrite } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface ScaffoldInput {\r\n template: string;\r\n name: string;\r\n cwd?: string | undefined;\r\n vars?: Record<string, string>;\r\n dry_run?: boolean | undefined;\r\n}\r\n\r\ninterface ScaffoldOutput {\r\n template: string;\r\n name: string;\r\n files_created: number;\r\n files: string[];\r\n dry_run: boolean;\r\n output: string;\r\n}\r\n\r\nconst BUILT_IN_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = {\r\n 'npm-package': {\r\n description: 'Basic npm package with ESM',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n main: './dist/index.js',\r\n scripts: { build: 'tsc', test: 'vitest run' },\r\n devDependencies: { typescript: '^5.0.0' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'tsconfig.json': JSON.stringify(\r\n {\r\n compilerOptions: { target: 'ES2022', module: 'ESNext', strict: true },\r\n include: ['src'],\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `export function hello() {\\n return 'Hello from {{name}}';\\n}\\n`,\r\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`,\r\n },\r\n },\r\n 'cli-tool': {\r\n description: 'CLI tool with argparse',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n bin: { '{{name}}': './src/index.js' },\r\n scripts: { build: 'tsc', start: 'node dist/index.js' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `#!/usr/bin/env node\\n\\nasync function main() {\\n console.log('Hello from {{name}}');\\n}\\n\\nmain();\\n`,\r\n },\r\n },\r\n 'react-component': {\r\n description: 'React component with TypeScript',\r\n files: {\r\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`,\r\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`,\r\n },\r\n },\r\n};\r\n\r\nexport const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput> = {\r\n name: 'scaffold',\r\n category: 'Project',\r\n description:\r\n 'Generate new files and folder structures from built-in templates or custom definitions. ' +\r\n 'This is the recommended way to bootstrap new packages, components, or modules instead of creating files one by one with `write`.',\r\n usageHint:\r\n 'PREFERRED FOR SCAFFOLDING:\\n\\n' +\r\n '- Use built-in templates when they match your needs (e.g. react-component, npm-package).\\n' +\r\n '- Supports `dry_run` so you can preview exactly what will be created.\\n' +\r\n '- Has the powerful `fs.write.outside-project` capability — review paths carefully.\\n' +\r\n 'Much cleaner and safer than manually writing multiple files.',\r\n permission: 'confirm',\r\n mutating: true,\r\n capabilities: ['fs.write.outside-project', 'fs.write'],\r\n timeoutMs: 30_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n template: {\r\n type: 'string',\r\n description:\r\n 'Template name (npm-package, cli-tool, react-component) or path to template directory',\r\n },\r\n name: {\r\n type: 'string',\r\n description: 'Project/component name (used in generated files)',\r\n },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n vars: {\r\n type: 'object',\r\n additionalProperties: { type: 'string' },\r\n description: 'Template variables for custom templates',\r\n },\r\n dry_run: {\r\n type: 'boolean',\r\n description: 'Preview generated files without creating (default: false)',\r\n },\r\n },\r\n required: ['template', 'name'],\r\n },\r\n async execute(input, ctx) {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const name = input.name;\r\n const vars = { name, ...input.vars };\r\n\r\n const builtIn = BUILT_IN_TEMPLATES[input.template];\r\n if (builtIn) {\r\n return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);\r\n }\r\n\r\n return {\r\n template: input.template,\r\n name,\r\n files_created: 0,\r\n files: [],\r\n dry_run: input.dry_run ?? false,\r\n output: `Template \"${input.template}\" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(', ')}`,\r\n };\r\n },\r\n};\r\n\r\nasync function handleBuiltIn(\r\n name: string,\r\n templateFiles: Record<string, string>,\r\n cwd: string,\r\n ctx: Parameters<Tool['execute']>[1],\r\n dryRun: boolean,\r\n vars: Record<string, string>,\r\n): Promise<ScaffoldOutput> {\r\n const files: string[] = [];\r\n let filesCreated = 0;\r\n\r\n for (const [filePath, content] of Object.entries(templateFiles)) {\r\n const resolvedPath = substituteVars(filePath, name, vars);\r\n const joinedPath = path.join(cwd, resolvedPath);\r\n // Ensure generated files cannot escape the project root via template variable injection (e.g. name containing \"../\")\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(joinedPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`scaffold: generated path \"${resolvedPath}\" would escape project root`);\r\n }\r\n const fullPath = target;\r\n\r\n if (!dryRun) {\r\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\r\n // atomicWrite: scaffolded files land in the user's tracked tree.\r\n // A torn write here would commit a corrupt file to their repo.\r\n await atomicWrite(fullPath, substituteVars(content, name, vars));\r\n }\r\n files.push(resolvedPath);\r\n filesCreated++;\r\n }\r\n\r\n return {\r\n template: 'built-in',\r\n name,\r\n files_created: filesCreated,\r\n files,\r\n dry_run: dryRun,\r\n output: dryRun\r\n ? `Would create ${filesCreated} files: ${files.join(', ')}`\r\n : `Created ${filesCreated} files: ${files.join(', ')}`,\r\n };\r\n}\r\n\r\nfunction substituteVars(content: string, name: string, vars: Record<string, string>): string {\r\n let result = content;\r\n result = result.replace(/\\{\\{name\\}\\}/g, name.toLowerCase().replace(/\\s+/g, '-'));\r\n result = result.replace(\r\n /\\{\\{Name\\}\\}/g,\r\n name.replace(/(?:^|[-_\\s]+)([a-z])/g, (_, c) => c.toUpperCase()),\r\n );\r\n for (const [k, v] of Object.entries(vars)) {\r\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), v);\r\n }\r\n return result;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/scaffold.ts"],"names":["path2"],"mappings":"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","file":"scaffold.js","sourcesContent":["import { expectDefined } from '@wrongstack/core';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(expectDefined(lines[i]), `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(expectDefined(lines[k]));\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { atomicWrite } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface ScaffoldInput {\r\n template: string;\r\n name: string;\r\n cwd?: string | undefined;\r\n vars?: Record<string, string>;\r\n dry_run?: boolean | undefined;\r\n}\r\n\r\ninterface ScaffoldOutput {\r\n template: string;\r\n name: string;\r\n files_created: number;\r\n files: string[];\r\n dry_run: boolean;\r\n output: string;\r\n}\r\n\r\nconst BUILT_IN_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = {\r\n 'npm-package': {\r\n description: 'Basic npm package with ESM',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n main: './dist/index.js',\r\n scripts: { build: 'tsc', test: 'vitest run' },\r\n devDependencies: { typescript: '^5.0.0' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'tsconfig.json': JSON.stringify(\r\n {\r\n compilerOptions: { target: 'ES2022', module: 'ESNext', strict: true },\r\n include: ['src'],\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `export function hello() {\\n return 'Hello from {{name}}';\\n}\\n`,\r\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`,\r\n },\r\n },\r\n 'cli-tool': {\r\n description: 'CLI tool with argparse',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n bin: { '{{name}}': './src/index.js' },\r\n scripts: { build: 'tsc', start: 'node dist/index.js' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `#!/usr/bin/env node\\n\\nasync function main() {\\n console.log('Hello from {{name}}');\\n}\\n\\nmain();\\n`,\r\n },\r\n },\r\n 'react-component': {\r\n description: 'React component with TypeScript',\r\n files: {\r\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`,\r\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`,\r\n },\r\n },\r\n};\r\n\r\nexport const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput> = {\r\n name: 'scaffold',\r\n category: 'Project',\r\n description:\r\n 'Generate new files and folder structures from built-in templates or custom definitions. ' +\r\n 'This is the recommended way to bootstrap new packages, components, or modules instead of creating files one by one with `write`.',\r\n usageHint:\r\n 'PREFERRED FOR SCAFFOLDING:\\n\\n' +\r\n '- Use built-in templates when they match your needs (e.g. react-component, npm-package).\\n' +\r\n '- Supports `dry_run` so you can preview exactly what will be created.\\n' +\r\n '- Has the powerful `fs.write.outside-project` capability — review paths carefully.\\n' +\r\n 'Much cleaner and safer than manually writing multiple files.',\r\n permission: 'confirm',\r\n mutating: true,\r\n capabilities: ['fs.write.outside-project', 'fs.write'],\r\n timeoutMs: 30_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n template: {\r\n type: 'string',\r\n description:\r\n 'Template name (npm-package, cli-tool, react-component) or path to template directory',\r\n },\r\n name: {\r\n type: 'string',\r\n description: 'Project/component name (used in generated files)',\r\n },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n vars: {\r\n type: 'object',\r\n additionalProperties: { type: 'string' },\r\n description: 'Template variables for custom templates',\r\n },\r\n dry_run: {\r\n type: 'boolean',\r\n description: 'Preview generated files without creating (default: false)',\r\n },\r\n },\r\n required: ['template', 'name'],\r\n },\r\n async execute(input, ctx) {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const name = input.name;\r\n const vars = { name, ...input.vars };\r\n\r\n const builtIn = BUILT_IN_TEMPLATES[input.template];\r\n if (builtIn) {\r\n return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);\r\n }\r\n\r\n return {\r\n template: input.template,\r\n name,\r\n files_created: 0,\r\n files: [],\r\n dry_run: input.dry_run ?? false,\r\n output: `Template \"${input.template}\" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(', ')}`,\r\n };\r\n },\r\n};\r\n\r\nasync function handleBuiltIn(\r\n name: string,\r\n templateFiles: Record<string, string>,\r\n cwd: string,\r\n ctx: Parameters<Tool['execute']>[1],\r\n dryRun: boolean,\r\n vars: Record<string, string>,\r\n): Promise<ScaffoldOutput> {\r\n const files: string[] = [];\r\n let filesCreated = 0;\r\n\r\n for (const [filePath, content] of Object.entries(templateFiles)) {\r\n const resolvedPath = substituteVars(filePath, name, vars);\r\n const joinedPath = path.join(cwd, resolvedPath);\r\n // Ensure generated files cannot escape the project root via template variable injection (e.g. name containing \"../\")\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(joinedPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`scaffold: generated path \"${resolvedPath}\" would escape project root`);\r\n }\r\n const fullPath = target;\r\n\r\n if (!dryRun) {\r\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\r\n // atomicWrite: scaffolded files land in the user's tracked tree.\r\n // A torn write here would commit a corrupt file to their repo.\r\n await atomicWrite(fullPath, substituteVars(content, name, vars));\r\n }\r\n files.push(resolvedPath);\r\n filesCreated++;\r\n }\r\n\r\n return {\r\n template: 'built-in',\r\n name,\r\n files_created: filesCreated,\r\n files,\r\n dry_run: dryRun,\r\n output: dryRun\r\n ? `Would create ${filesCreated} files: ${files.join(', ')}`\r\n : `Created ${filesCreated} files: ${files.join(', ')}`,\r\n };\r\n}\r\n\r\nfunction substituteVars(content: string, name: string, vars: Record<string, string>): string {\r\n let result = content;\r\n result = result.replace(/\\{\\{name\\}\\}/g, name.toLowerCase().replace(/\\s+/g, '-'));\r\n result = result.replace(\r\n /\\{\\{Name\\}\\}/g,\r\n name.replace(/(?:^|[-_\\s]+)([a-z])/g, (_, c) => c.toUpperCase()),\r\n );\r\n for (const [k, v] of Object.entries(vars)) {\r\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), v);\r\n }\r\n return result;\r\n}\r\n"]}
package/dist/search.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { expectDefined } from '@wrongstack/core';
1
2
  import * as dns from 'node:dns/promises';
2
3
  import * as net from 'node:net';
3
4
  import { Agent } from 'undici';
4
5
 
5
- // src/fetch.ts
6
+ // src/search.ts
6
7
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
7
8
  function guardedLookup(hostname, options, callback) {
8
9
  dns.lookup(hostname, { all: true }).then((records) => {
@@ -178,12 +179,6 @@ function expandIPv6(addr) {
178
179
  }
179
180
 
180
181
  // src/search.ts
181
- function expectDefined(value) {
182
- if (value === null || value === void 0) {
183
- throw new Error("Expected value to be defined");
184
- }
185
- return value;
186
- }
187
182
  var DEFAULT_NUM = 10;
188
183
  var MAX_RESULTS = 50;
189
184
  var TIMEOUT_MS = 15e3;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":[],"mappings":"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","file":"search.js","sourcesContent":["import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { Agent } from 'undici';\nimport { truncateMiddle } from './_util.js';\n\ninterface FetchInput {\n url: string;\n format?: 'markdown' | 'text' | 'raw' | undefined;\n}\n\ninterface FetchOutput {\n content: string;\n status: number;\n content_type: string;\n url: string;\n}\n\nconst MAX_BYTES = 131_072;\nconst TIMEOUT_MS = 20_000;\n\nconst ALLOW_PRIVATE = process.env['WRONGSTACK_FETCH_ALLOW_PRIVATE'] === '1';\n\n/**\n * Combine multiple AbortSignals into one. Prefers the native `AbortSignal.any`\n * when available, falling back to a manual controller for older runtimes that\n * lack it. The combined signal aborts as soon as any input signal aborts.\n */\nfunction combineSignals(signals: AbortSignal[]): AbortSignal {\n const anyFn = (AbortSignal as { any?: (s: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyFn === 'function') return anyFn(signals);\n\n const ctrl = new AbortController();\n for (const sig of signals) {\n if (sig.aborted) {\n ctrl.abort(sig.reason);\n return ctrl.signal;\n }\n sig.addEventListener('abort', () => ctrl.abort(sig.reason), { once: true });\n }\n return ctrl.signal;\n}\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address?: string | Array<{ address: string | undefined; family: number }>,\n family?: number | undefined,\n) => void;\n\n/**\n * DNS lookup used by the undici dispatcher below. It performs the SINGLE name\n * resolution that the TCP connection actually uses, and rejects if any\n * resolved address is private/loopback/link-local. Because the connection\n * reuses exactly this result, there is no DNS-rebinding TOCTOU window between\n * the security check and the connect — closing the gap the old code documented\n * (validate with one dns.lookup, then let fetch re-resolve independently).\n * TLS still validates the certificate against the hostname (SNI is set by\n * undici from the URL), so pinning the IP does not weaken cert checking.\n */\nfunction guardedLookup(\n hostname: string,\n options: { all?: boolean | undefined; family?: number | undefined },\n callback: LookupCallback,\n): void {\n dns\n .lookup(hostname, { all: true })\n .then((records) => {\n const family = options?.family;\n const byFamily =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;\n const list = byFamily.length > 0 ? byFamily : records;\n if (!ALLOW_PRIVATE) {\n for (const r of list) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n callback(\n Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {\n code: 'EAI_FAIL',\n }),\n );\n return;\n }\n }\n }\n if (options?.all) {\n callback(\n null,\n list.map((r) => ({ address: r.address, family: r.family })),\n );\n return;\n }\n const first = list[0];\n if (!first) {\n callback(\n Object.assign(new Error(`fetch: no address for ${hostname}`), { code: 'ENOTFOUND' }),\n );\n return;\n }\n callback(null, first.address, first.family);\n })\n .catch((err) => callback(err as NodeJS.ErrnoException));\n}\n\n// Reused across requests; guardedLookup re-validates on every new connection,\n// so connection pooling is safe. Literal-IP targets bypass lookup entirely and\n// are caught by assertNotPrivate's pre-check instead.\n// Destroyed on process exit so long-running processes (eternal autonomy,\n// MCP server mode) don't let the connection pool grow unboundedly.\nlet pinnedAgent: Agent | undefined;\nfunction getPinnedDispatcher(): Agent {\n if (!pinnedAgent) {\n pinnedAgent = new Agent({ connect: { lookup: guardedLookup as never } });\n }\n return pinnedAgent;\n}\n// Clean up the global dispatcher on exit — undici Agents maintain connection\n// pools and DNS caches that should be torn down in long-running processes.\nprocess.on('beforeExit', () => {\n pinnedAgent?.destroy();\n pinnedAgent = undefined;\n});\n\n/**\n * SSRF-guarded fetch with manual, per-hop-revalidated redirects, exported so\n * other builtin tools (e.g. `search`) get the same protections instead of a\n * weaker `redirect: 'follow'`. Every hop is re-checked against private/loopback\n * ranges and the connection is pinned to the validated IP via the undici\n * dispatcher (no DNS-rebinding TOCTOU). `headers` defaults to the plain `fetch`\n * tool's; callers may override (e.g. a browser User-Agent for search engines).\n */\nexport async function guardedFetch(\n url: string,\n maxRedirects: number,\n signal: AbortSignal,\n headers: Record<string, string> = {\n 'user-agent': 'WrongStack/1.0 (+https://wrongstack.com)',\n accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1',\n },\n): Promise<Response> {\n let redirectCount = 0;\n let currentUrl = url;\n for (;;) {\n // Re-validate every hop. A public host can 302 to 169.254.169.254 (cloud metadata),\n // or DNS can rebind between hops; checking only the initial URL is insufficient.\n const parsed = new URL(currentUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new Error(`fetch: redirect to unsupported protocol \"${parsed.protocol}\"`);\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: redirect to http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated —\n // no independent re-resolution, so DNS rebinding can't swap in a private\n // address between check and connect. `dispatcher` is a runtime option of\n // Node's undici-backed global fetch but isn't in lib.dom's RequestInit, and\n // our undici Agent's type differs from the @types/node copy — hence the\n // cast. (Verified: global fetch invokes the Agent's custom lookup.)\n const init = {\n redirect: 'manual' as const,\n signal,\n headers,\n dispatcher: getPinnedDispatcher(),\n };\n const res = await fetch(currentUrl, init as unknown as RequestInit);\n if (res.status < 300 || res.status > 399) {\n return res;\n }\n redirectCount++;\n if (redirectCount > maxRedirects) {\n throw new Error(`fetch: exceeded ${maxRedirects} redirects`);\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new Error('fetch: redirect status with no location header');\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport const fetchTool: Tool<FetchInput, FetchOutput> = {\n name: 'fetch',\n category: 'Network',\n description:\n 'Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. ' +\n 'This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).',\n usageHint:\n 'Use this when you need external information (documentation, API responses, web pages, etc.).\\n\\n' +\n 'Security notes:\\n' +\n '- Only HTTPS is allowed by default.\\n' +\n '- Internal/private networks are blocked unless explicitly enabled via environment variable.\\n' +\n '- Redirects are followed but re-validated at each hop.\\n' +\n '- Output is capped (128KB by default) to avoid flooding context.\\n' +\n 'Prefer this over raw `bash curl` or `bash wget`.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n // Trust rules for fetch match on the literal URL — declare it explicitly\n // so a user can trust `https://api.example.com/*` without accidentally\n // matching that pattern on any other tool that happens to have a `url`\n // input field.\n subjectKey: 'url',\n timeoutMs: TIMEOUT_MS,\n maxOutputBytes: MAX_BYTES,\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'The target URL (must use https://).',\n },\n format: {\n type: 'string',\n enum: ['markdown', 'text', 'raw'],\n description: 'Output format. \"markdown\" is recommended for HTML pages.',\n },\n },\n required: ['url'],\n },\n async execute(input, ctx, opts) {\n let final: FetchOutput | undefined;\n const executeStream = fetchTool.executeStream;\n if (!executeStream) throw new Error('fetchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('fetch: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) throw new Error('fetch: url is required');\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new Error(`fetch: unsupported protocol \"${u.protocol}\"`);\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(u.hostname);\n\n yield { type: 'log', text: `GET ${input.url}` };\n\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(new Error('fetch timeout')), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n const res = await guardedFetch(input.url, 5, combined);\n\n const ct = res.headers.get('content-type') ?? 'application/octet-stream';\n if (/^image\\/|^audio\\/|^video\\/|application\\/octet-stream/.test(ct)) {\n throw new Error(`fetch: refusing to read binary content-type \"${ct}\"`);\n }\n\n yield {\n type: 'log',\n text: `HTTP ${res.status} ${ct}`,\n data: { status: res.status, contentType: ct },\n };\n\n const reader = res.body?.getReader();\n let received = 0;\n const chunks: Uint8Array[] = [];\n let pendingBytes = 0;\n const FLUSH_AT = 4 * 1024;\n if (reader) {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (!value) continue;\n received += value.byteLength;\n pendingBytes += value.byteLength;\n chunks.push(value);\n if (pendingBytes >= FLUSH_AT) {\n // Snapshot recent bytes for the partial_output. Keep it cheap —\n // don't try to decode UTF-8 boundaries; the TUI just needs a\n // \"things are happening\" signal.\n const recent = Buffer.from(value).toString('utf8');\n yield {\n type: 'partial_output',\n text: recent,\n data: { received },\n };\n pendingBytes = 0;\n }\n if (received > MAX_BYTES) break;\n }\n }\n const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');\n\n const format = input.format ?? (ct.includes('text/html') ? 'markdown' : 'text');\n let content: string;\n if (format === 'raw') content = text;\n else if (format === 'markdown' && ct.includes('text/html')) content = htmlToMarkdown(text);\n else if (ct.includes('application/json')) content = prettyJson(text);\n else content = text;\n\n yield {\n type: 'final',\n output: {\n content: truncateMiddle(content, MAX_BYTES),\n status: res.status,\n content_type: ct,\n url: res.url,\n },\n };\n } finally {\n clearTimeout(timer);\n }\n },\n};\n\nasync function assertNotPrivate(hostname: string): Promise<void> {\n if (ALLOW_PRIVATE) return;\n\n const host =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n\n if (host === 'localhost' || host.endsWith('.localhost')) {\n throw new Error('fetch: blocked localhost target');\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else {\n // Hostname — pre-flight check: resolve and reject if any record is private,\n // so we fail fast with a clear error before opening a socket. The\n // authoritative anti-rebinding control is guardedLookup on the pinned\n // undici dispatcher (see getPinnedDispatcher): it performs the single\n // resolution the connection actually uses, so there is no TOCTOU between\n // this check and the connect. Each redirect target is re-checked too.\n try {\n const records = await dns.lookup(host, { all: true });\n for (const r of records) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n throw new Error(`fetch: resolved to private address ${r.address}`);\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\n // DNS failure — let fetch handle it\n }\n }\n}\n\nfunction isPrivateIPv4(addr: string): boolean {\n // net.isIP rejects octal/hex/decimal forms, so when isIP(addr) === 4 we\n // know it's canonical dotted-quad and safe to parse this way.\n const parts = addr.split('.').map((p) => Number.parseInt(p, 10));\n if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {\n return true; // defensive\n }\n const [a, b, c] = parts as [number, number, number, number];\n if (a === 0) return true; // 0.0.0.0/8\n if (a === 10) return true; // 10.0.0.0/8\n if (a === 127) return true; // 127.0.0.0/8 loopback\n if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local + AWS/GCE/Azure IMDS\n if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12\n if (a === 192 && b === 168) return true; // 192.168.0.0/16\n if (a === 192 && b === 0 && c === 0) return true; // 192.0.0.0/24 reserved\n if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10 CGNAT\n if (a >= 224) return true; // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved\n return false;\n}\n\nfunction isPrivateIPv6(addr: string): boolean {\n const lower = addr.toLowerCase();\n if (lower === '::' || lower === '::1') return true;\n // Convert to 8-group canonical form (16 hex words) so range checks\n // don't have to handle every shortening notation. Returns null on\n // anything we can't normalize; we conservatively return true in that\n // case so a parser surprise blocks rather than leaks.\n const groups = expandIPv6(lower);\n if (!groups) return true;\n // IPv4-mapped: ::ffff:0:0/96 → groups[0..5] all 0, groups[6..7] hold the\n // embedded IPv4 as two 16-bit words. Node URL normalizes the dotted form\n // to this representation (e.g. ::ffff:127.0.0.1 → ::ffff:7f00:1).\n if (\n groups[0] === 0 &&\n groups[1] === 0 &&\n groups[2] === 0 &&\n groups[3] === 0 &&\n groups[4] === 0 &&\n groups[5] === 0xffff\n ) {\n const a = (groups[6] ?? 0) >> 8;\n const b = (groups[6] ?? 0) & 0xff;\n const c = (groups[7] ?? 0) >> 8;\n const d = (groups[7] ?? 0) & 0xff;\n return isPrivateIPv4(`${a}.${b}.${c}.${d}`);\n }\n const high = groups[0] ?? 0;\n if ((high & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local (fc..fd)\n if ((high & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n if ((high & 0xff00) === 0xff00) return true; // ff00::/8 multicast\n return false;\n}\n\n/**\n * Expand an IPv6 string into exactly 8 16-bit numbers. Handles `::`\n * compression. Returns null on malformed input — caller should treat that\n * as \"block\".\n */\nfunction expandIPv6(addr: string): number[] | null {\n const parts = addr.split('::');\n if (parts.length > 2) return null;\n const parseGroups = (s: string): number[] | null => {\n if (s === '') return [];\n const out: number[] = [];\n for (const g of s.split(':')) {\n if (g.length === 0 || g.length > 4) return null;\n const n = Number.parseInt(g, 16);\n if (Number.isNaN(n) || n < 0 || n > 0xffff) return null;\n out.push(n);\n }\n return out;\n };\n if (parts.length === 1) {\n const groups = parseGroups(parts[0] ?? '');\n if (!groups || groups.length !== 8) return null;\n return groups;\n }\n const head = parseGroups(parts[0] ?? '');\n const tail = parseGroups(parts[1] ?? '');\n if (!head || !tail) return null;\n const fill = 8 - head.length - tail.length;\n if (fill < 0) return null;\n return [...head, ...new Array<number>(fill).fill(0), ...tail];\n}\n\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n\n/**\n * Simplified regex-based HTML-to-Markdown converter. Handles the common\n * case (headings, bold/italic, links, code blocks, lists) adequately for\n * LLM context consumption. Known limitations: nested tags, attributes\n * containing `>`, malformed HTML, or unusual markup may produce incorrect\n * output. This is acceptable — the result is fed to LLM context, not\n * rendered in a browser. For strict correctness, replace with a dedicated\n * converter library (e.g. turndown, marked).\n */\nfunction htmlToMarkdown(html: string): string {\n let s = html;\n // Strip scripts/styles\n s = s.replace(/<script[\\s\\S]*?<\\/script>/gi, '');\n s = s.replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n s = s.replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, '');\n // Headings\n s = s.replace(/<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_m, n, c) => {\n return '\\n' + '#'.repeat(Number(n)) + ' ' + stripTags(c).trim() + '\\n';\n });\n // Bold / italic\n s = s.replace(/<(strong|b)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '**$2**');\n s = s.replace(/<(em|i)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '*$2*');\n // Links — only emit markdown links for safe protocols\n // Explicitly reject dangerous schemes (javascript:, data:, vbscript:) to prevent XSS\n s = s.replace(/<a [^>]*href=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, (_m, href, text) => {\n const safe =\n /^(https?|ftps?):\\/\\//i.test(href) &&\n !/^(javascript|data|vbscript):/i.test(href);\n return safe ? `[${text}](${href})` : text;\n });\n // Code\n s = s.replace(/<pre[^>]*>([\\s\\S]*?)<\\/pre>/gi, (_m, c) => '\\n```\\n' + stripTags(c) + '\\n```\\n');\n s = s.replace(/<code[^>]*>([\\s\\S]*?)<\\/code>/gi, '`$1`');\n // Lists\n s = s.replace(/<li[^>]*>([\\s\\S]*?)<\\/li>/gi, '- $1\\n');\n // Breaks / paragraphs\n s = s.replace(/<br\\s*\\/?>/gi, '\\n');\n s = s.replace(/<\\/p>/gi, '\\n\\n');\n // Strip remaining tags\n s = stripTags(s);\n // Decode common entities\n s = s\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&nbsp;/g, ' ');\n // Collapse whitespace\n return s.replace(/\\n{3,}/g, '\\n\\n').trim();\n}\n\nfunction stripTags(s: string): string {\n return s.replace(/<[^>]+>/g, '');\n}\n","import type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\n\n\n\nfunction expectDefined<T>(value: T | null | undefined): T {\n if (value === null || value === undefined) {\n throw new Error('Expected value to be defined');\n }\n return value;\n}\n\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\n\nexport const searchTool: Tool<SearchInput, SearchOutput> = {\n name: 'search',\n category: 'Search',\n description:\n 'Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase.',\n usageHint:\n 'Good for: API documentation, error messages, library usage examples, current best practices.\\n\\n' +\n '- Prefer specific queries over very broad ones.\\n' +\n '- Results go through the guarded fetch system (same protections as the `fetch` tool).\\n' +\n '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n num_results: {\n type: 'integer',\n description: 'Number of results (1-50, default 10)',\n minimum: 1,\n maximum: MAX_RESULTS,\n },\n source: {\n type: 'string',\n enum: ['duckduckgo', 'google', 'bing'],\n description: 'Search engine to use (default: duckduckgo)',\n },\n },\n required: ['query'],\n },\n async execute(input, ctx, opts) {\n let final: SearchOutput | undefined;\n const executeStream = searchTool.executeStream;\n if (!executeStream) throw new Error('searchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('search: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<SearchOutput>> {\n if (!input?.query) throw new Error('search: query is required');\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query },\n };\n\n let output: SearchOutput;\n switch (source) {\n case 'duckduckgo':\n output = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n output = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n output = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new Error(`search: unknown source \"${source}\"`);\n }\n\n yield {\n type: 'partial_output',\n text: `${output.results.length} results from ${output.source}`,\n data: { count: output.results.length },\n };\n yield { type: 'final', output };\n },\n};\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\n const encoded = encodeURIComponent(query);\n const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;\n\n const results = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .then((html) => parseDuckDuckGo(html, num))\n .catch(() => [{ title: 'Search unavailable', url: '', snippet: 'Could not reach DuckDuckGo' }]);\n\n return {\n query,\n results,\n source: 'duckduckgo',\n truncated: results.length >= num,\n };\n}\n\nfunction takeFrom<T>(iter: Iterable<T>, max: number): T[] {\n const out: T[] = [];\n for (const item of iter) {\n if (out.length >= max) break;\n out.push(item);\n }\n return out;\n}\n\nfunction parseDuckDuckGo(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const snippetRegex = /<a class=\"result-link\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>/gi;\n const snippet2Regex = /<a class=\"result-snippet\"[^>]*>([^<]+)<\\/a>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n results.push({\n title: entry?.title ?? '',\n url: entry?.url ?? '',\n snippet: snippetMatches[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function googleSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.google.com/search?q=${encoded}&hl=en`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n const results = parseGoogleResults(html, num);\n\n return {\n query,\n results,\n source: 'google',\n truncated: results.length >= num,\n };\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const titleRegex = /<h3[^>]*class=\"[^\"]*DKV84\"[^>]*>([^<]+)<\\/h3>/gi;\n const urlRegex = /<cite[^>]*>([^<]+)<\\/cite>/gi;\n const snippetRegex = /<span[^>]*class=\"[^\"]*aXCZ0b[^>]*>([^<]+)<\\/span>/gi;\n\n const titles = takeFrom(\n [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n const urls = takeFrom(\n [...html.matchAll(urlRegex)]\n .filter((m) => m[1])\n .map((m) => stripTags(expectDefined(m[1])).replace(/^\\*(https?:\\/\\/[^\\s]+).*$/, '$1'))\n .filter((u) => u.startsWith('http')),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < Math.min(titles.length, num); i++) {\n results.push({\n title: titles[i] ?? '',\n url: urls[i] ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchOutput> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.bing.com/search?q=${encoded}`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n const results = parseBingResults(html, num);\n\n return {\n query,\n results,\n source: 'bing',\n truncated: results.length >= num,\n };\n}\n\nfunction parseBingResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const titleRegex = /<h2[^>]*>\\s*<a[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>\\s*<\\/h2>/gi;\n const snippetRegex = /<p[^>]*class=\"[^\"]*b_paractl[^\"]*\"[^>]*>([^<]+)<\\/p>/gi;\n\n const entries = takeFrom(\n [...html.matchAll(titleRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < entries.length; i++) {\n results.push({\n title: entries[i]?.title ?? '',\n url: entries[i]?.url ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function fetchWithTimeout(\n url: string,\n signal: AbortSignal,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const fetchSignal = anySignal(signal, controller.signal);\n try {\n // F-05: route through the SSRF-guarded fetch (private-IP blocking, HTTPS,\n // DNS-pinned dispatcher, per-hop redirect re-validation) instead of a bare\n // `fetch` with `redirect: 'follow'`. Search hosts are fixed/trusted, but\n // this closes the residual \"engine 30x → internal address\" redirect risk.\n const res = await guardedFetch(url, 5, fetchSignal, {\n 'user-agent':\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n });\n clearTimeout(timer);\n return res;\n } catch (e) {\n clearTimeout(timer);\n throw e;\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const s of signals) {\n if (s.aborted) {\n controller.abort();\n break;\n }\n s.addEventListener('abort', () => controller.abort());\n }\n return controller.signal;\n}\n\nfunction stripTags(html: string): string {\n return html\n .replace(/<[^>]+>/g, '')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .trim();\n}\n"]}
1
+ {"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":[],"mappings":"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","file":"search.js","sourcesContent":["import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { Agent } from 'undici';\nimport { truncateMiddle } from './_util.js';\n\ninterface FetchInput {\n url: string;\n format?: 'markdown' | 'text' | 'raw' | undefined;\n}\n\ninterface FetchOutput {\n content: string;\n status: number;\n content_type: string;\n url: string;\n}\n\nconst MAX_BYTES = 131_072;\nconst TIMEOUT_MS = 20_000;\n\nconst ALLOW_PRIVATE = process.env['WRONGSTACK_FETCH_ALLOW_PRIVATE'] === '1';\n\n/**\n * Combine multiple AbortSignals into one. Prefers the native `AbortSignal.any`\n * when available, falling back to a manual controller for older runtimes that\n * lack it. The combined signal aborts as soon as any input signal aborts.\n */\nfunction combineSignals(signals: AbortSignal[]): AbortSignal {\n const anyFn = (AbortSignal as { any?: (s: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyFn === 'function') return anyFn(signals);\n\n const ctrl = new AbortController();\n for (const sig of signals) {\n if (sig.aborted) {\n ctrl.abort(sig.reason);\n return ctrl.signal;\n }\n sig.addEventListener('abort', () => ctrl.abort(sig.reason), { once: true });\n }\n return ctrl.signal;\n}\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address?: string | Array<{ address: string | undefined; family: number }>,\n family?: number | undefined,\n) => void;\n\n/**\n * DNS lookup used by the undici dispatcher below. It performs the SINGLE name\n * resolution that the TCP connection actually uses, and rejects if any\n * resolved address is private/loopback/link-local. Because the connection\n * reuses exactly this result, there is no DNS-rebinding TOCTOU window between\n * the security check and the connect — closing the gap the old code documented\n * (validate with one dns.lookup, then let fetch re-resolve independently).\n * TLS still validates the certificate against the hostname (SNI is set by\n * undici from the URL), so pinning the IP does not weaken cert checking.\n */\nfunction guardedLookup(\n hostname: string,\n options: { all?: boolean | undefined; family?: number | undefined },\n callback: LookupCallback,\n): void {\n dns\n .lookup(hostname, { all: true })\n .then((records) => {\n const family = options?.family;\n const byFamily =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;\n const list = byFamily.length > 0 ? byFamily : records;\n if (!ALLOW_PRIVATE) {\n for (const r of list) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n callback(\n Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {\n code: 'EAI_FAIL',\n }),\n );\n return;\n }\n }\n }\n if (options?.all) {\n callback(\n null,\n list.map((r) => ({ address: r.address, family: r.family })),\n );\n return;\n }\n const first = list[0];\n if (!first) {\n callback(\n Object.assign(new Error(`fetch: no address for ${hostname}`), { code: 'ENOTFOUND' }),\n );\n return;\n }\n callback(null, first.address, first.family);\n })\n .catch((err) => callback(err as NodeJS.ErrnoException));\n}\n\n// Reused across requests; guardedLookup re-validates on every new connection,\n// so connection pooling is safe. Literal-IP targets bypass lookup entirely and\n// are caught by assertNotPrivate's pre-check instead.\n// Destroyed on process exit so long-running processes (eternal autonomy,\n// MCP server mode) don't let the connection pool grow unboundedly.\nlet pinnedAgent: Agent | undefined;\nfunction getPinnedDispatcher(): Agent {\n if (!pinnedAgent) {\n pinnedAgent = new Agent({ connect: { lookup: guardedLookup as never } });\n }\n return pinnedAgent;\n}\n// Clean up the global dispatcher on exit — undici Agents maintain connection\n// pools and DNS caches that should be torn down in long-running processes.\nprocess.on('beforeExit', () => {\n pinnedAgent?.destroy();\n pinnedAgent = undefined;\n});\n\n/**\n * SSRF-guarded fetch with manual, per-hop-revalidated redirects, exported so\n * other builtin tools (e.g. `search`) get the same protections instead of a\n * weaker `redirect: 'follow'`. Every hop is re-checked against private/loopback\n * ranges and the connection is pinned to the validated IP via the undici\n * dispatcher (no DNS-rebinding TOCTOU). `headers` defaults to the plain `fetch`\n * tool's; callers may override (e.g. a browser User-Agent for search engines).\n */\nexport async function guardedFetch(\n url: string,\n maxRedirects: number,\n signal: AbortSignal,\n headers: Record<string, string> = {\n 'user-agent': 'WrongStack/1.0 (+https://wrongstack.com)',\n accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1',\n },\n): Promise<Response> {\n let redirectCount = 0;\n let currentUrl = url;\n for (;;) {\n // Re-validate every hop. A public host can 302 to 169.254.169.254 (cloud metadata),\n // or DNS can rebind between hops; checking only the initial URL is insufficient.\n const parsed = new URL(currentUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new Error(`fetch: redirect to unsupported protocol \"${parsed.protocol}\"`);\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: redirect to http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated —\n // no independent re-resolution, so DNS rebinding can't swap in a private\n // address between check and connect. `dispatcher` is a runtime option of\n // Node's undici-backed global fetch but isn't in lib.dom's RequestInit, and\n // our undici Agent's type differs from the @types/node copy — hence the\n // cast. (Verified: global fetch invokes the Agent's custom lookup.)\n const init = {\n redirect: 'manual' as const,\n signal,\n headers,\n dispatcher: getPinnedDispatcher(),\n };\n const res = await fetch(currentUrl, init as unknown as RequestInit);\n if (res.status < 300 || res.status > 399) {\n return res;\n }\n redirectCount++;\n if (redirectCount > maxRedirects) {\n throw new Error(`fetch: exceeded ${maxRedirects} redirects`);\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new Error('fetch: redirect status with no location header');\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport const fetchTool: Tool<FetchInput, FetchOutput> = {\n name: 'fetch',\n category: 'Network',\n description:\n 'Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. ' +\n 'This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).',\n usageHint:\n 'Use this when you need external information (documentation, API responses, web pages, etc.).\\n\\n' +\n 'Security notes:\\n' +\n '- Only HTTPS is allowed by default.\\n' +\n '- Internal/private networks are blocked unless explicitly enabled via environment variable.\\n' +\n '- Redirects are followed but re-validated at each hop.\\n' +\n '- Output is capped (128KB by default) to avoid flooding context.\\n' +\n 'Prefer this over raw `bash curl` or `bash wget`.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n // Trust rules for fetch match on the literal URL — declare it explicitly\n // so a user can trust `https://api.example.com/*` without accidentally\n // matching that pattern on any other tool that happens to have a `url`\n // input field.\n subjectKey: 'url',\n timeoutMs: TIMEOUT_MS,\n maxOutputBytes: MAX_BYTES,\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'The target URL (must use https://).',\n },\n format: {\n type: 'string',\n enum: ['markdown', 'text', 'raw'],\n description: 'Output format. \"markdown\" is recommended for HTML pages.',\n },\n },\n required: ['url'],\n },\n async execute(input, ctx, opts) {\n let final: FetchOutput | undefined;\n const executeStream = fetchTool.executeStream;\n if (!executeStream) throw new Error('fetchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('fetch: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) throw new Error('fetch: url is required');\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new Error(`fetch: unsupported protocol \"${u.protocol}\"`);\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(u.hostname);\n\n yield { type: 'log', text: `GET ${input.url}` };\n\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(new Error('fetch timeout')), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n const res = await guardedFetch(input.url, 5, combined);\n\n const ct = res.headers.get('content-type') ?? 'application/octet-stream';\n if (/^image\\/|^audio\\/|^video\\/|application\\/octet-stream/.test(ct)) {\n throw new Error(`fetch: refusing to read binary content-type \"${ct}\"`);\n }\n\n yield {\n type: 'log',\n text: `HTTP ${res.status} ${ct}`,\n data: { status: res.status, contentType: ct },\n };\n\n const reader = res.body?.getReader();\n let received = 0;\n const chunks: Uint8Array[] = [];\n let pendingBytes = 0;\n const FLUSH_AT = 4 * 1024;\n if (reader) {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (!value) continue;\n received += value.byteLength;\n pendingBytes += value.byteLength;\n chunks.push(value);\n if (pendingBytes >= FLUSH_AT) {\n // Snapshot recent bytes for the partial_output. Keep it cheap —\n // don't try to decode UTF-8 boundaries; the TUI just needs a\n // \"things are happening\" signal.\n const recent = Buffer.from(value).toString('utf8');\n yield {\n type: 'partial_output',\n text: recent,\n data: { received },\n };\n pendingBytes = 0;\n }\n if (received > MAX_BYTES) break;\n }\n }\n const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');\n\n const format = input.format ?? (ct.includes('text/html') ? 'markdown' : 'text');\n let content: string;\n if (format === 'raw') content = text;\n else if (format === 'markdown' && ct.includes('text/html')) content = htmlToMarkdown(text);\n else if (ct.includes('application/json')) content = prettyJson(text);\n else content = text;\n\n yield {\n type: 'final',\n output: {\n content: truncateMiddle(content, MAX_BYTES),\n status: res.status,\n content_type: ct,\n url: res.url,\n },\n };\n } finally {\n clearTimeout(timer);\n }\n },\n};\n\nasync function assertNotPrivate(hostname: string): Promise<void> {\n if (ALLOW_PRIVATE) return;\n\n const host =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n\n if (host === 'localhost' || host.endsWith('.localhost')) {\n throw new Error('fetch: blocked localhost target');\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else {\n // Hostname — pre-flight check: resolve and reject if any record is private,\n // so we fail fast with a clear error before opening a socket. The\n // authoritative anti-rebinding control is guardedLookup on the pinned\n // undici dispatcher (see getPinnedDispatcher): it performs the single\n // resolution the connection actually uses, so there is no TOCTOU between\n // this check and the connect. Each redirect target is re-checked too.\n try {\n const records = await dns.lookup(host, { all: true });\n for (const r of records) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n throw new Error(`fetch: resolved to private address ${r.address}`);\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\n // DNS failure — let fetch handle it\n }\n }\n}\n\nfunction isPrivateIPv4(addr: string): boolean {\n // net.isIP rejects octal/hex/decimal forms, so when isIP(addr) === 4 we\n // know it's canonical dotted-quad and safe to parse this way.\n const parts = addr.split('.').map((p) => Number.parseInt(p, 10));\n if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {\n return true; // defensive\n }\n const [a, b, c] = parts as [number, number, number, number];\n if (a === 0) return true; // 0.0.0.0/8\n if (a === 10) return true; // 10.0.0.0/8\n if (a === 127) return true; // 127.0.0.0/8 loopback\n if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local + AWS/GCE/Azure IMDS\n if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12\n if (a === 192 && b === 168) return true; // 192.168.0.0/16\n if (a === 192 && b === 0 && c === 0) return true; // 192.0.0.0/24 reserved\n if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10 CGNAT\n if (a >= 224) return true; // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved\n return false;\n}\n\nfunction isPrivateIPv6(addr: string): boolean {\n const lower = addr.toLowerCase();\n if (lower === '::' || lower === '::1') return true;\n // Convert to 8-group canonical form (16 hex words) so range checks\n // don't have to handle every shortening notation. Returns null on\n // anything we can't normalize; we conservatively return true in that\n // case so a parser surprise blocks rather than leaks.\n const groups = expandIPv6(lower);\n if (!groups) return true;\n // IPv4-mapped: ::ffff:0:0/96 → groups[0..5] all 0, groups[6..7] hold the\n // embedded IPv4 as two 16-bit words. Node URL normalizes the dotted form\n // to this representation (e.g. ::ffff:127.0.0.1 → ::ffff:7f00:1).\n if (\n groups[0] === 0 &&\n groups[1] === 0 &&\n groups[2] === 0 &&\n groups[3] === 0 &&\n groups[4] === 0 &&\n groups[5] === 0xffff\n ) {\n const a = (groups[6] ?? 0) >> 8;\n const b = (groups[6] ?? 0) & 0xff;\n const c = (groups[7] ?? 0) >> 8;\n const d = (groups[7] ?? 0) & 0xff;\n return isPrivateIPv4(`${a}.${b}.${c}.${d}`);\n }\n const high = groups[0] ?? 0;\n if ((high & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local (fc..fd)\n if ((high & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n if ((high & 0xff00) === 0xff00) return true; // ff00::/8 multicast\n return false;\n}\n\n/**\n * Expand an IPv6 string into exactly 8 16-bit numbers. Handles `::`\n * compression. Returns null on malformed input — caller should treat that\n * as \"block\".\n */\nfunction expandIPv6(addr: string): number[] | null {\n const parts = addr.split('::');\n if (parts.length > 2) return null;\n const parseGroups = (s: string): number[] | null => {\n if (s === '') return [];\n const out: number[] = [];\n for (const g of s.split(':')) {\n if (g.length === 0 || g.length > 4) return null;\n const n = Number.parseInt(g, 16);\n if (Number.isNaN(n) || n < 0 || n > 0xffff) return null;\n out.push(n);\n }\n return out;\n };\n if (parts.length === 1) {\n const groups = parseGroups(parts[0] ?? '');\n if (!groups || groups.length !== 8) return null;\n return groups;\n }\n const head = parseGroups(parts[0] ?? '');\n const tail = parseGroups(parts[1] ?? '');\n if (!head || !tail) return null;\n const fill = 8 - head.length - tail.length;\n if (fill < 0) return null;\n return [...head, ...new Array<number>(fill).fill(0), ...tail];\n}\n\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n\n/**\n * Simplified regex-based HTML-to-Markdown converter. Handles the common\n * case (headings, bold/italic, links, code blocks, lists) adequately for\n * LLM context consumption. Known limitations: nested tags, attributes\n * containing `>`, malformed HTML, or unusual markup may produce incorrect\n * output. This is acceptable — the result is fed to LLM context, not\n * rendered in a browser. For strict correctness, replace with a dedicated\n * converter library (e.g. turndown, marked).\n */\nfunction htmlToMarkdown(html: string): string {\n let s = html;\n // Strip scripts/styles\n s = s.replace(/<script[\\s\\S]*?<\\/script>/gi, '');\n s = s.replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n s = s.replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, '');\n // Headings\n s = s.replace(/<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_m, n, c) => {\n return '\\n' + '#'.repeat(Number(n)) + ' ' + stripTags(c).trim() + '\\n';\n });\n // Bold / italic\n s = s.replace(/<(strong|b)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '**$2**');\n s = s.replace(/<(em|i)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '*$2*');\n // Links — only emit markdown links for safe protocols\n // Explicitly reject dangerous schemes (javascript:, data:, vbscript:) to prevent XSS\n s = s.replace(/<a [^>]*href=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, (_m, href, text) => {\n const safe =\n /^(https?|ftps?):\\/\\//i.test(href) &&\n !/^(javascript|data|vbscript):/i.test(href);\n return safe ? `[${text}](${href})` : text;\n });\n // Code\n s = s.replace(/<pre[^>]*>([\\s\\S]*?)<\\/pre>/gi, (_m, c) => '\\n```\\n' + stripTags(c) + '\\n```\\n');\n s = s.replace(/<code[^>]*>([\\s\\S]*?)<\\/code>/gi, '`$1`');\n // Lists\n s = s.replace(/<li[^>]*>([\\s\\S]*?)<\\/li>/gi, '- $1\\n');\n // Breaks / paragraphs\n s = s.replace(/<br\\s*\\/?>/gi, '\\n');\n s = s.replace(/<\\/p>/gi, '\\n\\n');\n // Strip remaining tags\n s = stripTags(s);\n // Decode common entities\n s = s\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&nbsp;/g, ' ');\n // Collapse whitespace\n return s.replace(/\\n{3,}/g, '\\n\\n').trim();\n}\n\nfunction stripTags(s: string): string {\n return s.replace(/<[^>]+>/g, '');\n}\n","import { expectDefined } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\n\nexport const searchTool: Tool<SearchInput, SearchOutput> = {\n name: 'search',\n category: 'Search',\n description:\n 'Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase.',\n usageHint:\n 'Good for: API documentation, error messages, library usage examples, current best practices.\\n\\n' +\n '- Prefer specific queries over very broad ones.\\n' +\n '- Results go through the guarded fetch system (same protections as the `fetch` tool).\\n' +\n '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n num_results: {\n type: 'integer',\n description: 'Number of results (1-50, default 10)',\n minimum: 1,\n maximum: MAX_RESULTS,\n },\n source: {\n type: 'string',\n enum: ['duckduckgo', 'google', 'bing'],\n description: 'Search engine to use (default: duckduckgo)',\n },\n },\n required: ['query'],\n },\n async execute(input, ctx, opts) {\n let final: SearchOutput | undefined;\n const executeStream = searchTool.executeStream;\n if (!executeStream) throw new Error('searchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('search: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<SearchOutput>> {\n if (!input?.query) throw new Error('search: query is required');\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query },\n };\n\n let output: SearchOutput;\n switch (source) {\n case 'duckduckgo':\n output = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n output = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n output = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new Error(`search: unknown source \"${source}\"`);\n }\n\n yield {\n type: 'partial_output',\n text: `${output.results.length} results from ${output.source}`,\n data: { count: output.results.length },\n };\n yield { type: 'final', output };\n },\n};\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\n const encoded = encodeURIComponent(query);\n const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;\n\n const results = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .then((html) => parseDuckDuckGo(html, num))\n .catch(() => [{ title: 'Search unavailable', url: '', snippet: 'Could not reach DuckDuckGo' }]);\n\n return {\n query,\n results,\n source: 'duckduckgo',\n truncated: results.length >= num,\n };\n}\n\nfunction takeFrom<T>(iter: Iterable<T>, max: number): T[] {\n const out: T[] = [];\n for (const item of iter) {\n if (out.length >= max) break;\n out.push(item);\n }\n return out;\n}\n\nfunction parseDuckDuckGo(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const snippetRegex = /<a class=\"result-link\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>/gi;\n const snippet2Regex = /<a class=\"result-snippet\"[^>]*>([^<]+)<\\/a>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n results.push({\n title: entry?.title ?? '',\n url: entry?.url ?? '',\n snippet: snippetMatches[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function googleSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.google.com/search?q=${encoded}&hl=en`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n const results = parseGoogleResults(html, num);\n\n return {\n query,\n results,\n source: 'google',\n truncated: results.length >= num,\n };\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const titleRegex = /<h3[^>]*class=\"[^\"]*DKV84\"[^>]*>([^<]+)<\\/h3>/gi;\n const urlRegex = /<cite[^>]*>([^<]+)<\\/cite>/gi;\n const snippetRegex = /<span[^>]*class=\"[^\"]*aXCZ0b[^>]*>([^<]+)<\\/span>/gi;\n\n const titles = takeFrom(\n [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n const urls = takeFrom(\n [...html.matchAll(urlRegex)]\n .filter((m) => m[1])\n .map((m) => stripTags(expectDefined(m[1])).replace(/^\\*(https?:\\/\\/[^\\s]+).*$/, '$1'))\n .filter((u) => u.startsWith('http')),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < Math.min(titles.length, num); i++) {\n results.push({\n title: titles[i] ?? '',\n url: urls[i] ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchOutput> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.bing.com/search?q=${encoded}`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n const results = parseBingResults(html, num);\n\n return {\n query,\n results,\n source: 'bing',\n truncated: results.length >= num,\n };\n}\n\nfunction parseBingResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const titleRegex = /<h2[^>]*>\\s*<a[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>\\s*<\\/h2>/gi;\n const snippetRegex = /<p[^>]*class=\"[^\"]*b_paractl[^\"]*\"[^>]*>([^<]+)<\\/p>/gi;\n\n const entries = takeFrom(\n [...html.matchAll(titleRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < entries.length; i++) {\n results.push({\n title: entries[i]?.title ?? '',\n url: entries[i]?.url ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function fetchWithTimeout(\n url: string,\n signal: AbortSignal,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const fetchSignal = anySignal(signal, controller.signal);\n try {\n // F-05: route through the SSRF-guarded fetch (private-IP blocking, HTTPS,\n // DNS-pinned dispatcher, per-hop redirect re-validation) instead of a bare\n // `fetch` with `redirect: 'follow'`. Search hosts are fixed/trusted, but\n // this closes the residual \"engine 30x → internal address\" redirect risk.\n const res = await guardedFetch(url, 5, fetchSignal, {\n 'user-agent':\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n });\n clearTimeout(timer);\n return res;\n } catch (e) {\n clearTimeout(timer);\n throw e;\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const s of signals) {\n if (s.aborted) {\n controller.abort();\n break;\n }\n s.addEventListener('abort', () => controller.abort());\n }\n return controller.signal;\n}\n\nfunction stripTags(html: string): string {\n return html\n .replace(/<[^>]+>/g, '')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .trim();\n}\n"]}
package/dist/test.js CHANGED
@@ -1,15 +1,9 @@
1
1
  import * as path from 'node:path';
2
- import { spawn } from 'node:child_process';
3
2
  import * as Core from '@wrongstack/core';
4
- import { buildChildEnv } from '@wrongstack/core';
3
+ import { buildChildEnv, expectDefined } from '@wrongstack/core';
4
+ import { spawn } from 'node:child_process';
5
5
 
6
6
  // src/test.ts
7
- function expectDefined(value) {
8
- if (value === null || value === void 0) {
9
- throw new Error("Expected value to be defined");
10
- }
11
- return value;
12
- }
13
7
  async function* spawnStream(opts) {
14
8
  const max = opts.maxBytes;
15
9
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -88,12 +82,6 @@ async function* spawnStream(opts) {
88
82
  error
89
83
  };
90
84
  }
91
- function expectDefined2(value) {
92
- if (value === null || value === void 0) {
93
- throw new Error("Expected value to be defined");
94
- }
95
- return value;
96
- }
97
85
  function resolvePath(input, ctx) {
98
86
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
99
87
  }
@@ -125,9 +113,9 @@ function collapseConsecutiveDuplicates(text, minRun = REPEAT_RUN_THRESHOLD) {
125
113
  while (j < lines.length && lines[j] === lines[i]) j++;
126
114
  const run = j - i;
127
115
  if (run >= minRun) {
128
- out.push(expectDefined2(lines[i]), `\u2026 \u27E8repeated ${run}\xD7\u27E9`);
116
+ out.push(expectDefined(lines[i]), `\u2026 \u27E8repeated ${run}\xD7\u27E9`);
129
117
  } else {
130
- for (let k = i; k < j; k++) out.push(expectDefined2(lines[k]));
118
+ for (let k = i; k < j; k++) out.push(expectDefined(lines[k]));
131
119
  }
132
120
  i = j;
133
121
  }
package/dist/test.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_spawn-stream.ts","../src/_util.ts","../src/test.ts"],"names":["resolve","expectDefined","path2"],"mappings":"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","file":"test.js","sourcesContent":["import { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { ToolProgressEvent } from '@wrongstack/core';\n\n\n\nfunction expectDefined<T>(value: T | null | undefined): T {\n if (value === null || value === undefined) {\n throw new Error('Expected value to be defined');\n }\n return value;\n}\n\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string | undefined;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number | undefined;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number | undefined;\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 env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number | undefined };\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 = expectDefined(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 fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n\n\nfunction expectDefined<T>(value: T | null | undefined): T {\n if (value === null || value === undefined) {\n throw new Error('Expected value to be defined');\n }\n return value;\n}\n\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(expectDefined(lines[i]), `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(expectDefined(lines[k]));\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as path from 'node:path';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { spawnStream } from './_spawn-stream.js';\nimport { normalizeCommandOutput, safeResolve } from './_util.js';\n\ninterface TestInput {\n files?: string | string[] | undefined;\n runner?: 'vitest' | 'jest' | 'mocha' | 'auto' | undefined;\n watch?: boolean | undefined;\n coverage?: boolean | undefined;\n cwd?: string | undefined;\n grep?: string | undefined;\n timeout?: number | undefined;\n}\n\ninterface TestOutput {\n runner: string;\n exit_code: number;\n tests_run: number;\n passed: number;\n failed: number;\n duration_ms: number;\n output: string;\n truncated: boolean;\n}\n\nexport const testTool: Tool<TestInput, TestOutput> = {\n name: 'test',\n category: 'Code Quality',\n description:\n 'Execute the project\\'s test suite. This is one of the most critical tools for validating that your changes are correct.',\n usageHint:\n 'ESSENTIAL BEFORE CONSIDERING WORK DONE:\\n\\n' +\n '- Use `files` or `grep` to run only relevant tests during development.\\n' +\n '- `coverage: true` is useful when working on critical paths.\\n' +\n 'Run tests frequently. A clean test run is usually required before the task can be considered complete.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 120_000,\n inputSchema: {\n type: 'object',\n properties: {\n files: {\n type: 'string',\n description: 'Test files: single path, comma-separated list, or glob (e.g. \"**/*.test.ts\")',\n },\n runner: {\n type: 'string',\n enum: ['vitest', 'jest', 'mocha', 'auto'],\n description: 'Test runner (default: auto-detect)',\n },\n watch: { type: 'boolean', description: 'Run in watch mode (default: false)' },\n coverage: { type: 'boolean', description: 'Generate coverage report (default: false)' },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n grep: { type: 'string', description: 'Filter tests by name pattern (default: none)' },\n timeout: { type: 'integer', description: 'Test timeout in ms (default: 30000)' },\n },\n },\n async execute(input, ctx, opts) {\n let final: TestOutput | undefined;\n const executeStream = testTool.executeStream;\n if (!executeStream) throw new Error('testTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('test: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<TestOutput>> {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const runner = input.runner ?? 'auto';\n\n const detected = runner === 'auto' ? await detectRunner(cwd) : runner;\n if (!detected) {\n yield {\n type: 'final',\n output: {\n runner: 'none',\n exit_code: 1,\n tests_run: 0,\n passed: 0,\n failed: 0,\n duration_ms: 0,\n output: 'No test runner found (vitest.config.ts, jest.config.js, .mocharc.json)',\n truncated: false,\n },\n };\n return;\n }\n\n yield { type: 'log', text: `Running ${detected}…`, data: { runner: detected } };\n\n const start = Date.now();\n const args = buildArgs(detected, input);\n\n const result = yield* spawnStream({\n cmd: detected,\n args,\n cwd,\n signal: opts.signal,\n maxBytes: 200_000,\n });\n const duration = Date.now() - start;\n\n yield { type: 'final', output: parseResult(detected, result, duration) };\n },\n};\n\nasync function detectRunner(cwd: string): Promise<string | null> {\n const { stat } = await import('node:fs/promises');\n const candidates = ['vitest.config.ts', 'jest.config.js', '.mocharc.json'];\n for (const f of candidates) {\n try {\n await stat(path.join(cwd, f));\n if (f.includes('vitest')) return 'vitest';\n if (f.includes('jest')) return 'jest';\n if (f.includes('mocha')) return 'mocha';\n } catch {\n // continue\n }\n }\n return 'vitest';\n}\n\nfunction buildArgs(runner: string, input: TestInput): string[] {\n const args: string[] = [];\n const timeout = input.timeout ?? 30000;\n\n switch (runner) {\n case 'vitest':\n args.push('run', '--reporter=verbose');\n if (input.watch) {\n args[1] = '';\n args.push('watch');\n }\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testNamePattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'jest':\n args.push('--verbose');\n if (input.watch) args.push('--watch');\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testPathPattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'mocha':\n args.push('--reporter', 'spec');\n if (input.grep) args.push('--grep', input.grep);\n args.push('--timeout', String(timeout));\n break;\n }\n\n if (input.files) {\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\n args.push('--', ...files.map((f) => f.trim()));\n }\n\n return args;\n}\n\nfunction parseResult(\n runner: string,\n result: { stdout: string; stderr: string; exitCode: number; truncated: boolean; error?: string | undefined },\n duration: number,\n): TestOutput {\n const out = result.stdout + result.stderr;\n\n let tests_run = 0;\n let passed = 0;\n let failed = 0;\n\n if (runner === 'vitest') {\n const passedMatch = out.match(/(\\d+) passed/);\n const failedMatch = out.match(/(\\d+) failed/);\n if (passedMatch?.[1]) passed = Number.parseInt(passedMatch[1], 10);\n if (failedMatch?.[1]) failed = Number.parseInt(failedMatch[1], 10);\n tests_run = passed + failed;\n } else if (runner === 'jest') {\n const suitesMatch = out.match(/Test Suites:\\s+(\\d+)\\s+total/);\n const passedMatch = out.match(/Tests:\\s+(\\d+)\\s+passed/);\n const failedMatch = out.match(/Tests:\\s+(\\d+)\\s+failed/);\n tests_run = Number.parseInt(suitesMatch?.[1] ?? '0', 10);\n passed = Number.parseInt(passedMatch?.[1] ?? '0', 10);\n failed = Number.parseInt(failedMatch?.[1] ?? '0', 10);\n }\n\n return {\n runner,\n exit_code: result.exitCode,\n tests_run,\n passed,\n failed,\n duration_ms: duration,\n output: normalizeCommandOutput(result.stdout || result.error || ''),\n truncated: result.truncated,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/_spawn-stream.ts","../src/_util.ts","../src/test.ts"],"names":["resolve","expectDefined","path2"],"mappings":"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","file":"test.js","sourcesContent":["import { expectDefined } from '@wrongstack/core';\nimport { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { ToolProgressEvent } from '@wrongstack/core';\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string | undefined;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number | undefined;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number | undefined;\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 env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number | undefined };\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 = expectDefined(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 { expectDefined } from '@wrongstack/core';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n const realRoot = await fsp.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n const rel = path.relative(realRoot, real);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoot}\"`,\n );\n }\n return;\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(expectDefined(lines[i]), `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(expectDefined(lines[k]));\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as path from 'node:path';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { spawnStream } from './_spawn-stream.js';\nimport { normalizeCommandOutput, safeResolve } from './_util.js';\n\ninterface TestInput {\n files?: string | string[] | undefined;\n runner?: 'vitest' | 'jest' | 'mocha' | 'auto' | undefined;\n watch?: boolean | undefined;\n coverage?: boolean | undefined;\n cwd?: string | undefined;\n grep?: string | undefined;\n timeout?: number | undefined;\n}\n\ninterface TestOutput {\n runner: string;\n exit_code: number;\n tests_run: number;\n passed: number;\n failed: number;\n duration_ms: number;\n output: string;\n truncated: boolean;\n}\n\nexport const testTool: Tool<TestInput, TestOutput> = {\n name: 'test',\n category: 'Code Quality',\n description:\n 'Execute the project\\'s test suite. This is one of the most critical tools for validating that your changes are correct.',\n usageHint:\n 'ESSENTIAL BEFORE CONSIDERING WORK DONE:\\n\\n' +\n '- Use `files` or `grep` to run only relevant tests during development.\\n' +\n '- `coverage: true` is useful when working on critical paths.\\n' +\n 'Run tests frequently. A clean test run is usually required before the task can be considered complete.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 120_000,\n inputSchema: {\n type: 'object',\n properties: {\n files: {\n type: 'string',\n description: 'Test files: single path, comma-separated list, or glob (e.g. \"**/*.test.ts\")',\n },\n runner: {\n type: 'string',\n enum: ['vitest', 'jest', 'mocha', 'auto'],\n description: 'Test runner (default: auto-detect)',\n },\n watch: { type: 'boolean', description: 'Run in watch mode (default: false)' },\n coverage: { type: 'boolean', description: 'Generate coverage report (default: false)' },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n grep: { type: 'string', description: 'Filter tests by name pattern (default: none)' },\n timeout: { type: 'integer', description: 'Test timeout in ms (default: 30000)' },\n },\n },\n async execute(input, ctx, opts) {\n let final: TestOutput | undefined;\n const executeStream = testTool.executeStream;\n if (!executeStream) throw new Error('testTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('test: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<TestOutput>> {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const runner = input.runner ?? 'auto';\n\n const detected = runner === 'auto' ? await detectRunner(cwd) : runner;\n if (!detected) {\n yield {\n type: 'final',\n output: {\n runner: 'none',\n exit_code: 1,\n tests_run: 0,\n passed: 0,\n failed: 0,\n duration_ms: 0,\n output: 'No test runner found (vitest.config.ts, jest.config.js, .mocharc.json)',\n truncated: false,\n },\n };\n return;\n }\n\n yield { type: 'log', text: `Running ${detected}…`, data: { runner: detected } };\n\n const start = Date.now();\n const args = buildArgs(detected, input);\n\n const result = yield* spawnStream({\n cmd: detected,\n args,\n cwd,\n signal: opts.signal,\n maxBytes: 200_000,\n });\n const duration = Date.now() - start;\n\n yield { type: 'final', output: parseResult(detected, result, duration) };\n },\n};\n\nasync function detectRunner(cwd: string): Promise<string | null> {\n const { stat } = await import('node:fs/promises');\n const candidates = ['vitest.config.ts', 'jest.config.js', '.mocharc.json'];\n for (const f of candidates) {\n try {\n await stat(path.join(cwd, f));\n if (f.includes('vitest')) return 'vitest';\n if (f.includes('jest')) return 'jest';\n if (f.includes('mocha')) return 'mocha';\n } catch {\n // continue\n }\n }\n return 'vitest';\n}\n\nfunction buildArgs(runner: string, input: TestInput): string[] {\n const args: string[] = [];\n const timeout = input.timeout ?? 30000;\n\n switch (runner) {\n case 'vitest':\n args.push('run', '--reporter=verbose');\n if (input.watch) {\n args[1] = '';\n args.push('watch');\n }\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testNamePattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'jest':\n args.push('--verbose');\n if (input.watch) args.push('--watch');\n if (input.coverage) args.push('--coverage');\n if (input.grep) args.push('--testPathPattern', input.grep);\n args.push('--testTimeout', String(timeout));\n break;\n case 'mocha':\n args.push('--reporter', 'spec');\n if (input.grep) args.push('--grep', input.grep);\n args.push('--timeout', String(timeout));\n break;\n }\n\n if (input.files) {\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\n args.push('--', ...files.map((f) => f.trim()));\n }\n\n return args;\n}\n\nfunction parseResult(\n runner: string,\n result: { stdout: string; stderr: string; exitCode: number; truncated: boolean; error?: string | undefined },\n duration: number,\n): TestOutput {\n const out = result.stdout + result.stderr;\n\n let tests_run = 0;\n let passed = 0;\n let failed = 0;\n\n if (runner === 'vitest') {\n const passedMatch = out.match(/(\\d+) passed/);\n const failedMatch = out.match(/(\\d+) failed/);\n if (passedMatch?.[1]) passed = Number.parseInt(passedMatch[1], 10);\n if (failedMatch?.[1]) failed = Number.parseInt(failedMatch[1], 10);\n tests_run = passed + failed;\n } else if (runner === 'jest') {\n const suitesMatch = out.match(/Test Suites:\\s+(\\d+)\\s+total/);\n const passedMatch = out.match(/Tests:\\s+(\\d+)\\s+passed/);\n const failedMatch = out.match(/Tests:\\s+(\\d+)\\s+failed/);\n tests_run = Number.parseInt(suitesMatch?.[1] ?? '0', 10);\n passed = Number.parseInt(passedMatch?.[1] ?? '0', 10);\n failed = Number.parseInt(failedMatch?.[1] ?? '0', 10);\n }\n\n return {\n runner,\n exit_code: result.exitCode,\n tests_run,\n passed,\n failed,\n duration_ms: duration,\n output: normalizeCommandOutput(result.stdout || result.error || ''),\n truncated: result.truncated,\n };\n}\n"]}
package/dist/tree.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { expectDefined } from '@wrongstack/core';
1
2
  import * as fs from 'node:fs/promises';
2
3
  import * as path from 'node:path';
3
- import '@wrongstack/core';
4
4
 
5
5
  // src/tree.ts
6
6
  function resolvePath(input, ctx) {
@@ -20,12 +20,6 @@ function safeResolve(input, ctx) {
20
20
  }
21
21
 
22
22
  // src/tree.ts
23
- function expectDefined(value) {
24
- if (value === null || value === void 0) {
25
- throw new Error("Expected value to be defined");
26
- }
27
- return value;
28
- }
29
23
  var DEFAULT_IGNORE = [
30
24
  "node_modules",
31
25
  ".git",