@wrongstack/tools 0.272.2 → 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/builtin.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, 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$1 } 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) {
@@ -1084,6 +1100,21 @@ function parseAuditOutput(json, exitCode) {
1084
1100
  }
1085
1101
  }
1086
1102
  var REGISTRY_FILE = ".wrongstack/process-registry.json";
1103
+ function toErrorMessage(err) {
1104
+ return err instanceof Error ? err.message : String(err);
1105
+ }
1106
+ function emitStructuredLog(level, event, message, error) {
1107
+ const payload = {
1108
+ level,
1109
+ event,
1110
+ message,
1111
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1112
+ };
1113
+ if (error !== void 0) {
1114
+ payload.error = toErrorMessage(error);
1115
+ }
1116
+ console.log(JSON.stringify(payload));
1117
+ }
1087
1118
  var HEARTBEAT_INTERVAL_MS = 5e3;
1088
1119
  var STALE_THRESHOLD_MS = 3e4;
1089
1120
  var LOCKFILE = ".wrongstack/.process-registry.lock";
@@ -1093,6 +1124,9 @@ function generateInstanceId() {
1093
1124
  const random = Math.random().toString(36).slice(2, 8);
1094
1125
  return `${hostname2}:${pid}:${random}`;
1095
1126
  }
1127
+ function isNodeError(err) {
1128
+ return typeof err === "object" && err !== null && "code" in err;
1129
+ }
1096
1130
  async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1097
1131
  const start = Date.now();
1098
1132
  const pidStr = String(process.pid);
@@ -1107,7 +1141,7 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1107
1141
  }
1108
1142
  };
1109
1143
  } catch (err) {
1110
- if (err.code === "EEXIST") {
1144
+ if (isNodeError(err) && err.code === "EEXIST") {
1111
1145
  try {
1112
1146
  const content = await fs2.readFile(lockfilePath, "utf-8");
1113
1147
  const parts = content.split(":");
@@ -1144,7 +1178,7 @@ async function readRegistryFile(filePath) {
1144
1178
  }
1145
1179
  return parsed;
1146
1180
  } catch (err) {
1147
- if (err.code === "ENOENT") {
1181
+ if (isNodeError(err) && err.code === "ENOENT") {
1148
1182
  return {
1149
1183
  version: 1,
1150
1184
  instances: /* @__PURE__ */ new Map(),
@@ -1180,7 +1214,7 @@ var PersistentProcessRegistry = class {
1180
1214
  this.lockPath = path3.join(homeDir, LOCKFILE);
1181
1215
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
1182
1216
  this.ensureDirectory().catch((err) => {
1183
- console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
1217
+ emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
1184
1218
  });
1185
1219
  }
1186
1220
  async ensureDirectory() {
@@ -1188,7 +1222,7 @@ var PersistentProcessRegistry = class {
1188
1222
  try {
1189
1223
  await fs2.mkdir(dir, { recursive: true });
1190
1224
  } catch (err) {
1191
- if (err.code !== "EEXIST") throw err;
1225
+ if (!isNodeError(err) || err.code !== "EEXIST") throw err;
1192
1226
  }
1193
1227
  }
1194
1228
  /**
@@ -1268,6 +1302,7 @@ var PersistentProcessRegistry = class {
1268
1302
  try {
1269
1303
  const data = await readRegistryFile(this.registryPath);
1270
1304
  data.instances.set(String(entry.pid), entry);
1305
+ const child = null;
1271
1306
  this.baseRegistry.register({
1272
1307
  pid: entry.pid,
1273
1308
  name: entry.name,
@@ -1275,8 +1310,7 @@ var PersistentProcessRegistry = class {
1275
1310
  startedAt: entry.startedAt,
1276
1311
  sessionId: entry.sessionId,
1277
1312
  protected: entry.protected,
1278
- child: null
1279
- // Main process has no child handle
1313
+ child
1280
1314
  });
1281
1315
  await writeRegistryFile(this.registryPath, data);
1282
1316
  } finally {
@@ -1324,7 +1358,7 @@ var PersistentProcessRegistry = class {
1324
1358
  data.lastCleanup = now;
1325
1359
  await writeRegistryFile(this.registryPath, data);
1326
1360
  } catch (err) {
1327
- console.error("PersistentProcessRegistry: sync failed", err);
1361
+ emitStructuredLog("warn", "process_registry.sync_failed", "PersistentProcessRegistry: sync failed", err);
1328
1362
  } finally {
1329
1363
  await release();
1330
1364
  }
@@ -1345,7 +1379,11 @@ var PersistentProcessRegistry = class {
1345
1379
  if (process.platform !== "win32") {
1346
1380
  process.kill(entry.pid, 0);
1347
1381
  } else {
1348
- console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
1382
+ emitStructuredLog(
1383
+ "debug",
1384
+ "process_registry.stale_pid_check",
1385
+ `PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`
1386
+ );
1349
1387
  }
1350
1388
  } catch {
1351
1389
  stalePids.push(_pidStr);
@@ -1359,7 +1397,7 @@ var PersistentProcessRegistry = class {
1359
1397
  await writeRegistryFile(this.registryPath, data);
1360
1398
  }
1361
1399
  } catch (err) {
1362
- console.error("PersistentProcessRegistry: cleanup failed", err);
1400
+ emitStructuredLog("warn", "process_registry.cleanup_failed", "PersistentProcessRegistry: cleanup failed", err);
1363
1401
  } finally {
1364
1402
  await release();
1365
1403
  }
@@ -1582,7 +1620,7 @@ async function isKillProtected(kill) {
1582
1620
  const entries = await getProtectedEntries();
1583
1621
  const killNameLower = kill.name.toLowerCase();
1584
1622
  for (const entry of entries) {
1585
- if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
1623
+ if (entry.name?.toLowerCase().includes(killNameLower)) {
1586
1624
  return true;
1587
1625
  }
1588
1626
  }
@@ -1637,6 +1675,110 @@ async function checkAndBlockKillCommand(command) {
1637
1675
  }
1638
1676
  return { blocked: false };
1639
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
+ }
1640
1782
 
1641
1783
  // src/bash.ts
1642
1784
  var MAX_OUTPUT = 32768;
@@ -1733,18 +1875,36 @@ var bashTool = {
1733
1875
  }
1734
1876
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1735
1877
  const isWin3 = os2.platform() === "win32";
1736
- const shell = (() => {
1737
- const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
1738
- if (explicit) return explicit;
1739
- if (isWin3) return process.env["COMSPEC"] ?? "cmd.exe";
1740
- const fromEnv = process.env["SHELL"];
1741
- if (fromEnv) {
1742
- const name = fromEnv.split("/").pop() ?? "";
1743
- if (["bash", "zsh", "sh", "dash", "fish"].includes(name)) return fromEnv;
1744
- }
1745
- return "/bin/bash";
1746
- })();
1747
- 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];
1748
1908
  const env = buildChildEnv(ctx.session?.id);
1749
1909
  const detached = !isWin3;
1750
1910
  const startedAt = Date.now();
@@ -1754,7 +1914,9 @@ var bashTool = {
1754
1914
  const child2 = spawn(shell, args, {
1755
1915
  cwd: ctx.projectRoot,
1756
1916
  env,
1757
- 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"],
1758
1920
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
1759
1921
  // DETACHED_PROCESS (detached: true) is set, so the console-less
1760
1922
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -1765,6 +1927,13 @@ var bashTool = {
1765
1927
  detached: !isWin3,
1766
1928
  windowsHide: true
1767
1929
  });
1930
+ if (plan.useStdin) {
1931
+ try {
1932
+ child2.stdin?.write(plan.stdinBody ?? input.command);
1933
+ child2.stdin?.end();
1934
+ } catch {
1935
+ }
1936
+ }
1768
1937
  const pid2 = child2.pid;
1769
1938
  if (typeof pid2 === "number") {
1770
1939
  registry.register({
@@ -1819,11 +1988,20 @@ var bashTool = {
1819
1988
  const child = spawn(shell, args, {
1820
1989
  cwd: ctx.projectRoot,
1821
1990
  env,
1822
- 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"],
1823
1994
  detached,
1824
1995
  windowsHide: true,
1825
1996
  ...isWin3 ? {} : { signal: opts.signal }
1826
1997
  });
1998
+ if (plan.useStdin) {
1999
+ try {
2000
+ child.stdin?.write(plan.stdinBody ?? input.command);
2001
+ child.stdin?.end();
2002
+ } catch {
2003
+ }
2004
+ }
1827
2005
  const pid = child.pid;
1828
2006
  if (typeof pid === "number") {
1829
2007
  registry.register({
@@ -1974,10 +2152,13 @@ var bashTool = {
1974
2152
  yield { type: "partial_output", text: remainder };
1975
2153
  }
1976
2154
  const spooled = spool.finalize();
2155
+ const hint = !timedOut && typeof c.code === "number" && c.code !== 0 && winShellKind ? diagnoseBashism(input.command, winShellKind) : void 0;
1977
2156
  yield {
1978
2157
  type: "final",
1979
2158
  output: {
1980
- output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
2159
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
2160
+
2161
+ ${hint}` : ""),
1981
2162
  exit_code: c.code,
1982
2163
  timed_out: timedOut
1983
2164
  }
@@ -2346,7 +2527,7 @@ function loadDatabaseSync() {
2346
2527
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
2347
2528
  } catch (err) {
2348
2529
  throw new Error(
2349
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage$1(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)}`
2350
2531
  );
2351
2532
  }
2352
2533
  return DatabaseSyncCtor;
@@ -3683,11 +3864,15 @@ async function tryNativeParse(file, content) {
3683
3864
  const crateDir = path3.join(toolsDir, "syn-parser");
3684
3865
  const tmpFile = path3.join(crateDir, "src", "input.rs");
3685
3866
  await fs2.writeFile(tmpFile, content, "utf8");
3686
- const proc = spawn("cargo", ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")], {
3687
- cwd: process.cwd(),
3688
- stdio: ["pipe", "pipe", "pipe"],
3689
- windowsHide: true
3690
- });
3867
+ const proc = spawn(
3868
+ "cargo",
3869
+ ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")],
3870
+ {
3871
+ cwd: process.cwd(),
3872
+ stdio: ["pipe", "pipe", "pipe"],
3873
+ windowsHide: true
3874
+ }
3875
+ );
3691
3876
  let stdout = "";
3692
3877
  proc.stdout?.on("data", (chunk) => {
3693
3878
  stdout += chunk.toString();
@@ -5359,49 +5544,71 @@ function findSimilarity(haystack, needle) {
5359
5544
  return line;
5360
5545
  }
5361
5546
  var isWin2 = process.platform === "win32";
5362
- var ALLOWED_COMMANDS = {
5363
- node: ["--version", "-r", "--input-type=module"],
5364
- npm: ["--version", "list", "pkg", "doctor", "view", "outdated", "audit"],
5365
- pnpm: ["--version", "remove", "list", "view", "outdated", "audit"],
5366
- npx: ["--version"],
5367
- git: [
5368
- "--version",
5369
- "status",
5370
- "log",
5371
- "diff",
5372
- "branch",
5373
- "checkout",
5374
- "stash",
5375
- "add",
5376
- "commit",
5377
- "push",
5378
- "pull"
5379
- ],
5380
- ls: ["-la", "-l", "-a"],
5381
- cat: [],
5382
- head: ["-n"],
5383
- tail: ["-n"],
5384
- wc: ["-l", "-w", "-c"],
5385
- grep: [],
5386
- find: [],
5387
- echo: [],
5388
- mkdir: ["-p"],
5389
- cp: ["-r"],
5390
- mv: [],
5391
- rm: ["-rf"],
5392
- touch: [],
5393
- bun: ["--version"],
5394
- tsc: ["--version", "--noEmit", "--project"],
5395
- vitest: ["--version", "run", "--coverage"],
5396
- biome: ["--version", "lint", "format", "check"],
5397
- cargo: ["--version", "build", "test", "check"],
5398
- rustc: ["--version"],
5399
- go: ["version", "run", "build", "test"],
5400
- python: ["--version"],
5401
- pip: ["--version", "list"],
5402
- docker: ["--version", "ps", "images"],
5403
- kubectl: ["version", "get", "describe", "logs"]
5404
- };
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
+ }
5405
5612
  var MAX_ARGS = 20;
5406
5613
  var MAX_OUTPUT2 = 2e5;
5407
5614
  var DEFAULT_TIMEOUT_MS2 = 3e4;
@@ -5463,7 +5670,7 @@ var execTool = {
5463
5670
  name: "exec",
5464
5671
  category: "Shell",
5465
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.",
5466
- 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.",
5467
5674
  permission: "confirm",
5468
5675
  mutating: true,
5469
5676
  riskTier: "standard",
@@ -5517,12 +5724,12 @@ var execTool = {
5517
5724
  truncated: false,
5518
5725
  allowed: false
5519
5726
  };
5520
- if (!(cmd in ALLOWED_COMMANDS)) {
5727
+ if (!isExecCommandAllowed(cmd)) {
5521
5728
  return {
5522
5729
  command: cmd,
5523
5730
  args: input.args ?? [],
5524
5731
  stdout: "",
5525
- 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.`,
5526
5733
  exitCode: 1,
5527
5734
  truncated: false,
5528
5735
  allowed: false
@@ -5565,19 +5772,58 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5565
5772
  let stdout = "";
5566
5773
  let stderr = "";
5567
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
+ };
5568
5781
  const startedAt = Date.now();
5569
5782
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
5570
5783
  const resolved = resolveWin32Command(cmd);
5571
5784
  const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5572
5785
  const spawnCmd = needsShell ? cmd : resolved;
5573
5786
  if (needsShell) assertSafeWin32ShellArgs(args);
5574
- const child = spawn(spawnCmd, args, {
5575
- cwd,
5576
- env: buildChildEnv(sessionId),
5577
- stdio: ["ignore", "pipe", "pipe"],
5578
- windowsHide: true,
5579
- ...isWin2 ? {} : { signal },
5580
- ...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
+ });
5581
5827
  });
5582
5828
  const registry = getProcessRegistry();
5583
5829
  const pid = child.pid;
@@ -5617,7 +5863,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5617
5863
  const exitCode = killed ? 124 : code ?? 1;
5618
5864
  registry.afterCall(durationMs, exitCode !== 0);
5619
5865
  const spooled = spool.finalize();
5620
- resolve6({
5866
+ finish({
5621
5867
  command: cmd,
5622
5868
  args,
5623
5869
  stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
@@ -5627,22 +5873,6 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
5627
5873
  allowed: true
5628
5874
  });
5629
5875
  });
5630
- child.on("error", (err) => {
5631
- clearTimeout(timer);
5632
- if (isWin2) signal.removeEventListener("abort", onAbort);
5633
- if (typeof pid === "number") registry.unregister(pid);
5634
- registry.afterCall(Date.now() - startedAt, true);
5635
- spool.finalize();
5636
- resolve6({
5637
- command: cmd,
5638
- args,
5639
- stdout: normalizeCommandOutput(stdout),
5640
- stderr: err.message,
5641
- exitCode: 1,
5642
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
5643
- allowed: true
5644
- });
5645
- });
5646
5876
  });
5647
5877
  }
5648
5878
  var TD = new TurndownService({
@@ -7842,7 +8072,7 @@ var readTool = {
7842
8072
  } catch (err) {
7843
8073
  const code = err.code;
7844
8074
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
7845
- throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
8075
+ throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage$2(err)}`);
7846
8076
  }
7847
8077
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
7848
8078
  if (stat11.size > MAX_BYTES2) {
@@ -8017,7 +8247,7 @@ var replaceTool = {
8017
8247
  if (err.code === "ENOENT") return null;
8018
8248
  throw err;
8019
8249
  });
8020
- if (!lstat2 || !lstat2.isFile()) continue;
8250
+ if (!lstat2?.isFile()) continue;
8021
8251
  if (lstat2.isSymbolicLink()) continue;
8022
8252
  let realPath;
8023
8253
  try {
@@ -8028,7 +8258,7 @@ var replaceTool = {
8028
8258
  const rel = path3.relative(realRoot, realPath);
8029
8259
  if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
8030
8260
  const stat11 = await fs2.stat(realPath).catch(() => null);
8031
- if (!stat11 || !stat11.isFile()) continue;
8261
+ if (!stat11?.isFile()) continue;
8032
8262
  let content;
8033
8263
  try {
8034
8264
  const buf = await fs2.readFile(realPath);
@@ -8438,7 +8668,7 @@ async function duckduckgoSearch(query2, num, signal) {
8438
8668
  truncated: results.length >= num
8439
8669
  };
8440
8670
  } catch (err) {
8441
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
8671
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$3(err) }));
8442
8672
  return {
8443
8673
  query: query2,
8444
8674
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -8602,7 +8832,7 @@ var setWorkingDirTool = {
8602
8832
  } catch (err) {
8603
8833
  return {
8604
8834
  current: ctx.workingDir,
8605
- error: toErrorMessage$1(err)
8835
+ error: toErrorMessage$3(err)
8606
8836
  };
8607
8837
  }
8608
8838
  try {
@@ -8752,7 +8982,11 @@ var taskTool = {
8752
8982
  const newIds = new Set(input.tasks.map((t) => t.id));
8753
8983
  if (newIds.size !== input.tasks.length) {
8754
8984
  const seen = /* @__PURE__ */ new Set();
8755
- const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
8985
+ const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => {
8986
+ if (seen.has(id)) return true;
8987
+ seen.add(id);
8988
+ return false;
8989
+ }))];
8756
8990
  early = {
8757
8991
  ok: false,
8758
8992
  message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
@@ -8787,7 +9021,7 @@ var taskTool = {
8787
9021
  }
8788
9022
  case "add": {
8789
9023
  const t = input.task;
8790
- if (!t || !t.title) {
9024
+ if (!t?.title) {
8791
9025
  early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
8792
9026
  return f;
8793
9027
  }