@wrongstack/tools 0.283.0 → 0.284.0

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 (59) hide show
  1. package/dist/audit.js.map +1 -1
  2. package/dist/bash.js +4 -0
  3. package/dist/bash.js.map +1 -1
  4. package/dist/builtin.js +710 -259
  5. package/dist/builtin.js.map +1 -1
  6. package/dist/codebase-index/index.d.ts +11 -0
  7. package/dist/codebase-index/index.js +27 -8
  8. package/dist/codebase-index/index.js.map +1 -1
  9. package/dist/codebase-index/worker.js +25 -6
  10. package/dist/codebase-index/worker.js.map +1 -1
  11. package/dist/diff.js.map +1 -1
  12. package/dist/document.js.map +1 -1
  13. package/dist/edit.d.ts +43 -0
  14. package/dist/edit.js +386 -60
  15. package/dist/edit.js.map +1 -1
  16. package/dist/{exec-Ca3fnpUh.d.ts → exec-2OKoT6tk.d.ts} +1 -1
  17. package/dist/exec.d.ts +1 -1
  18. package/dist/exec.js +13 -14
  19. package/dist/exec.js.map +1 -1
  20. package/dist/fetch.js.map +1 -1
  21. package/dist/format.js.map +1 -1
  22. package/dist/git.js.map +1 -1
  23. package/dist/glob.js +10 -1
  24. package/dist/glob.js.map +1 -1
  25. package/dist/grep.js +4 -0
  26. package/dist/grep.js.map +1 -1
  27. package/dist/index.d.ts +13 -2
  28. package/dist/index.js +755 -274
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js.map +1 -1
  31. package/dist/json.js.map +1 -1
  32. package/dist/lint.js.map +1 -1
  33. package/dist/logs.js.map +1 -1
  34. package/dist/next-steps.d.ts +66 -0
  35. package/dist/next-steps.js +116 -0
  36. package/dist/next-steps.js.map +1 -0
  37. package/dist/outdated.js.map +1 -1
  38. package/dist/pack.js +710 -259
  39. package/dist/pack.js.map +1 -1
  40. package/dist/patch.js +43 -0
  41. package/dist/patch.js.map +1 -1
  42. package/dist/read.js +13 -4
  43. package/dist/read.js.map +1 -1
  44. package/dist/replace.js +14 -0
  45. package/dist/replace.js.map +1 -1
  46. package/dist/scaffold.js.map +1 -1
  47. package/dist/test.js.map +1 -1
  48. package/dist/tool-diff.d.ts +88 -0
  49. package/dist/tool-diff.js +229 -0
  50. package/dist/tool-diff.js.map +1 -0
  51. package/dist/tool-summary.d.ts +24 -0
  52. package/dist/tool-summary.js +208 -0
  53. package/dist/tool-summary.js.map +1 -0
  54. package/dist/tree.js.map +1 -1
  55. package/dist/typecheck.js.map +1 -1
  56. package/dist/write.d.ts +6 -0
  57. package/dist/write.js +118 -19
  58. package/dist/write.js.map +1 -1
  59. package/package.json +15 -2
package/dist/git.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/git.ts"],"names":["resolve"],"mappings":"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","file":"git.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/** 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.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\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 // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\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 if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\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(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 { spawn } from 'node:child_process';\nimport { statSync } from 'node:fs';\nimport { dirname, resolve, sep } from 'node:path';\nimport { assessCommitSafety, buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { COMMAND_OUTPUT_MAX_BYTES, normalizeCommandOutput } from './_util.js';\n\ntype GitSubcommand =\n | 'status'\n | 'log'\n | 'diff'\n | 'commit'\n | 'branch'\n | 'checkout'\n | 'stash'\n | 'push'\n | 'pull'\n | 'fetch'\n | 'reset'\n | 'worktree';\n\ninterface GitInput {\n command: GitSubcommand;\n files?: string | string[] | undefined;\n dry_run?: boolean | undefined;\n /** commit message for `commit` subcommand */\n message?: string | undefined;\n /** branch name for `checkout` / `branch` */\n branch?: string | undefined;\n /** pass --graph, --oneline, --stat for `log` */\n format?: 'short' | 'oneline' | 'stat' | 'graph' | undefined;\n /** limit for `log` */\n limit?: number | undefined;\n /** worktree action: list, add, remove, prune */\n worktreeAction?: 'list' | 'add' | 'remove' | 'prune' | undefined;\n /** path for worktree add/remove (e.g. \"../wt-feature-xyz\") */\n worktreePath?: string | undefined;\n /** create new branch when adding worktree */\n newBranch?: boolean | undefined;\n /** force operation (e.g. worktree remove --force) */\n force?: boolean | undefined;\n}\n\ninterface GitOutput {\n command: GitSubcommand;\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n /** Staged diff shown for commit commands so the caller can verify. */\n diff?: string | undefined;\n /**\n * Shared-worktree warning for `commit`: present when uncommitted changes\n * were authored by another agent/session (or a concurrent non-wrongstack\n * process). The commit still proceeds — this is advisory so the caller can\n * reconsider committing work it did not write.\n */\n warning?: string | undefined;\n}\n\nconst TIMEOUT_MS = 30_000;\nconst MAX_OUTPUT = 100_000;\n\nexport const gitTool: Tool<GitInput, GitOutput> = {\n name: 'git',\n category: 'Git',\n description:\n 'Safe wrapper around common git operations. Supports status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset, worktree, etc. ' +\n 'This is the preferred way to interact with git instead of using the raw `bash` or `exec` tools.',\n usageHint:\n 'ALWAYS prefer this tool over raw shell git commands.\\n\\n' +\n 'Key fields:\\n' +\n '- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\\n' +\n '- Use `message` only for commit operations.\\n' +\n '- Use `files` array for operations that take paths (status, diff, add, etc.).\\n' +\n '- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\\n' +\n '- For `commit` in a possibly-shared working tree, pass an explicit `files` list scoped to ' +\n 'what YOU changed. A bare commit (no `files`) includes ALL staged changes and may capture ' +\n \"another agent's half-done work. Heed the `warning` field on the result.\\n\" +\n 'Never pass raw git flags through `args` for dangerous operations — use the structured fields.',\n permission: 'confirm',\n icon: 'git',\n // Conservative: any of these may mutate. The non-mutating commands\n // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`\n // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.\n mutating: true,\n capabilities: ['fs.write', 'shell.restricted'],\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n command: {\n type: 'string',\n enum: [\n 'status',\n 'log',\n 'diff',\n 'commit',\n 'branch',\n 'checkout',\n 'stash',\n 'push',\n 'pull',\n 'fetch',\n 'reset',\n 'worktree',\n ],\n description: 'Git subcommand',\n },\n files: {\n type: 'string',\n description:\n 'File(s) for status/diff: single path, comma-separated list, or \"**/*.ts\" glob',\n },\n message: { type: 'string', description: 'Commit message (required for commit)' },\n branch: { type: 'string', description: 'Branch name for checkout/branch' },\n format: {\n type: 'string',\n enum: ['short', 'oneline', 'stat', 'graph'],\n description: 'Log format (default: short)',\n },\n limit: { type: 'integer', description: 'Limit for log (default: 20)' },\n dry_run: { type: 'boolean', description: 'For commit: show what would be committed' },\n worktreeAction: {\n type: 'string',\n enum: ['list', 'add', 'remove', 'prune'],\n description: 'Worktree action: list, add, remove, prune',\n },\n worktreePath: {\n type: 'string',\n description: 'Path for worktree add/remove (e.g. \"../wt-feature-xyz\")',\n },\n newBranch: {\n type: 'boolean',\n description: 'Create new branch when adding worktree',\n },\n force: {\n type: 'boolean',\n description: 'Force operation (e.g. worktree remove --force)',\n },\n },\n required: ['command'],\n },\n async execute(input, ctx, opts) {\n if (!input?.command) throw new Error('git: command is required');\n\n if (input.command === 'commit' && !input.message) {\n return {\n command: 'commit',\n stdout: '',\n stderr: 'git commit requires a message (-m flag)',\n exitCode: 1,\n truncated: false,\n };\n }\n\n // Validate worktree paths/branches before touching the filesystem: reject\n // flag injection and any path that escapes the project root.\n if (input.command === 'worktree') {\n const guard = validateWorktreeInput(input, ctx.projectRoot);\n if (guard) return guard;\n }\n\n // Bound the search at projectRoot so a non-git project doesn't drift\n // into a parent repo (e.g. ~/repos/.git) and operate on the wrong tree.\n const gitDir = findGitDir(ctx.cwd, ctx.projectRoot);\n if (!gitDir) {\n return {\n command: input.command,\n stdout: '',\n stderr: 'Not in a git repository (within project root)',\n exitCode: 128,\n truncated: false,\n };\n }\n\n const args = buildArgs(input);\n\n // For commits, check whether the working tree holds changes this session\n // did not author (a concurrent agent / separate wrongstack process / human).\n // Warn-only: the commit still runs, but the caller sees the risk of sweeping\n // up another agent's half-done work. Best-effort — never blocks the commit.\n let safetyWarning: string | undefined;\n if (input.command === 'commit') {\n try {\n const report = await assessCommitSafety({\n cwd: ctx.cwd,\n projectRoot: ctx.projectRoot,\n sessionId: ctx.session?.id,\n signal: opts.signal,\n });\n if (report.warning) {\n // Committing without an explicit file list stages/commits everything\n // already staged — the highest-risk path for capturing foreign work.\n const scopeNote = input.files\n ? ''\n : '\\nNote: this commit has no explicit `files` list, so it will include ALL ' +\n 'staged changes. Pass `files` to scope the commit to only what you changed.';\n safetyWarning = report.warning + scopeNote;\n }\n } catch {\n // commit-safety is advisory; ignore failures\n }\n }\n\n // For commits, capture the staged diff BEFORE committing so the caller\n // can verify what is about to land without another tool call.\n let stagedDiff: string | undefined;\n if (input.command === 'commit' && !input.dry_run) {\n try {\n const diffResult = await runGit(['diff', '--cached'], gitDir, opts.signal);\n if (diffResult.exitCode === 0) {\n const MAX_DIFF = 20_000;\n stagedDiff =\n diffResult.stdout.length > MAX_DIFF\n ? diffResult.stdout.slice(0, MAX_DIFF) + '\\n\\n... (diff truncated)'\n : diffResult.stdout;\n }\n } catch {\n // Diff capture is best-effort; don't fail the whole operation\n }\n }\n\n const result = await runGit(args, gitDir, opts.signal);\n if (stagedDiff !== undefined) result.diff = stagedDiff;\n if (safetyWarning !== undefined) result.warning = safetyWarning;\n return result;\n },\n};\n\n/**\n * Reject worktree inputs that could inject git flags or escape the project\n * root. Returns a `GitOutput` describing the rejection, or `null` if safe.\n */\nfunction validateWorktreeInput(input: GitInput, projectRoot: string): GitOutput | null {\n const reject = (stderr: string): GitOutput => ({\n command: 'worktree',\n stdout: '',\n stderr,\n exitCode: 1,\n truncated: false,\n });\n\n // Flag injection: a leading '-' would be parsed as a git option.\n if (input.branch?.startsWith('-')) return reject(`unsafe branch name: ${input.branch}`);\n if (input.worktreePath?.startsWith('-')) {\n return reject(`unsafe worktree path: ${input.worktreePath}`);\n }\n\n // Path escape: add/remove targets must resolve inside the project root.\n if (\n (input.worktreeAction === 'add' || input.worktreeAction === 'remove') &&\n input.worktreePath\n ) {\n const root = resolve(projectRoot);\n const abs = resolve(root, input.worktreePath);\n if (abs !== root && !abs.startsWith(root + sep)) {\n return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);\n }\n }\n\n return null;\n}\n\nfunction findGitDir(cwd: string, projectRoot: string): string | null {\n const root = projectRoot;\n let dir = cwd;\n for (let i = 0; i < 20; i++) {\n try {\n const stat = statSync(`${dir}/.git`);\n // A normal repo has a `.git` directory; a linked worktree has a `.git`\n // *file* (gitlink pointing at the main repo). Accept both so the tool\n // operates inside a worktree when a subagent's cwd is a worktree dir.\n if (stat.isDirectory() || stat.isFile()) return dir;\n } catch {\n // continue\n }\n if (dir === root) break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\nfunction buildArgs(input: GitInput): string[] {\n const limit = input.limit ?? 20;\n const files = input.files\n ? (Array.isArray(input.files) ? input.files : input.files.split(','))\n .map((s: string) => s.trim())\n .filter(Boolean)\n : [];\n\n switch (input.command) {\n case 'status':\n return ['status', ...(files.length ? ['--', ...files] : [])];\n case 'log':\n return [\n 'log',\n `--max-count=${limit}`,\n ...(input.format === 'oneline' ? ['--oneline'] : []),\n ...(input.format === 'stat' ? ['--stat'] : []),\n ...(input.format === 'graph' ? ['--oneline', '--graph', '--decorate'] : []),\n ...(input.format === 'short' || !input.format ? [] : []),\n ];\n case 'diff':\n return ['diff', '--no-color', ...(files.length ? ['--', ...files] : [])];\n case 'commit':\n return [\n 'commit',\n ...(input.dry_run ? ['--dry-run', '--porcelain'] : []),\n ...(input.message ? ['-m', input.message] : []),\n ...(files.length ? ['--', ...files] : []),\n ];\n case 'branch':\n // Validate branch name: reject names starting with '-' or containing ' --'\n // to prevent flag injection (e.g. \"foo --force\").\n return input.branch\n ? ['branch', ...(input.branch.startsWith('-') || input.branch.includes(' --') ? [] : [input.branch])]\n : ['branch'];\n case 'checkout':\n return [\n 'checkout',\n ...(input.branch ? ['--', input.branch] : []),\n ...(files.length ? ['--', ...files] : []),\n ];\n case 'stash':\n return input.message ? ['stash', 'push', '-m', input.message] : ['stash', 'push'];\n case 'push':\n return ['push'];\n case 'pull':\n return ['pull'];\n case 'fetch':\n return ['fetch', ...(input.branch ? [input.branch] : ['--all'])];\n case 'reset':\n return ['reset', ...(files.length ? ['--', ...files] : [])];\n case 'worktree':\n switch (input.worktreeAction) {\n case 'list':\n return ['worktree', 'list'];\n case 'add': {\n // git worktree add [-b <new-branch>] <path> [<commit-ish>]\n // The path comes BEFORE the branch/commit-ish. With --newBranch the\n // branch is the name to create (`-b <branch> <path>`); without it the\n // branch is an existing branch/commit to check out (`<path> <branch>`).\n if (!input.worktreePath) return ['worktree', 'list'];\n const add = ['worktree', 'add'];\n if (input.newBranch && input.branch) add.push('-b', input.branch);\n add.push(input.worktreePath);\n if (!input.newBranch && input.branch) add.push(input.branch);\n return add;\n }\n case 'remove':\n return [\n 'worktree',\n 'remove',\n ...(input.force ? ['--force'] : []),\n input.worktreePath ?? '',\n ].filter(Boolean);\n case 'prune':\n return ['worktree', 'prune'];\n default:\n return ['worktree', 'list'];\n }\n default:\n return [input.command];\n }\n}\n\nfunction runGit(args: string[], cwd: string, signal: AbortSignal): Promise<GitOutput> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n const child = spawn('git', args, {\n cwd,\n signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n if (stdout.length < MAX_OUTPUT) {\n stdout += chunk.toString();\n }\n });\n\n child.stderr?.on('data', (chunk: Buffer) => {\n if (stderr.length < MAX_OUTPUT) {\n stderr += chunk.toString();\n }\n });\n\n child.on('error', (err) => {\n resolve({\n command: args[0] as GitSubcommand,\n stdout: normalizeCommandOutput(stdout),\n stderr: err.message,\n exitCode: 1,\n truncated: Buffer.byteLength(stdout, 'utf8') > COMMAND_OUTPUT_MAX_BYTES,\n });\n });\n\n child.on('close', (code) => {\n // `MAX_OUTPUT` already bounded the raw buffers in memory; normalize strips\n // ANSI / progress / duplicate noise and head+tail-truncates to the shared\n // command cap so only useful output reaches the model.\n resolve({\n command: args[0] as GitSubcommand,\n stdout: normalizeCommandOutput(stdout),\n stderr: normalizeCommandOutput(stderr),\n exitCode: code ?? 1,\n truncated:\n Buffer.byteLength(stdout, 'utf8') > COMMAND_OUTPUT_MAX_BYTES ||\n Buffer.byteLength(stderr, 'utf8') > COMMAND_OUTPUT_MAX_BYTES,\n });\n });\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/git.ts"],"names":["resolve"],"mappings":"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","file":"git.js","sourcesContent":["import { createHash } from 'node:crypto';\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\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` — the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\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.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\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 // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\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 if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\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(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 { spawn } from 'node:child_process';\nimport { statSync } from 'node:fs';\nimport { dirname, resolve, sep } from 'node:path';\nimport { assessCommitSafety, buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { COMMAND_OUTPUT_MAX_BYTES, normalizeCommandOutput } from './_util.js';\n\ntype GitSubcommand =\n | 'status'\n | 'log'\n | 'diff'\n | 'commit'\n | 'branch'\n | 'checkout'\n | 'stash'\n | 'push'\n | 'pull'\n | 'fetch'\n | 'reset'\n | 'worktree';\n\ninterface GitInput {\n command: GitSubcommand;\n files?: string | string[] | undefined;\n dry_run?: boolean | undefined;\n /** commit message for `commit` subcommand */\n message?: string | undefined;\n /** branch name for `checkout` / `branch` */\n branch?: string | undefined;\n /** pass --graph, --oneline, --stat for `log` */\n format?: 'short' | 'oneline' | 'stat' | 'graph' | undefined;\n /** limit for `log` */\n limit?: number | undefined;\n /** worktree action: list, add, remove, prune */\n worktreeAction?: 'list' | 'add' | 'remove' | 'prune' | undefined;\n /** path for worktree add/remove (e.g. \"../wt-feature-xyz\") */\n worktreePath?: string | undefined;\n /** create new branch when adding worktree */\n newBranch?: boolean | undefined;\n /** force operation (e.g. worktree remove --force) */\n force?: boolean | undefined;\n}\n\ninterface GitOutput {\n command: GitSubcommand;\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n /** Staged diff shown for commit commands so the caller can verify. */\n diff?: string | undefined;\n /**\n * Shared-worktree warning for `commit`: present when uncommitted changes\n * were authored by another agent/session (or a concurrent non-wrongstack\n * process). The commit still proceeds — this is advisory so the caller can\n * reconsider committing work it did not write.\n */\n warning?: string | undefined;\n}\n\nconst TIMEOUT_MS = 30_000;\nconst MAX_OUTPUT = 100_000;\n\nexport const gitTool: Tool<GitInput, GitOutput> = {\n name: 'git',\n category: 'Git',\n description:\n 'Safe wrapper around common git operations. Supports status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset, worktree, etc. ' +\n 'This is the preferred way to interact with git instead of using the raw `bash` or `exec` tools.',\n usageHint:\n 'ALWAYS prefer this tool over raw shell git commands.\\n\\n' +\n 'Key fields:\\n' +\n '- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\\n' +\n '- Use `message` only for commit operations.\\n' +\n '- Use `files` array for operations that take paths (status, diff, add, etc.).\\n' +\n '- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\\n' +\n '- For `commit` in a possibly-shared working tree, pass an explicit `files` list scoped to ' +\n 'what YOU changed. A bare commit (no `files`) includes ALL staged changes and may capture ' +\n \"another agent's half-done work. Heed the `warning` field on the result.\\n\" +\n 'Never pass raw git flags through `args` for dangerous operations — use the structured fields.',\n permission: 'confirm',\n icon: 'git',\n // Conservative: any of these may mutate. The non-mutating commands\n // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`\n // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.\n mutating: true,\n capabilities: ['fs.write', 'shell.restricted'],\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n command: {\n type: 'string',\n enum: [\n 'status',\n 'log',\n 'diff',\n 'commit',\n 'branch',\n 'checkout',\n 'stash',\n 'push',\n 'pull',\n 'fetch',\n 'reset',\n 'worktree',\n ],\n description: 'Git subcommand',\n },\n files: {\n type: 'string',\n description:\n 'File(s) for status/diff: single path, comma-separated list, or \"**/*.ts\" glob',\n },\n message: { type: 'string', description: 'Commit message (required for commit)' },\n branch: { type: 'string', description: 'Branch name for checkout/branch' },\n format: {\n type: 'string',\n enum: ['short', 'oneline', 'stat', 'graph'],\n description: 'Log format (default: short)',\n },\n limit: { type: 'integer', description: 'Limit for log (default: 20)' },\n dry_run: { type: 'boolean', description: 'For commit: show what would be committed' },\n worktreeAction: {\n type: 'string',\n enum: ['list', 'add', 'remove', 'prune'],\n description: 'Worktree action: list, add, remove, prune',\n },\n worktreePath: {\n type: 'string',\n description: 'Path for worktree add/remove (e.g. \"../wt-feature-xyz\")',\n },\n newBranch: {\n type: 'boolean',\n description: 'Create new branch when adding worktree',\n },\n force: {\n type: 'boolean',\n description: 'Force operation (e.g. worktree remove --force)',\n },\n },\n required: ['command'],\n },\n async execute(input, ctx, opts) {\n if (!input?.command) throw new Error('git: command is required');\n\n if (input.command === 'commit' && !input.message) {\n return {\n command: 'commit',\n stdout: '',\n stderr: 'git commit requires a message (-m flag)',\n exitCode: 1,\n truncated: false,\n };\n }\n\n // Validate worktree paths/branches before touching the filesystem: reject\n // flag injection and any path that escapes the project root.\n if (input.command === 'worktree') {\n const guard = validateWorktreeInput(input, ctx.projectRoot);\n if (guard) return guard;\n }\n\n // Bound the search at projectRoot so a non-git project doesn't drift\n // into a parent repo (e.g. ~/repos/.git) and operate on the wrong tree.\n const gitDir = findGitDir(ctx.cwd, ctx.projectRoot);\n if (!gitDir) {\n return {\n command: input.command,\n stdout: '',\n stderr: 'Not in a git repository (within project root)',\n exitCode: 128,\n truncated: false,\n };\n }\n\n const args = buildArgs(input);\n\n // For commits, check whether the working tree holds changes this session\n // did not author (a concurrent agent / separate wrongstack process / human).\n // Warn-only: the commit still runs, but the caller sees the risk of sweeping\n // up another agent's half-done work. Best-effort — never blocks the commit.\n let safetyWarning: string | undefined;\n if (input.command === 'commit') {\n try {\n const report = await assessCommitSafety({\n cwd: ctx.cwd,\n projectRoot: ctx.projectRoot,\n sessionId: ctx.session?.id,\n signal: opts.signal,\n });\n if (report.warning) {\n // Committing without an explicit file list stages/commits everything\n // already staged — the highest-risk path for capturing foreign work.\n const scopeNote = input.files\n ? ''\n : '\\nNote: this commit has no explicit `files` list, so it will include ALL ' +\n 'staged changes. Pass `files` to scope the commit to only what you changed.';\n safetyWarning = report.warning + scopeNote;\n }\n } catch {\n // commit-safety is advisory; ignore failures\n }\n }\n\n // For commits, capture the staged diff BEFORE committing so the caller\n // can verify what is about to land without another tool call.\n let stagedDiff: string | undefined;\n if (input.command === 'commit' && !input.dry_run) {\n try {\n const diffResult = await runGit(['diff', '--cached'], gitDir, opts.signal);\n if (diffResult.exitCode === 0) {\n const MAX_DIFF = 20_000;\n stagedDiff =\n diffResult.stdout.length > MAX_DIFF\n ? diffResult.stdout.slice(0, MAX_DIFF) + '\\n\\n... (diff truncated)'\n : diffResult.stdout;\n }\n } catch {\n // Diff capture is best-effort; don't fail the whole operation\n }\n }\n\n const result = await runGit(args, gitDir, opts.signal);\n if (stagedDiff !== undefined) result.diff = stagedDiff;\n if (safetyWarning !== undefined) result.warning = safetyWarning;\n return result;\n },\n};\n\n/**\n * Reject worktree inputs that could inject git flags or escape the project\n * root. Returns a `GitOutput` describing the rejection, or `null` if safe.\n */\nfunction validateWorktreeInput(input: GitInput, projectRoot: string): GitOutput | null {\n const reject = (stderr: string): GitOutput => ({\n command: 'worktree',\n stdout: '',\n stderr,\n exitCode: 1,\n truncated: false,\n });\n\n // Flag injection: a leading '-' would be parsed as a git option.\n if (input.branch?.startsWith('-')) return reject(`unsafe branch name: ${input.branch}`);\n if (input.worktreePath?.startsWith('-')) {\n return reject(`unsafe worktree path: ${input.worktreePath}`);\n }\n\n // Path escape: add/remove targets must resolve inside the project root.\n if (\n (input.worktreeAction === 'add' || input.worktreeAction === 'remove') &&\n input.worktreePath\n ) {\n const root = resolve(projectRoot);\n const abs = resolve(root, input.worktreePath);\n if (abs !== root && !abs.startsWith(root + sep)) {\n return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);\n }\n }\n\n return null;\n}\n\nfunction findGitDir(cwd: string, projectRoot: string): string | null {\n const root = projectRoot;\n let dir = cwd;\n for (let i = 0; i < 20; i++) {\n try {\n const stat = statSync(`${dir}/.git`);\n // A normal repo has a `.git` directory; a linked worktree has a `.git`\n // *file* (gitlink pointing at the main repo). Accept both so the tool\n // operates inside a worktree when a subagent's cwd is a worktree dir.\n if (stat.isDirectory() || stat.isFile()) return dir;\n } catch {\n // continue\n }\n if (dir === root) break;\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\nfunction buildArgs(input: GitInput): string[] {\n const limit = input.limit ?? 20;\n const files = input.files\n ? (Array.isArray(input.files) ? input.files : input.files.split(','))\n .map((s: string) => s.trim())\n .filter(Boolean)\n : [];\n\n switch (input.command) {\n case 'status':\n return ['status', ...(files.length ? ['--', ...files] : [])];\n case 'log':\n return [\n 'log',\n `--max-count=${limit}`,\n ...(input.format === 'oneline' ? ['--oneline'] : []),\n ...(input.format === 'stat' ? ['--stat'] : []),\n ...(input.format === 'graph' ? ['--oneline', '--graph', '--decorate'] : []),\n ...(input.format === 'short' || !input.format ? [] : []),\n ];\n case 'diff':\n return ['diff', '--no-color', ...(files.length ? ['--', ...files] : [])];\n case 'commit':\n return [\n 'commit',\n ...(input.dry_run ? ['--dry-run', '--porcelain'] : []),\n ...(input.message ? ['-m', input.message] : []),\n ...(files.length ? ['--', ...files] : []),\n ];\n case 'branch':\n // Validate branch name: reject names starting with '-' or containing ' --'\n // to prevent flag injection (e.g. \"foo --force\").\n return input.branch\n ? ['branch', ...(input.branch.startsWith('-') || input.branch.includes(' --') ? [] : [input.branch])]\n : ['branch'];\n case 'checkout':\n return [\n 'checkout',\n ...(input.branch ? ['--', input.branch] : []),\n ...(files.length ? ['--', ...files] : []),\n ];\n case 'stash':\n return input.message ? ['stash', 'push', '-m', input.message] : ['stash', 'push'];\n case 'push':\n return ['push'];\n case 'pull':\n return ['pull'];\n case 'fetch':\n return ['fetch', ...(input.branch ? [input.branch] : ['--all'])];\n case 'reset':\n return ['reset', ...(files.length ? ['--', ...files] : [])];\n case 'worktree':\n switch (input.worktreeAction) {\n case 'list':\n return ['worktree', 'list'];\n case 'add': {\n // git worktree add [-b <new-branch>] <path> [<commit-ish>]\n // The path comes BEFORE the branch/commit-ish. With --newBranch the\n // branch is the name to create (`-b <branch> <path>`); without it the\n // branch is an existing branch/commit to check out (`<path> <branch>`).\n if (!input.worktreePath) return ['worktree', 'list'];\n const add = ['worktree', 'add'];\n if (input.newBranch && input.branch) add.push('-b', input.branch);\n add.push(input.worktreePath);\n if (!input.newBranch && input.branch) add.push(input.branch);\n return add;\n }\n case 'remove':\n return [\n 'worktree',\n 'remove',\n ...(input.force ? ['--force'] : []),\n input.worktreePath ?? '',\n ].filter(Boolean);\n case 'prune':\n return ['worktree', 'prune'];\n default:\n return ['worktree', 'list'];\n }\n default:\n return [input.command];\n }\n}\n\nfunction runGit(args: string[], cwd: string, signal: AbortSignal): Promise<GitOutput> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n const child = spawn('git', args, {\n cwd,\n signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n if (stdout.length < MAX_OUTPUT) {\n stdout += chunk.toString();\n }\n });\n\n child.stderr?.on('data', (chunk: Buffer) => {\n if (stderr.length < MAX_OUTPUT) {\n stderr += chunk.toString();\n }\n });\n\n child.on('error', (err) => {\n resolve({\n command: args[0] as GitSubcommand,\n stdout: normalizeCommandOutput(stdout),\n stderr: err.message,\n exitCode: 1,\n truncated: Buffer.byteLength(stdout, 'utf8') > COMMAND_OUTPUT_MAX_BYTES,\n });\n });\n\n child.on('close', (code) => {\n // `MAX_OUTPUT` already bounded the raw buffers in memory; normalize strips\n // ANSI / progress / duplicate noise and head+tail-truncates to the shared\n // command cap so only useful output reaches the model.\n resolve({\n command: args[0] as GitSubcommand,\n stdout: normalizeCommandOutput(stdout),\n stderr: normalizeCommandOutput(stderr),\n exitCode: code ?? 1,\n truncated:\n Buffer.byteLength(stdout, 'utf8') > COMMAND_OUTPUT_MAX_BYTES ||\n Buffer.byteLength(stderr, 'utf8') > COMMAND_OUTPUT_MAX_BYTES,\n });\n });\n });\n}\n"]}
package/dist/glob.js CHANGED
@@ -81,6 +81,10 @@ var globTool = {
81
81
  category: "Filesystem",
82
82
  description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
83
83
  usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
84
+ selection: {
85
+ doNotUseWhen: "you need to search inside file contents.",
86
+ useInstead: ["grep"]
87
+ },
84
88
  permission: "auto",
85
89
  mutating: false,
86
90
  capabilities: ["fs.read"],
@@ -105,8 +109,9 @@ var globTool = {
105
109
  },
106
110
  required: ["pattern"]
107
111
  },
108
- async execute(input, ctx) {
112
+ async execute(input, ctx, opts) {
109
113
  if (!input?.pattern) throw new Error("glob: pattern is required");
114
+ const signal = opts?.signal;
110
115
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
111
116
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
112
117
  const ignored = await readGitignore(base);
@@ -130,6 +135,10 @@ var globTool = {
130
135
  }
131
136
  };
132
137
  const walk = async (dir, relPrefix) => {
138
+ if (signal?.aborted) {
139
+ truncated = true;
140
+ return;
141
+ }
133
142
  if (results.length >= limit) {
134
143
  truncated = true;
135
144
  return;
package/dist/glob.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_concurrency.ts","../src/_util.ts","../src/glob.ts"],"names":["fsp","path2"],"mappings":"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","file":"glob.js","sourcesContent":["/**\n * Bounded-concurrency async map.\n *\n * Runs `fn` over `items` with at most `limit` in-flight promises. Order of\n * results matches input order. Errors reject the returned promise on the first\n * failure — the remaining in-flight work is not awaited; this matches\n * `Promise.all` semantics for callers that want fail-fast behavior.\n *\n * Lives here (rather than imported from `@wrongstack/bench`) because:\n * 1. tools sits below bench in the dependency graph — bench is a consumer of\n * tools, never the other way around.\n * 2. The semantics used by grep/glob are deliberately simple (fail-fast,\n * no cancellation, no progress) and don't need bench's richer options.\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n if (items.length === 0) return [];\n const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n\n const worker = async (): Promise<void> => {\n while (true) {\n const i = nextIndex++;\n if (i >= items.length) return;\n results[i] = await fn(items[i] as T);\n }\n };\n\n await Promise.all(Array.from({ length: effectiveLimit }, worker));\n return results;\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/** 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.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\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 // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\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 if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\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(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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';\nimport * as path from 'node:path';\nimport { compileGlob } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { mapWithConcurrency } from './_concurrency.js';\nimport { assertRealInsideRoot, safeResolveReal } from './_util.js';\n\ninterface GlobInput {\n pattern: string;\n path?: string | undefined;\n limit?: number | undefined;\n}\n\ninterface GlobOutput {\n files: string[];\n truncated: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.turbo'];\nconst WALK_CONCURRENCY = 16;\n\nexport const globTool: Tool<GlobInput, GlobOutput> = {\n name: 'glob',\n category: 'Filesystem',\n description:\n 'Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.',\n usageHint:\n 'RECOMMENDED FOR SCOPING SEARCHES:\\n\\n' +\n '- Use early to get a list of files you actually care about.\\n' +\n '- Combine with `path` and `limit`.\\n' +\n '- Default ignores common build/dependency directories.\\n' +\n 'Much more efficient than shell `find` for most use cases inside the agent.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'folder',\n maxOutputBytes: 65_536,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: {\n type: 'string',\n description: 'Glob pattern to match (e.g. \"**/*.ts\", \"src/**\").',\n },\n path: {\n type: 'string',\n description: 'Base directory to search from (defaults to project root).',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of results to return (default 1000, max 5000).',\n },\n },\n required: ['pattern'],\n },\n async execute(input, ctx) {\n if (!input?.pattern) throw new Error('glob: pattern is required');\n // `safeResolveReal` validates that the input base — even if symlinked —\n // resolves to a real path inside the project root (or `~/.wrongstack`).\n // Throws on escape, matching how single-file tools (`read`, `edit`,\n // `write`) reject out-of-root paths: the caller named the base explicitly.\n const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;\n const limit = Math.max(1, Math.min(input.limit ?? 1000, 5000));\n\n const ignored = await readGitignore(base);\n const re = compileGlob(input.pattern);\n\n const results: { rel: string; mtime: number }[] = [];\n let truncated = false;\n const pushResult = async (full: string): Promise<void> => {\n // Bail before stat if a concurrent worker has already filled the budget —\n // the limit is a global cap across all parallel walkers, not per-worker.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n try {\n const st = await fs.stat(full);\n // Re-check after the await: another worker may have filled the budget\n // while we were waiting on fs.stat.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n results.push({ rel: full, mtime: st.mtimeMs });\n if (results.length >= limit) truncated = true;\n } catch {\n // skip stat error\n }\n };\n const walk = async (dir: string, relPrefix: string): Promise<void> => {\n /* v8 ignore start -- the inner limit guards (file push + post-recursion return) always stop first; this re-entry guard is defensive. */\n if (results.length >= limit) {\n truncated = true;\n return;\n }\n /* v8 ignore stop */\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n const subdirs: Array<{ full: string; rel: string }> = [];\n const matchedFiles: string[] = [];\n for (const e of entries) {\n const name = e.name;\n if (DEFAULT_IGNORE.includes(name)) continue;\n if (ignored.includes(name)) continue;\n const rel = relPrefix ? `${relPrefix}/${name}` : name;\n const full = path.join(dir, name);\n if (e.isDirectory()) {\n subdirs.push({ full, rel });\n } else if (e.isFile()) {\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) {\n matchedFiles.push(full);\n }\n } else if (e.isSymbolicLink()) {\n try {\n const st = await fs.stat(full);\n if (st.isDirectory()) {\n // CWE-59 containment: a symlink inside the workspace can point\n // outside it. Before recursing into it (or including the\n // resolved file in results), realpath the symlink and verify\n // its target is still inside the project root. Skip silently\n // on escape so a single bad symlink doesn't poison the whole\n // walk.\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n subdirs.push({ full, rel });\n } else if (st.isFile()) {\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) matchedFiles.push(full);\n }\n } catch {\n // Skip broken symlink, stat error, OR out-of-root target. All\n // three should fail the walk without aborting the whole search.\n }\n }\n if (truncated) return;\n }\n await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);\n if (truncated) return;\n // Subdir walks: each one re-checks the limit at entry (re-entry guard),\n // but we also stop dispatching new walks once truncated, so siblings of\n // a hit-limit subdir don't keep adding results.\n const remainingSubdirs = truncated ? [] : subdirs;\n await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));\n };\n await walk(base, '');\n results.sort((a, b) => b.mtime - a.mtime);\n return { files: results.map((r) => r.rel), truncated };\n },\n};\n\nasync function readGitignore(dir: string): Promise<string[]> {\n try {\n const raw = await fs.readFile(path.join(dir, '.gitignore'), 'utf8');\n return raw\n .split('\\n')\n .map((l) => l.trim())\n .filter((l) => l && !l.startsWith('#'));\n } catch {\n return [];\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/_concurrency.ts","../src/_util.ts","../src/glob.ts"],"names":["fsp","path2"],"mappings":"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","file":"glob.js","sourcesContent":["/**\n * Bounded-concurrency async map.\n *\n * Runs `fn` over `items` with at most `limit` in-flight promises. Order of\n * results matches input order. Errors reject the returned promise on the first\n * failure — the remaining in-flight work is not awaited; this matches\n * `Promise.all` semantics for callers that want fail-fast behavior.\n *\n * Lives here (rather than imported from `@wrongstack/bench`) because:\n * 1. tools sits below bench in the dependency graph — bench is a consumer of\n * tools, never the other way around.\n * 2. The semantics used by grep/glob are deliberately simple (fail-fast,\n * no cancellation, no progress) and don't need bench's richer options.\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n if (items.length === 0) return [];\n const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n\n const worker = async (): Promise<void> => {\n while (true) {\n const i = nextIndex++;\n if (i >= items.length) return;\n results[i] = await fn(items[i] as T);\n }\n };\n\n await Promise.all(Array.from({ length: effectiveLimit }, worker));\n return results;\n}\n","import { createHash } from 'node:crypto';\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\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` — the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\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.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\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 // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\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 if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\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(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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 /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\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';\nimport * as path from 'node:path';\nimport { compileGlob } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { mapWithConcurrency } from './_concurrency.js';\nimport { assertRealInsideRoot, safeResolveReal } from './_util.js';\n\ninterface GlobInput {\n pattern: string;\n path?: string | undefined;\n limit?: number | undefined;\n}\n\ninterface GlobOutput {\n files: string[];\n truncated: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.turbo'];\nconst WALK_CONCURRENCY = 16;\n\nexport const globTool: Tool<GlobInput, GlobOutput> = {\n name: 'glob',\n category: 'Filesystem',\n description:\n 'Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.',\n usageHint:\n 'RECOMMENDED FOR SCOPING SEARCHES:\\n\\n' +\n '- Use early to get a list of files you actually care about.\\n' +\n '- Combine with `path` and `limit`.\\n' +\n '- Default ignores common build/dependency directories.\\n' +\n 'Much more efficient than shell `find` for most use cases inside the agent.',\n selection: {\n doNotUseWhen: 'you need to search inside file contents.',\n useInstead: ['grep'],\n },\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'folder',\n maxOutputBytes: 65_536,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: {\n type: 'string',\n description: 'Glob pattern to match (e.g. \"**/*.ts\", \"src/**\").',\n },\n path: {\n type: 'string',\n description: 'Base directory to search from (defaults to project root).',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of results to return (default 1000, max 5000).',\n },\n },\n required: ['pattern'],\n },\n async execute(input, ctx, opts) {\n if (!input?.pattern) throw new Error('glob: pattern is required');\n const signal = opts?.signal;\n // `safeResolveReal` validates that the input base — even if symlinked —\n // resolves to a real path inside the project root (or `~/.wrongstack`).\n // Throws on escape, matching how single-file tools (`read`, `edit`,\n // `write`) reject out-of-root paths: the caller named the base explicitly.\n const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;\n const limit = Math.max(1, Math.min(input.limit ?? 1000, 5000));\n\n const ignored = await readGitignore(base);\n const re = compileGlob(input.pattern);\n\n const results: { rel: string; mtime: number }[] = [];\n let truncated = false;\n const pushResult = async (full: string): Promise<void> => {\n // Bail before stat if a concurrent worker has already filled the budget —\n // the limit is a global cap across all parallel walkers, not per-worker.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n try {\n const st = await fs.stat(full);\n // Re-check after the await: another worker may have filled the budget\n // while we were waiting on fs.stat.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n results.push({ rel: full, mtime: st.mtimeMs });\n if (results.length >= limit) truncated = true;\n } catch {\n // skip stat error\n }\n };\n const walk = async (dir: string, relPrefix: string): Promise<void> => {\n // Abort check per directory: a huge tree walk must stop promptly on\n // Ctrl+C instead of running to completion with a discarded result.\n if (signal?.aborted) {\n truncated = true;\n return;\n }\n /* v8 ignore start -- the inner limit guards (file push + post-recursion return) always stop first; this re-entry guard is defensive. */\n if (results.length >= limit) {\n truncated = true;\n return;\n }\n /* v8 ignore stop */\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n const subdirs: Array<{ full: string; rel: string }> = [];\n const matchedFiles: string[] = [];\n for (const e of entries) {\n const name = e.name;\n if (DEFAULT_IGNORE.includes(name)) continue;\n if (ignored.includes(name)) continue;\n const rel = relPrefix ? `${relPrefix}/${name}` : name;\n const full = path.join(dir, name);\n if (e.isDirectory()) {\n subdirs.push({ full, rel });\n } else if (e.isFile()) {\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) {\n matchedFiles.push(full);\n }\n } else if (e.isSymbolicLink()) {\n try {\n const st = await fs.stat(full);\n if (st.isDirectory()) {\n // CWE-59 containment: a symlink inside the workspace can point\n // outside it. Before recursing into it (or including the\n // resolved file in results), realpath the symlink and verify\n // its target is still inside the project root. Skip silently\n // on escape so a single bad symlink doesn't poison the whole\n // walk.\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n subdirs.push({ full, rel });\n } else if (st.isFile()) {\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) matchedFiles.push(full);\n }\n } catch {\n // Skip broken symlink, stat error, OR out-of-root target. All\n // three should fail the walk without aborting the whole search.\n }\n }\n if (truncated) return;\n }\n await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);\n if (truncated) return;\n // Subdir walks: each one re-checks the limit at entry (re-entry guard),\n // but we also stop dispatching new walks once truncated, so siblings of\n // a hit-limit subdir don't keep adding results.\n const remainingSubdirs = truncated ? [] : subdirs;\n await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));\n };\n await walk(base, '');\n results.sort((a, b) => b.mtime - a.mtime);\n return { files: results.map((r) => r.rel), truncated };\n },\n};\n\nasync function readGitignore(dir: string): Promise<string[]> {\n try {\n const raw = await fs.readFile(path.join(dir, '.gitignore'), 'utf8');\n return raw\n .split('\\n')\n .map((l) => l.trim())\n .filter((l) => l && !l.startsWith('#'));\n } catch {\n return [];\n }\n}\n"]}
package/dist/grep.js CHANGED
@@ -106,6 +106,10 @@ var grepTool = {
106
106
  category: "Search",
107
107
  description: "Search across files using a regular expression. This is one of the primary code search tools. Prefers ripgrep for speed and features when available.",
108
108
  usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
109
+ selection: {
110
+ doNotUseWhen: "you only need to locate files by name or path pattern.",
111
+ useInstead: ["glob"]
112
+ },
109
113
  permission: "auto",
110
114
  mutating: false,
111
115
  capabilities: ["fs.read"],