@wrongstack/tools 0.273.0 → 0.273.1

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.
package/dist/pack.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import { spawn, execFileSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$1, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
5
  import { statSync, mkdirSync, createWriteStream } from 'node:fs';
6
6
  import * as fs2 from 'node:fs/promises';
7
7
  import * as path3 from 'node:path';
8
8
  import { resolve, sep, dirname, join } from 'node:path';
9
9
  import * as os2 from 'node:os';
10
- import { toErrorMessage as toErrorMessage$2 } from '@wrongstack/core/utils';
10
+ import { toErrorMessage as toErrorMessage$3 } from '@wrongstack/core/utils';
11
11
  import { createRequire } from 'node:module';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { Worker } from 'node:worker_threads';
14
14
  import * as ts from 'typescript';
15
+ import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils/error';
15
16
  import * as dns from 'node:dns/promises';
16
17
  import * as net from 'node:net';
17
18
  import { Agent } from 'undici';
@@ -660,6 +661,21 @@ function resolveWin32Command(cmd) {
660
661
  }
661
662
  return cmd;
662
663
  }
664
+ function resolvePowerShell(cmd) {
665
+ if (process.platform !== "win32") return cmd;
666
+ const lower = cmd.toLowerCase();
667
+ if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
668
+ return resolveWin32Command(cmd);
669
+ }
670
+ const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
671
+ const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
672
+ const resolved = resolveWin32Command(primary);
673
+ if (resolved !== primary) {
674
+ const fb = resolveWin32Command(fallback);
675
+ return fb === fallback ? cmd : fb;
676
+ }
677
+ return resolved;
678
+ }
663
679
  var WIN32_SHELL_META = /[&|<>\r\n\0]/;
664
680
  function assertSafeWin32ShellArgs(args) {
665
681
  for (const a of args) {
@@ -1659,6 +1675,110 @@ async function checkAndBlockKillCommand(command) {
1659
1675
  }
1660
1676
  return { blocked: false };
1661
1677
  }
1678
+ function pickShell(platform3, command, env) {
1679
+ const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
1680
+ if (override === "cmd" || override === "cmd.exe") return "cmd";
1681
+ if (override === "powershell" || override === "powershell.exe") return "powershell";
1682
+ if (override === "pwsh" || override === "pwsh.exe") return "pwsh";
1683
+ if (looksLikePowerShell(command)) return "pwsh";
1684
+ return "cmd";
1685
+ }
1686
+ function looksLikePowerShell(command) {
1687
+ if (!command) return false;
1688
+ const trimmed = command.trimStart();
1689
+ if (/\.ps1\b/i.test(trimmed)) return true;
1690
+ if (/^\s*#requires\s/i.test(trimmed)) return true;
1691
+ if (/^\s*param\s*\(/i.test(trimmed)) return true;
1692
+ if (/\$[\w:{]/i.test(trimmed)) return true;
1693
+ if (/\$\(/.test(trimmed)) return true;
1694
+ if (/@\s*['"]/.test(trimmed)) return true;
1695
+ if (/&\s+\$/.test(trimmed)) return true;
1696
+ if (/(^|\s)@\s*\(/.test(trimmed)) return true;
1697
+ if (/(^|\s)@\{/.test(trimmed)) return true;
1698
+ if (/(?:^|[\s\[\(\{,;])(?:-eq|-ne|-lt|-gt|-le|-ge|-like|-notlike|-match|-notmatch|-contains|-notcontains|-in|-notin|-and|-or|-not|-band|-bor|-bxor|-replace|-isplit|-csplit|-osplit|-join|-is|-as|-f)(?:$|[\s\]\)\},;])/i.test(trimmed)) {
1699
+ return true;
1700
+ }
1701
+ if (PS_VERB_RE.test(trimmed)) return true;
1702
+ if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps|sl|rm|cat|cp|mv)\b/i.test(trimmed)) {
1703
+ return true;
1704
+ }
1705
+ if (looksLikePowerShellExtended(command)) return true;
1706
+ return false;
1707
+ }
1708
+ function looksLikePowerShellExtended(command) {
1709
+ if (!command) return false;
1710
+ const trimmed = command.trimStart();
1711
+ if (/(?:^|\s)[-/](?:WhatIf|Confirm|ErrorAction)(?::[^\s]+|\s|=|$)/i.test(trimmed)) {
1712
+ return true;
1713
+ }
1714
+ if (/(?:^|[\s;&|])(Where-Object|ForEach-Object|Select-Object|Sort-Object|Group-Object|Measure-Object|Compare-Object|Tee-Object)(?:\s|$)/i.test(trimmed)) {
1715
+ return true;
1716
+ }
1717
+ if (/\bWrite-(?:Host|Output|Error|Warning|Verbose|Debug|Information)(?:\s|$)/i.test(trimmed)) {
1718
+ return true;
1719
+ }
1720
+ if (/HK(?:LM|CU|CR|U|CC|DD|PD):\\/i.test(trimmed)) return true;
1721
+ if (/\[(?:string|int|bool|xml|double|float|decimal|char|byte|long|System\.)/i.test(trimmed)) {
1722
+ return true;
1723
+ }
1724
+ if (/^\s*<#|#>\s*$/m.test(trimmed)) return true;
1725
+ if (/(?:^|\s)[-/\/](?:AsPlainText|PipelineVariable|pv|FilterHashtable|OutVariable|ov)(?:\s|=|$)/i.test(trimmed)) {
1726
+ return true;
1727
+ }
1728
+ return false;
1729
+ }
1730
+ function wrapPowerShellScript(command) {
1731
+ const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
1732
+ return "\uFEFF" + bootstrap + "\ntry {\n" + command + "\n} finally { exit $LASTEXITCODE }";
1733
+ }
1734
+ var PS_VERB_RE = new RegExp(
1735
+ // Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
1736
+ "(?:^|[\\s;&|\\(\\{,])(?:Get|Set|New|Remove|Add|Clear|Copy|Move|Rename|Test|Update|Write|Read|Push|Pop|Invoke|Start|Stop|Wait|Out|Format|Group|Measure|Compare|Resolve|ConvertTo|ConvertFrom|Convert|Import|Export|Select|Where|ForEach|Sort|Tee|Split|Join|Limit|Skip|Step|Trace|Debug|Register|Unregister|Enable|Disable|Restart|Suspend|Resume|Save|Open|Close|Lock|Unlock|Mount|Dismount|Enter|Exit|Use|Show|Hide|Find|Search|Watch|Initialize|Optimize|Compress|Expand|Merge|Checkpoint|Undo|Redo|Approve|Deny|Block|Grant|Revoke|Assert|Confirm|Receive|Send|Connect|Disconnect|Reset|Backup|Restore|Publish|Unpublish|Install|Uninstall|Build|Rebuild|Deploy|Submit|Process|Complete|Approve|Revoke|Pay|Refund|Decline|Receive|Send)-[A-Za-z][A-Za-z0-9]+(?:[\\-\\+][A-Za-z][A-Za-z0-9]+)*(?:$|[\\s\\-\\;\\&\\|\\(\\)\\{\\},])",
1737
+ "i"
1738
+ );
1739
+ function shellArgs(shell) {
1740
+ if (shell === "powershell" || shell === "pwsh") {
1741
+ return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "-"];
1742
+ }
1743
+ return ["/c"];
1744
+ }
1745
+ function diagnoseBashism(command, shell) {
1746
+ if (!command) return void 0;
1747
+ const isCmd = shell === "cmd";
1748
+ const hints = [];
1749
+ const add = (h) => {
1750
+ if (!hints.includes(h)) hints.push(h);
1751
+ };
1752
+ if (/\/dev\/null/.test(command)) {
1753
+ add(
1754
+ isCmd ? "use `nul` instead of `/dev/null` (e.g. `2>nul`)" : "use `$null` instead of `/dev/null` (e.g. `2>$null`)"
1755
+ );
1756
+ }
1757
+ if (/(^|[;&|]\s*)export\s+[A-Za-z_]\w*=/.test(command)) {
1758
+ add(
1759
+ isCmd ? "set env vars with `set NAME=value`, not `export`" : "set env vars with `$env:NAME = 'value'`, not `export`"
1760
+ );
1761
+ }
1762
+ if (/<<-?\s*['"]?[A-Za-z_]\w*/.test(command)) {
1763
+ add(
1764
+ isCmd ? "cmd has no heredocs \u2014 write the content to a file or use multiple `echo` lines" : "PowerShell has no heredocs \u2014 use a single-quoted here-string `@'\u2026'@` (closing `'@` at column 0)"
1765
+ );
1766
+ }
1767
+ if (shell === "powershell" && /(&&|\|\|)/.test(command)) {
1768
+ add("Windows PowerShell 5.1 has no `&&`/`||` \u2014 separate commands with `;` (check `$LASTEXITCODE`)");
1769
+ }
1770
+ if (/\brm\s+-[A-Za-z]*[rf]/.test(command)) {
1771
+ add(
1772
+ isCmd ? "`rm` is not a cmd builtin \u2014 use `del` (files) or `rmdir /s /q` (dirs)" : "use `Remove-Item -Recurse -Force` \u2014 the `rm -rf` bash flags don't exist in PowerShell"
1773
+ );
1774
+ }
1775
+ if (/\bwhich\s+\S/.test(command)) {
1776
+ add(isCmd ? "use `where <cmd>` instead of `which`" : "use `Get-Command <cmd>` instead of `which`");
1777
+ }
1778
+ if (hints.length === 0) return void 0;
1779
+ const label = isCmd ? "cmd.exe" : shell === "pwsh" ? "PowerShell 7" : "Windows PowerShell";
1780
+ return `[wrongstack] This command failed and contains bash/POSIX syntax that ${label} does not accept \u2014 ${hints.join("; ")}. Rewrite it in ${isCmd ? "cmd" : "PowerShell"} syntax and retry.`;
1781
+ }
1662
1782
 
1663
1783
  // src/bash.ts
1664
1784
  var MAX_OUTPUT = 32768;
@@ -1755,18 +1875,36 @@ var bashTool = {
1755
1875
  }
1756
1876
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1757
1877
  const isWin3 = os2.platform() === "win32";
1758
- const shell = (() => {
1759
- const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
1760
- if (explicit) return explicit;
1761
- if (isWin3) return process.env["COMSPEC"] ?? "cmd.exe";
1762
- const fromEnv = process.env["SHELL"];
1763
- if (fromEnv) {
1764
- const name = fromEnv.split("/").pop() ?? "";
1765
- if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) return fromEnv;
1766
- }
1767
- return "/bin/bash";
1768
- })();
1769
- const args = isWin3 ? ["/c", input.command] : ["-c", input.command];
1878
+ let plan;
1879
+ let winShellKind;
1880
+ if (isWin3) {
1881
+ const shell2 = pickShell("win32", input.command, {
1882
+ get: (k) => process.env[k]
1883
+ });
1884
+ winShellKind = shell2;
1885
+ const bin = shell2 === "powershell" ? resolvePowerShell("powershell.exe") : shell2 === "pwsh" ? resolvePowerShell("pwsh.exe") : process.env["COMSPEC"] ?? "cmd.exe";
1886
+ plan = {
1887
+ bin,
1888
+ argv: shellArgs(shell2),
1889
+ useStdin: shell2 === "powershell" || shell2 === "pwsh",
1890
+ stdinBody: shell2 === "powershell" || shell2 === "pwsh" ? wrapPowerShellScript(input.command) : void 0
1891
+ };
1892
+ } else {
1893
+ const explicit = process.env["WRONGSTACK_SHELL"];
1894
+ let bin;
1895
+ if (explicit) bin = explicit;
1896
+ else {
1897
+ const fromEnv = process.env["SHELL"];
1898
+ if (fromEnv) {
1899
+ const name = fromEnv.split("/").pop() ?? "";
1900
+ if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) bin = fromEnv;
1901
+ else bin = "/bin/bash";
1902
+ } else bin = "/bin/bash";
1903
+ }
1904
+ plan = { bin, argv: ["-c"], useStdin: false, stdinBody: void 0 };
1905
+ }
1906
+ const shell = plan.bin;
1907
+ const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
1770
1908
  const env = buildChildEnv(ctx.session?.id);
1771
1909
  const detached = !isWin3;
1772
1910
  const startedAt = Date.now();
@@ -1776,7 +1914,9 @@ var bashTool = {
1776
1914
  const child2 = spawn(shell, args, {
1777
1915
  cwd: ctx.projectRoot,
1778
1916
  env,
1779
- stdio: ["ignore", "pipe", "pipe"],
1917
+ // PowerShell takes the script on stdin (no argv quoting); cmd.exe
1918
+ // and POSIX shells ignore stdin when given the command inline.
1919
+ stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
1780
1920
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
1781
1921
  // DETACHED_PROCESS (detached: true) is set, so the console-less
1782
1922
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -1787,6 +1927,13 @@ var bashTool = {
1787
1927
  detached: !isWin3,
1788
1928
  windowsHide: true
1789
1929
  });
1930
+ if (plan.useStdin) {
1931
+ try {
1932
+ child2.stdin?.write(plan.stdinBody ?? input.command);
1933
+ child2.stdin?.end();
1934
+ } catch {
1935
+ }
1936
+ }
1790
1937
  const pid2 = child2.pid;
1791
1938
  if (typeof pid2 === "number") {
1792
1939
  registry.register({
@@ -1841,11 +1988,20 @@ var bashTool = {
1841
1988
  const child = spawn(shell, args, {
1842
1989
  cwd: ctx.projectRoot,
1843
1990
  env,
1844
- stdio: ["ignore", "pipe", "pipe"],
1991
+ // PowerShell takes the script on stdin (no argv quoting); cmd.exe
1992
+ // and POSIX shells ignore stdin when given the command inline.
1993
+ stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
1845
1994
  detached,
1846
1995
  windowsHide: true,
1847
1996
  ...isWin3 ? {} : { signal: opts.signal }
1848
1997
  });
1998
+ if (plan.useStdin) {
1999
+ try {
2000
+ child.stdin?.write(plan.stdinBody ?? input.command);
2001
+ child.stdin?.end();
2002
+ } catch {
2003
+ }
2004
+ }
1849
2005
  const pid = child.pid;
1850
2006
  if (typeof pid === "number") {
1851
2007
  registry.register({
@@ -1996,10 +2152,13 @@ var bashTool = {
1996
2152
  yield { type: "partial_output", text: remainder };
1997
2153
  }
1998
2154
  const spooled = spool.finalize();
2155
+ const hint = !timedOut && typeof c.code === "number" && c.code !== 0 && winShellKind ? diagnoseBashism(input.command, winShellKind) : void 0;
1999
2156
  yield {
2000
2157
  type: "final",
2001
2158
  output: {
2002
- output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
2159
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
2160
+
2161
+ ${hint}` : ""),
2003
2162
  exit_code: c.code,
2004
2163
  timed_out: timedOut
2005
2164
  }
@@ -2368,7 +2527,7 @@ function loadDatabaseSync() {
2368
2527
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
2369
2528
  } catch (err) {
2370
2529
  throw new Error(
2371
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage$2(err)}`
2530
+ `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage$3(err)}`
2372
2531
  );
2373
2532
  }
2374
2533
  return DatabaseSyncCtor;
@@ -5385,49 +5544,71 @@ function findSimilarity(haystack, needle) {
5385
5544
  return line;
5386
5545
  }
5387
5546
  var isWin2 = process.platform === "win32";
5388
- var ALLOWED_COMMANDS = {
5389
- node: ["--version", "-r", "--input-type=module"],
5390
- npm: ["--version", "list", "pkg", "doctor", "view", "outdated", "audit"],
5391
- pnpm: ["--version", "remove", "list", "view", "outdated", "audit"],
5392
- npx: ["--version"],
5393
- git: [
5394
- "--version",
5395
- "status",
5396
- "log",
5397
- "diff",
5398
- "branch",
5399
- "checkout",
5400
- "stash",
5401
- "add",
5402
- "commit",
5403
- "push",
5404
- "pull"
5405
- ],
5406
- ls: ["-la", "-l", "-a"],
5407
- cat: [],
5408
- head: ["-n"],
5409
- tail: ["-n"],
5410
- wc: ["-l", "-w", "-c"],
5411
- grep: [],
5412
- find: [],
5413
- echo: [],
5414
- mkdir: ["-p"],
5415
- cp: ["-r"],
5416
- mv: [],
5417
- rm: ["-rf"],
5418
- touch: [],
5419
- bun: ["--version"],
5420
- tsc: ["--version", "--noEmit", "--project"],
5421
- vitest: ["--version", "run", "--coverage"],
5422
- biome: ["--version", "lint", "format", "check"],
5423
- cargo: ["--version", "build", "test", "check"],
5424
- rustc: ["--version"],
5425
- go: ["version", "run", "build", "test"],
5426
- python: ["--version"],
5427
- pip: ["--version", "list"],
5428
- docker: ["--version", "ps", "images"],
5429
- kubectl: ["version", "get", "describe", "logs"]
5430
- };
5547
+ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
5548
+ // JS / TS toolchain
5549
+ "node",
5550
+ "npm",
5551
+ "pnpm",
5552
+ "yarn",
5553
+ "npx",
5554
+ "bun",
5555
+ "deno",
5556
+ "tsc",
5557
+ "vitest",
5558
+ "jest",
5559
+ "biome",
5560
+ "eslint",
5561
+ "prettier",
5562
+ // version control
5563
+ "git",
5564
+ // Rust
5565
+ "cargo",
5566
+ "rustc",
5567
+ // Go
5568
+ "go",
5569
+ // Python
5570
+ "python",
5571
+ "python3",
5572
+ "pip",
5573
+ "pip3",
5574
+ // Ruby
5575
+ "ruby",
5576
+ "gem",
5577
+ "bundle",
5578
+ // JVM
5579
+ "java",
5580
+ "javac",
5581
+ "mvn",
5582
+ "gradle",
5583
+ "gradlew",
5584
+ // .NET
5585
+ "dotnet",
5586
+ // C / C++ / native build
5587
+ "make",
5588
+ "cmake",
5589
+ // containers / orchestration (read-only subcommands; see BLOCKED_ARG_PATTERNS)
5590
+ "docker",
5591
+ "kubectl",
5592
+ // common POSIX file/text utilities
5593
+ "ls",
5594
+ "cat",
5595
+ "head",
5596
+ "tail",
5597
+ "wc",
5598
+ "grep",
5599
+ "find",
5600
+ "echo",
5601
+ "mkdir",
5602
+ "cp",
5603
+ "mv",
5604
+ "rm",
5605
+ "touch"
5606
+ ]);
5607
+ var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
5608
+ var normalizeCmd = (c) => c.trim();
5609
+ function isExecCommandAllowed(cmd) {
5610
+ return allowedCommands.has(normalizeCmd(cmd));
5611
+ }
5431
5612
  var MAX_ARGS = 20;
5432
5613
  var MAX_OUTPUT2 = 2e5;
5433
5614
  var DEFAULT_TIMEOUT_MS2 = 3e4;
@@ -5489,7 +5670,7 @@ var execTool = {
5489
5670
  name: "exec",
5490
5671
  category: "Shell",
5491
5672
  description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
5492
- usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be one of the allowed commands (node, npm, pnpm, git, tsc, eslint, vitest, etc.).\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- For anything that requires real shell features (pipes, complex redirection, arbitrary commands), fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
5673
+ usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
5493
5674
  permission: "confirm",
5494
5675
  mutating: true,
5495
5676
  riskTier: "standard",
@@ -5543,12 +5724,12 @@ var execTool = {
5543
5724
  truncated: false,
5544
5725
  allowed: false
5545
5726
  };
5546
- if (!(cmd in ALLOWED_COMMANDS)) {
5727
+ if (!isExecCommandAllowed(cmd)) {
5547
5728
  return {
5548
5729
  command: cmd,
5549
5730
  args: input.args ?? [],
5550
5731
  stdout: "",
5551
- stderr: `Command "${cmd}" not in allowlist. Use the bash tool for arbitrary commands.`,
5732
+ stderr: `Command "${cmd}" not in allowlist. Add it to your ~/.wrongstack/config.json under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
5552
5733
  exitCode: 1,
5553
5734
  truncated: false,
5554
5735
  allowed: false
@@ -5591,19 +5772,58 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5591
5772
  let stdout = "";
5592
5773
  let stderr = "";
5593
5774
  let killed = false;
5775
+ const resolvedOnce = { value: false };
5776
+ const finish = (result) => {
5777
+ if (resolvedOnce.value) return;
5778
+ resolvedOnce.value = true;
5779
+ resolve6(result);
5780
+ };
5594
5781
  const startedAt = Date.now();
5595
5782
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
5596
5783
  const resolved = resolveWin32Command(cmd);
5597
5784
  const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5598
5785
  const spawnCmd = needsShell ? cmd : resolved;
5599
5786
  if (needsShell) assertSafeWin32ShellArgs(args);
5600
- const child = spawn(spawnCmd, args, {
5601
- cwd,
5602
- env: buildChildEnv(sessionId),
5603
- stdio: ["ignore", "pipe", "pipe"],
5604
- windowsHide: true,
5605
- ...isWin2 ? {} : { signal },
5606
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
5787
+ let child;
5788
+ try {
5789
+ child = spawn(spawnCmd, args, {
5790
+ cwd,
5791
+ env: buildChildEnv(sessionId),
5792
+ stdio: ["ignore", "pipe", "pipe"],
5793
+ windowsHide: true,
5794
+ ...isWin2 ? {} : { signal },
5795
+ ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
5796
+ });
5797
+ } catch (err) {
5798
+ spool.finalize();
5799
+ finish({
5800
+ command: cmd,
5801
+ args,
5802
+ stdout: "",
5803
+ stderr: `spawn failed: ${toErrorMessage$1(err)}`,
5804
+ exitCode: 1,
5805
+ truncated: false,
5806
+ allowed: true
5807
+ });
5808
+ return;
5809
+ }
5810
+ child.on("error", (err) => {
5811
+ const isAbort = err && err.code === "ABORT_ERR";
5812
+ const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
5813
+ clearTimeout(timer);
5814
+ if (isWin2) signal.removeEventListener("abort", onAbort);
5815
+ if (typeof pid === "number") registry.unregister(pid);
5816
+ registry.afterCall(Date.now() - startedAt, true);
5817
+ spool.finalize();
5818
+ finish({
5819
+ command: cmd,
5820
+ args,
5821
+ stdout: normalizeCommandOutput(stdout),
5822
+ stderr: stderrText,
5823
+ exitCode: isAbort ? 124 : 1,
5824
+ truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
5825
+ allowed: true
5826
+ });
5607
5827
  });
5608
5828
  const registry = getProcessRegistry();
5609
5829
  const pid = child.pid;
@@ -5643,7 +5863,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5643
5863
  const exitCode = killed ? 124 : code ?? 1;
5644
5864
  registry.afterCall(durationMs, exitCode !== 0);
5645
5865
  const spooled = spool.finalize();
5646
- resolve6({
5866
+ finish({
5647
5867
  command: cmd,
5648
5868
  args,
5649
5869
  stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
@@ -5653,22 +5873,6 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5653
5873
  allowed: true
5654
5874
  });
5655
5875
  });
5656
- child.on("error", (err) => {
5657
- clearTimeout(timer);
5658
- if (isWin2) signal.removeEventListener("abort", onAbort);
5659
- if (typeof pid === "number") registry.unregister(pid);
5660
- registry.afterCall(Date.now() - startedAt, true);
5661
- spool.finalize();
5662
- resolve6({
5663
- command: cmd,
5664
- args,
5665
- stdout: normalizeCommandOutput(stdout),
5666
- stderr: err.message,
5667
- exitCode: 1,
5668
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
5669
- allowed: true
5670
- });
5671
- });
5672
5876
  });
5673
5877
  }
5674
5878
  var TD = new TurndownService({
@@ -7868,7 +8072,7 @@ var readTool = {
7868
8072
  } catch (err) {
7869
8073
  const code = err.code;
7870
8074
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
7871
- throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage$1(err)}`);
8075
+ throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage$2(err)}`);
7872
8076
  }
7873
8077
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
7874
8078
  if (stat11.size > MAX_BYTES2) {
@@ -8464,7 +8668,7 @@ async function duckduckgoSearch(query2, num, signal) {
8464
8668
  truncated: results.length >= num
8465
8669
  };
8466
8670
  } catch (err) {
8467
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$2(err) }));
8671
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$3(err) }));
8468
8672
  return {
8469
8673
  query: query2,
8470
8674
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -8628,7 +8832,7 @@ var setWorkingDirTool = {
8628
8832
  } catch (err) {
8629
8833
  return {
8630
8834
  current: ctx.workingDir,
8631
- error: toErrorMessage$2(err)
8835
+ error: toErrorMessage$3(err)
8632
8836
  };
8633
8837
  }
8634
8838
  try {