@wrongstack/tools 0.273.1 → 0.275.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 (44) hide show
  1. package/dist/audit.js +57 -19
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +166 -83
  4. package/dist/bash.js.map +1 -1
  5. package/dist/builtin.js +669 -189
  6. package/dist/builtin.js.map +1 -1
  7. package/dist/circuit-breaker.js +1 -0
  8. package/dist/circuit-breaker.js.map +1 -1
  9. package/dist/codebase-index/index.js +24 -16
  10. package/dist/codebase-index/index.js.map +1 -1
  11. package/dist/codebase-index/worker.js +24 -16
  12. package/dist/codebase-index/worker.js.map +1 -1
  13. package/dist/edit.js +1 -1
  14. package/dist/edit.js.map +1 -1
  15. package/dist/exec.js +136 -38
  16. package/dist/exec.js.map +1 -1
  17. package/dist/fetch.js +9 -1
  18. package/dist/fetch.js.map +1 -1
  19. package/dist/format.js +57 -19
  20. package/dist/format.js.map +1 -1
  21. package/dist/glob.js +61 -11
  22. package/dist/glob.js.map +1 -1
  23. package/dist/grep.js +92 -39
  24. package/dist/grep.js.map +1 -1
  25. package/dist/index.d.ts +37 -1
  26. package/dist/index.js +680 -200
  27. package/dist/index.js.map +1 -1
  28. package/dist/install.js +65 -19
  29. package/dist/install.js.map +1 -1
  30. package/dist/lint.js +57 -19
  31. package/dist/lint.js.map +1 -1
  32. package/dist/pack.js +667 -188
  33. package/dist/pack.js.map +1 -1
  34. package/dist/process-registry.d.ts +30 -0
  35. package/dist/process-registry.js +57 -19
  36. package/dist/process-registry.js.map +1 -1
  37. package/dist/search.js.map +1 -1
  38. package/dist/test.js +57 -19
  39. package/dist/test.js.map +1 -1
  40. package/dist/typecheck.js +57 -19
  41. package/dist/typecheck.js.map +1 -1
  42. package/dist/write.js +2 -2
  43. package/dist/write.js.map +1 -1
  44. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs7 from 'node:fs/promises';
2
2
  import * as Core from '@wrongstack/core';
3
- import { toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as path from 'node:path';
5
5
  import { resolve, sep, dirname, join } from 'node:path';
6
6
  import { spawn, execFileSync } from 'node:child_process';
@@ -375,7 +375,7 @@ var writeTool = {
375
375
  if (existed) {
376
376
  if (!ctx.hasRead(absPath)) {
377
377
  prev = await fs7.readFile(absPath, "utf8");
378
- ctx.recordRead(absPath, stat12.mtimeMs);
378
+ ctx.recordRead(absPath, stat12.mtimeMs, "write");
379
379
  } else {
380
380
  prev = await fs7.readFile(absPath, "utf8");
381
381
  }
@@ -389,7 +389,7 @@ var writeTool = {
389
389
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
390
390
  + (new file, ${input.content.split("\n").length} lines)`;
391
391
  const stat11 = await fs7.stat(absPath);
392
- ctx.recordRead(absPath, stat11.mtimeMs);
392
+ ctx.recordRead(absPath, stat11.mtimeMs, "write");
393
393
  ctx.session.recordFileChange({
394
394
  path: absPath,
395
395
  action: existed ? "modified" : "created",
@@ -486,7 +486,7 @@ var editTool = {
486
486
  const newFile = toStyle(newFileLf, style);
487
487
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
488
488
  const written = await fs7.stat(absPath);
489
- ctx.recordRead(absPath, written.mtimeMs);
489
+ ctx.recordRead(absPath, written.mtimeMs, "write");
490
490
  ctx.session.recordFileChange({
491
491
  path: absPath,
492
492
  action: "modified",
@@ -778,7 +778,27 @@ async function globNative(pattern, base, extraGlob) {
778
778
  await walk(base);
779
779
  return results;
780
780
  }
781
+
782
+ // src/_concurrency.ts
783
+ async function mapWithConcurrency(items, limit, fn) {
784
+ if (items.length === 0) return [];
785
+ const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
786
+ const results = new Array(items.length);
787
+ let nextIndex = 0;
788
+ const worker2 = async () => {
789
+ while (true) {
790
+ const i = nextIndex++;
791
+ if (i >= items.length) return;
792
+ results[i] = await fn(items[i]);
793
+ }
794
+ };
795
+ await Promise.all(Array.from({ length: effectiveLimit }, worker2));
796
+ return results;
797
+ }
798
+
799
+ // src/glob.ts
781
800
  var DEFAULT_IGNORE2 = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".turbo"];
801
+ var WALK_CONCURRENCY = 16;
782
802
  var globTool = {
783
803
  name: "glob",
784
804
  category: "Filesystem",
@@ -816,6 +836,22 @@ var globTool = {
816
836
  const re = compileGlob(input.pattern);
817
837
  const results = [];
818
838
  let truncated = false;
839
+ const pushResult = async (full) => {
840
+ if (truncated || results.length >= limit) {
841
+ truncated = true;
842
+ return;
843
+ }
844
+ try {
845
+ const st = await fs7.stat(full);
846
+ if (truncated || results.length >= limit) {
847
+ truncated = true;
848
+ return;
849
+ }
850
+ results.push({ rel: full, mtime: st.mtimeMs });
851
+ if (results.length >= limit) truncated = true;
852
+ } catch {
853
+ }
854
+ };
819
855
  const walk = async (dir, relPrefix) => {
820
856
  if (results.length >= limit) {
821
857
  truncated = true;
@@ -827,6 +863,8 @@ var globTool = {
827
863
  } catch {
828
864
  return;
829
865
  }
866
+ const subdirs = [];
867
+ const matchedFiles = [];
830
868
  for (const e of entries) {
831
869
  const name = e.name;
832
870
  if (DEFAULT_IGNORE2.includes(name)) continue;
@@ -834,22 +872,36 @@ var globTool = {
834
872
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
835
873
  const full = path.join(dir, name);
836
874
  if (e.isDirectory()) {
837
- await walk(full, rel);
838
- if (truncated) return;
875
+ subdirs.push({ full, rel });
839
876
  } else if (e.isFile()) {
840
- if (re.test(rel) || re.test(name)) {
841
- try {
842
- const st = await fs7.stat(full);
843
- results.push({ rel: full, mtime: st.mtimeMs });
844
- if (results.length >= limit) {
845
- truncated = true;
846
- return;
847
- }
848
- } catch {
877
+ re.lastIndex = 0;
878
+ const relMatch = re.test(rel);
879
+ re.lastIndex = 0;
880
+ const nameMatch = re.test(name);
881
+ if (relMatch || nameMatch) {
882
+ matchedFiles.push(full);
883
+ }
884
+ } else if (e.isSymbolicLink()) {
885
+ try {
886
+ const st = await fs7.stat(full);
887
+ if (st.isDirectory()) {
888
+ subdirs.push({ full, rel });
889
+ } else if (st.isFile()) {
890
+ re.lastIndex = 0;
891
+ const relMatch = re.test(rel);
892
+ re.lastIndex = 0;
893
+ const nameMatch = re.test(name);
894
+ if (relMatch || nameMatch) matchedFiles.push(full);
849
895
  }
896
+ } catch {
850
897
  }
851
898
  }
899
+ if (truncated) return;
852
900
  }
901
+ await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);
902
+ if (truncated) return;
903
+ const remainingSubdirs = truncated ? [] : subdirs;
904
+ await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));
853
905
  };
854
906
  await walk(base, "");
855
907
  results.sort((a, b) => b.mtime - a.mtime);
@@ -866,6 +918,8 @@ async function readGitignore(dir) {
866
918
  }
867
919
  var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
868
920
  var NATIVE_SCAN_CONCURRENCY = 32;
921
+ var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
922
+ var NATIVE_MAX_FILE_BYTES = 1e6;
869
923
  var grepTool = {
870
924
  name: "grep",
871
925
  category: "Search",
@@ -1090,7 +1144,8 @@ async function runNative(input, base, mode, limit, signal) {
1090
1144
  const re = compiled.regex;
1091
1145
  const globRe = input.glob ? compileGlob(input.glob) : null;
1092
1146
  const matches = [];
1093
- const fileMatches = /* @__PURE__ */ new Map();
1147
+ const countOnlyFirstHit = mode === "count" && limit === 1;
1148
+ const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
1094
1149
  let total = 0;
1095
1150
  let stopped = false;
1096
1151
  const scanFile = async (full, name) => {
@@ -1099,34 +1154,79 @@ async function runNative(input, base, mode, limit, signal) {
1099
1154
  if (globRe) globRe.lastIndex = 0;
1100
1155
  try {
1101
1156
  const stat11 = await fs7.stat(full);
1102
- if (stat11.size > 1e6 || stopped || signal.aborted) return;
1103
- const head = await fs7.readFile(full);
1104
- if (isBinaryBuffer(head) || stopped || signal.aborted) return;
1105
- const text = head.toString("utf8");
1106
- const lines = text.split(/\r?\n/);
1107
- let fileHits = 0;
1108
- for (let i = 0; i < lines.length; i++) {
1109
- if (stopped || signal.aborted) break;
1110
- const ln = capSubject(lines[i] ?? "");
1111
- re.lastIndex = 0;
1112
- if (re.test(ln)) {
1113
- fileHits++;
1114
- total++;
1115
- if (mode === "content" && matches.length < limit) {
1116
- matches.push(`${full}:${i + 1}:${ln}`);
1157
+ if (!stat11.isFile() || stat11.size > maxBytes || stopped || signal.aborted) return;
1158
+ const file = await fs7.open(full, "r");
1159
+ try {
1160
+ let bytesReadTotal = 0;
1161
+ let lineNumber = 0;
1162
+ let fileHits = 0;
1163
+ let leftover = "";
1164
+ let binaryChecked = false;
1165
+ const buffer = Buffer.allocUnsafe(Math.min(NATIVE_READ_CHUNK_BYTES, maxBytes));
1166
+ while (!stopped && !signal.aborted && bytesReadTotal < maxBytes) {
1167
+ const remaining = maxBytes - bytesReadTotal;
1168
+ const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, remaining), null);
1169
+ if (bytesRead === 0) break;
1170
+ const chunk = buffer.subarray(0, bytesRead);
1171
+ if (!binaryChecked) {
1172
+ binaryChecked = true;
1173
+ if (isBinaryBuffer(chunk)) return;
1174
+ }
1175
+ bytesReadTotal += bytesRead;
1176
+ const text = leftover + chunk.toString("utf8");
1177
+ const lines = text.split(/\r?\n/);
1178
+ leftover = lines.pop() ?? "";
1179
+ for (const rawLine of lines) {
1180
+ if (stopped || signal.aborted) break;
1181
+ lineNumber++;
1182
+ const ln = capSubject(rawLine);
1183
+ re.lastIndex = 0;
1184
+ if (!re.test(ln)) continue;
1185
+ fileHits++;
1186
+ total++;
1187
+ if (mode === "content") {
1188
+ if (matches.length < limit) matches.push(`${full}:${lineNumber}:${ln}`);
1189
+ } else if (mode === "files_with_matches") {
1190
+ if (fileHits === 1 && matches.length < limit) matches.push(full);
1191
+ break;
1192
+ } else if (fileHits === 1 && matches.length < limit) {
1193
+ matches.push(`${full}:${countOnlyFirstHit ? 1 : 0}`);
1194
+ }
1195
+ if (countOnlyFirstHit || mode !== "content" && matches.length >= limit) {
1196
+ stopped = true;
1197
+ break;
1198
+ }
1117
1199
  }
1200
+ if (mode === "files_with_matches" && fileHits > 0) break;
1118
1201
  }
1119
- }
1120
- if (fileHits > 0) {
1121
- fileMatches.set(full, fileHits);
1122
- if (mode === "files_with_matches" && matches.length < limit) {
1123
- matches.push(full);
1202
+ if (!stopped && !signal.aborted && leftover.length > 0) {
1203
+ lineNumber++;
1204
+ const ln = capSubject(leftover);
1205
+ re.lastIndex = 0;
1206
+ if (re.test(ln)) {
1207
+ fileHits++;
1208
+ total++;
1209
+ if (mode === "content") {
1210
+ if (matches.length < limit) matches.push(`${full}:${lineNumber}:${ln}`);
1211
+ } else if (mode === "files_with_matches") {
1212
+ if (matches.length < limit) matches.push(full);
1213
+ } else if (matches.length < limit) {
1214
+ matches.push(`${full}:${countOnlyFirstHit ? 1 : fileHits}`);
1215
+ }
1216
+ }
1124
1217
  }
1125
- if (mode === "count" && matches.length < limit) {
1126
- matches.push(`${full}:${fileHits}`);
1218
+ if (fileHits > 0) {
1219
+ if (mode === "count") {
1220
+ const idx = matches.findIndex((entry) => entry.startsWith(`${full}:`));
1221
+ if (idx !== -1) matches[idx] = `${full}:${fileHits}`;
1222
+ }
1223
+ if (mode === "files_with_matches" && matches.length >= limit) stopped = true;
1127
1224
  }
1225
+ if (mode === "content" && matches.length >= limit) stopped = true;
1226
+ if (mode === "count" && matches.length >= limit && (countOnlyFirstHit || mode !== "count")) stopped = true;
1227
+ } finally {
1228
+ await file.close();
1128
1229
  }
1129
- if (matches.length >= limit) stopped = true;
1130
1230
  } catch {
1131
1231
  }
1132
1232
  };
@@ -1139,18 +1239,20 @@ async function runNative(input, base, mode, limit, signal) {
1139
1239
  return;
1140
1240
  }
1141
1241
  const files = [];
1242
+ const subdirs = [];
1142
1243
  for (const e of entries) {
1143
1244
  if (stopped) return;
1144
1245
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
1145
1246
  if (e.isSymbolicLink()) continue;
1146
1247
  const full = path.join(dir, e.name);
1147
1248
  if (e.isDirectory()) {
1148
- await walk(full);
1249
+ subdirs.push(full);
1149
1250
  } else if (e.isFile()) {
1150
1251
  files.push({ full, name: e.name });
1151
1252
  }
1152
1253
  }
1153
1254
  await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
1255
+ await mapWithConcurrency(subdirs, Math.min(16, NATIVE_SCAN_CONCURRENCY), walk);
1154
1256
  };
1155
1257
  await walk(base);
1156
1258
  return {
@@ -1160,20 +1262,6 @@ async function runNative(input, base, mode, limit, signal) {
1160
1262
  used: "native"
1161
1263
  };
1162
1264
  }
1163
- async function mapWithConcurrency(items, concurrency, fn) {
1164
- if (items.length === 0) return;
1165
- let next = 0;
1166
- const workerCount = Math.min(Math.max(1, concurrency), items.length);
1167
- const workers = Array.from({ length: workerCount }, async () => {
1168
- for (; ; ) {
1169
- const idx = next++;
1170
- if (idx >= items.length) return;
1171
- const item = items[idx];
1172
- if (item !== void 0) await fn(item);
1173
- }
1174
- });
1175
- await Promise.all(workers);
1176
- }
1177
1265
  var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1178
1266
  var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
1179
1267
  var sweepStarted = false;
@@ -1215,7 +1303,7 @@ function createOutputSpool(opts) {
1215
1303
  let filePath = null;
1216
1304
  let failed = false;
1217
1305
  let finalized = false;
1218
- const open = () => {
1306
+ const open2 = () => {
1219
1307
  if (stream || failed) return;
1220
1308
  try {
1221
1309
  const dir = toolOutputDir();
@@ -1248,7 +1336,7 @@ function createOutputSpool(opts) {
1248
1336
  return;
1249
1337
  }
1250
1338
  head += text;
1251
- open();
1339
+ open2();
1252
1340
  head = "";
1253
1341
  return;
1254
1342
  }
@@ -1434,6 +1522,7 @@ var CircuitBreaker = class {
1434
1522
  if (this.state === "open") return;
1435
1523
  this.state = "open";
1436
1524
  this.openedAt = Date.now();
1525
+ this.window = [];
1437
1526
  try {
1438
1527
  this.onTrip?.();
1439
1528
  } catch {
@@ -1473,7 +1562,7 @@ var SENSITIVE_FLAG_PATTERNS = [
1473
1562
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
1474
1563
  // -f "value" style short flags
1475
1564
  /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
1476
- /(?<!\w)-p(?:ssword)?(?:\s+|\s*=\s*)[^\s,]+/gi,
1565
+ /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
1477
1566
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
1478
1567
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
1479
1568
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -1534,12 +1623,35 @@ var ProcessRegistryImpl = class {
1534
1623
  register(info) {
1535
1624
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
1536
1625
  }
1626
+ _isSafeSignalPid(pid) {
1627
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
1628
+ }
1629
+ _canSignalProcessGroup(p) {
1630
+ return os2.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
1631
+ }
1632
+ _killChildDirect(p, signal) {
1633
+ try {
1634
+ p.child.kill(signal);
1635
+ } catch {
1636
+ }
1637
+ }
1638
+ _killPosix(p, signal) {
1639
+ if (this._canSignalProcessGroup(p)) {
1640
+ try {
1641
+ process.kill(-p.pid, signal);
1642
+ return;
1643
+ } catch {
1644
+ }
1645
+ }
1646
+ this._killChildDirect(p, signal);
1647
+ }
1537
1648
  /** Unregister a process by PID. Called on 'close' / 'exit' events. */
1538
1649
  unregister(pid) {
1539
1650
  this.processes.delete(pid);
1540
1651
  }
1541
1652
  /** Get a single process by PID. */
1542
1653
  get(pid) {
1654
+ this._pruneStale(pid);
1543
1655
  return this.processes.get(pid);
1544
1656
  }
1545
1657
  /** Get all tracked processes. */
@@ -1703,13 +1815,14 @@ var ProcessRegistryImpl = class {
1703
1815
  * Returns true if the process was found and kill was attempted.
1704
1816
  */
1705
1817
  kill(pid, opts = {}) {
1818
+ this._pruneStale(pid);
1706
1819
  const p = this.processes.get(pid);
1707
1820
  if (!p) return false;
1708
1821
  if (p.killed) return true;
1709
1822
  if (p.protected) return false;
1710
1823
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
1711
- const isWin3 = os2.platform() === "win32";
1712
- if (isWin3) {
1824
+ const isWin4 = os2.platform() === "win32";
1825
+ if (isWin4) {
1713
1826
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
1714
1827
  if (liveRealChild && killWin32Tree(pid)) {
1715
1828
  const fallback = setTimeout(() => {
@@ -1732,27 +1845,12 @@ var ProcessRegistryImpl = class {
1732
1845
  }
1733
1846
  try {
1734
1847
  if (force) {
1735
- try {
1736
- process.kill(-pid, "SIGKILL");
1737
- } catch {
1738
- p.child.kill("SIGKILL");
1739
- }
1848
+ this._killPosix(p, "SIGKILL");
1740
1849
  } else {
1741
- try {
1742
- process.kill(-pid, "SIGTERM");
1743
- } catch {
1744
- p.child.kill("SIGTERM");
1745
- }
1850
+ this._killPosix(p, "SIGTERM");
1746
1851
  const timer = setTimeout(() => {
1747
1852
  if (this.processes.has(pid) && !p.child.killed) {
1748
- try {
1749
- process.kill(-pid, "SIGKILL");
1750
- } catch {
1751
- try {
1752
- p.child.kill("SIGKILL");
1753
- } catch {
1754
- }
1755
- }
1853
+ this._killPosix(p, "SIGKILL");
1756
1854
  }
1757
1855
  }, graceMs);
1758
1856
  timer.unref?.();
@@ -1787,6 +1885,34 @@ var ProcessRegistryImpl = class {
1787
1885
  }
1788
1886
  return killed;
1789
1887
  }
1888
+ /**
1889
+ * Check whether a tracked process entry is stale — the child has exited
1890
+ * (exitCode !== null) AND it's been in the registry long enough that the
1891
+ * OS may have reused the PID for a new, unrelated process.
1892
+ *
1893
+ * P3 #24 (before-release.md): on POSIX, PIDs are reused after process
1894
+ * exit. If a tracked process exits but its 'close' event hasn't fired yet
1895
+ * (or was missed), the registry still holds the entry. A new process
1896
+ * gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
1897
+ * may incorrectly protect or target the wrong process.
1898
+ *
1899
+ * The 60s threshold is conservative — the OS typically waits much longer
1900
+ * before reusing a PID, but we want to clean up before that becomes a risk.
1901
+ */
1902
+ _isStaleEntry(entry) {
1903
+ return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
1904
+ }
1905
+ /**
1906
+ * Remove a stale entry for a specific PID before any PID-based lookup.
1907
+ * This prevents PID reuse from causing the registry to act on a dead
1908
+ * process that has been replaced by a new one with the same PID.
1909
+ */
1910
+ _pruneStale(pid) {
1911
+ const entry = this.processes.get(pid);
1912
+ if (entry && this._isStaleEntry(entry)) {
1913
+ this.processes.delete(pid);
1914
+ }
1915
+ }
1790
1916
  };
1791
1917
  var _registry;
1792
1918
  function getProcessRegistry() {
@@ -2211,17 +2337,18 @@ function resetPersistentProcessRegistry() {
2211
2337
  }
2212
2338
 
2213
2339
  // src/bash-kill-guard.ts
2340
+ var isWin = os2.platform() === "win32";
2214
2341
  function extractKillCommand(command) {
2215
2342
  const normalized = command.replace(/\s+/g, " ").trim();
2216
2343
  const shellCMatch = normalized.match(
2217
- /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
2344
+ /^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
2218
2345
  );
2219
2346
  if (shellCMatch?.[1]) {
2220
2347
  const inner = shellCMatch[1].trim();
2221
2348
  return isKillRelatedCommand(inner) ? inner : null;
2222
2349
  }
2223
2350
  const shellCUnquoted = normalized.match(
2224
- /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
2351
+ /^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
2225
2352
  );
2226
2353
  if (shellCUnquoted?.[1]) {
2227
2354
  return shellCUnquoted[1];
@@ -2230,15 +2357,43 @@ function extractKillCommand(command) {
2230
2357
  }
2231
2358
  function isKillRelatedCommand(cmd) {
2232
2359
  const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
2360
+ if (isWin) {
2361
+ if (/^taskkill\s/i.test(normalized)) return true;
2362
+ if (/^tskill\s/i.test(normalized)) return true;
2363
+ return false;
2364
+ }
2233
2365
  if (/^kill(\s|$)/.test(normalized)) return true;
2234
2366
  if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
2235
- if (/^taskkill\s/i.test(normalized)) return true;
2236
- if (/^tskill\s/i.test(normalized)) return true;
2237
2367
  if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
2238
2368
  return false;
2239
2369
  }
2240
2370
  function parseKillCommand(command) {
2241
2371
  const normalized = command.replace(/\s+/g, " ").trim();
2372
+ if (isWin) {
2373
+ const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
2374
+ if (taskkillMatch?.[1]) {
2375
+ const pidStr = taskkillMatch[1];
2376
+ return {
2377
+ pid: parseInt(pidStr, 10),
2378
+ signal: normalized.includes("/F") ? "FORCE" : "TERM",
2379
+ isGroupKill: false,
2380
+ isAllKill: false,
2381
+ originalCommand: command
2382
+ };
2383
+ }
2384
+ const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
2385
+ if (tskillMatch?.[1]) {
2386
+ const pidStr = tskillMatch[1];
2387
+ return {
2388
+ pid: parseInt(pidStr, 10),
2389
+ signal: "TERM",
2390
+ isGroupKill: false,
2391
+ isAllKill: false,
2392
+ originalCommand: command
2393
+ };
2394
+ }
2395
+ return null;
2396
+ }
2242
2397
  const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
2243
2398
  if (simpleMatch) {
2244
2399
  const signal = simpleMatch[1] ?? "-TERM";
@@ -2282,28 +2437,6 @@ function parseKillCommand(command) {
2282
2437
  if (pgrepMatch) {
2283
2438
  return null;
2284
2439
  }
2285
- const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
2286
- if (taskkillMatch?.[1]) {
2287
- const pidStr = taskkillMatch[1];
2288
- return {
2289
- pid: parseInt(pidStr, 10),
2290
- signal: normalized.includes("/F") ? "FORCE" : "TERM",
2291
- isGroupKill: false,
2292
- isAllKill: false,
2293
- originalCommand: command
2294
- };
2295
- }
2296
- const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
2297
- if (tskillMatch?.[1]) {
2298
- const pidStr = tskillMatch[1];
2299
- return {
2300
- pid: parseInt(pidStr, 10),
2301
- signal: "TERM",
2302
- isGroupKill: false,
2303
- isAllKill: false,
2304
- originalCommand: command
2305
- };
2306
- }
2307
2440
  return null;
2308
2441
  }
2309
2442
  async function getProtectedEntries() {
@@ -2380,7 +2513,7 @@ async function checkAndBlockKillCommand(command) {
2380
2513
  }
2381
2514
  return { blocked: false };
2382
2515
  }
2383
- function pickShell(platform4, command, env) {
2516
+ function pickShell(platform5, command, env) {
2384
2517
  const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
2385
2518
  if (override === "cmd" || override === "cmd.exe") return "cmd";
2386
2519
  if (override === "powershell" || override === "powershell.exe") return "powershell";
@@ -2434,7 +2567,7 @@ function looksLikePowerShellExtended(command) {
2434
2567
  }
2435
2568
  function wrapPowerShellScript(command) {
2436
2569
  const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
2437
- return "\uFEFF" + bootstrap + "\ntry {\n" + command + "\n} finally { exit $LASTEXITCODE }";
2570
+ return bootstrap + "\n$ErrorActionPreference='Stop'\n" + command + "\nif ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }";
2438
2571
  }
2439
2572
  var PS_VERB_RE = new RegExp(
2440
2573
  // Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
@@ -2624,10 +2757,10 @@ var bashTool = {
2624
2757
  }));
2625
2758
  }
2626
2759
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
2627
- const isWin3 = os2.platform() === "win32";
2760
+ const isWin4 = os2.platform() === "win32";
2628
2761
  let plan;
2629
2762
  let winShellKind;
2630
- if (isWin3) {
2763
+ if (isWin4) {
2631
2764
  const shell2 = pickShell("win32", input.command, {
2632
2765
  get: (k) => process.env[k]
2633
2766
  });
@@ -2656,7 +2789,7 @@ var bashTool = {
2656
2789
  const shell = plan.bin;
2657
2790
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
2658
2791
  const env = buildChildEnv(ctx.session?.id);
2659
- const detached = !isWin3;
2792
+ const detached = !isWin4;
2660
2793
  const startedAt = Date.now();
2661
2794
  if (input.background) {
2662
2795
  let buf2 = "";
@@ -2674,7 +2807,7 @@ var bashTool = {
2674
2807
  // apply: the child gets a hidden console that grandchildren inherit.
2675
2808
  // Windows children survive parent exit either way. POSIX keeps
2676
2809
  // detached for the process-group kill semantics.
2677
- detached: !isWin3,
2810
+ detached: !isWin4,
2678
2811
  windowsHide: true
2679
2812
  });
2680
2813
  if (plan.useStdin) {
@@ -2692,7 +2825,8 @@ var bashTool = {
2692
2825
  command: redactCommand(input.command),
2693
2826
  startedAt: Date.now(),
2694
2827
  sessionId: ctx.session?.id,
2695
- child: child2
2828
+ child: child2,
2829
+ processGroupLeader: detached && child2.pid === pid2
2696
2830
  });
2697
2831
  child2.on("close", () => registry.unregister(pid2));
2698
2832
  }
@@ -2733,6 +2867,14 @@ var bashTool = {
2733
2867
  pid: pid2
2734
2868
  }
2735
2869
  };
2870
+ ctx.recordSideEffect?.({
2871
+ toolUseId: `bash-bg-${Date.now()}`,
2872
+ toolName: "bash",
2873
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2874
+ input: { command: redactCommand(input.command), background: true },
2875
+ outcome: `launched (pid ${pid2 ?? "unknown"})`,
2876
+ risk: "shell"
2877
+ });
2736
2878
  return;
2737
2879
  }
2738
2880
  const child = spawn(shell, args, {
@@ -2743,7 +2885,7 @@ var bashTool = {
2743
2885
  stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
2744
2886
  detached,
2745
2887
  windowsHide: true,
2746
- ...isWin3 ? {} : { signal: opts.signal }
2888
+ ...isWin4 ? {} : { signal: opts.signal }
2747
2889
  });
2748
2890
  if (plan.useStdin) {
2749
2891
  try {
@@ -2760,7 +2902,8 @@ var bashTool = {
2760
2902
  command: redactCommand(input.command),
2761
2903
  startedAt: Date.now(),
2762
2904
  sessionId: ctx.session?.id,
2763
- child
2905
+ child,
2906
+ processGroupLeader: detached && child.pid === pid
2764
2907
  });
2765
2908
  }
2766
2909
  let buf = "";
@@ -2769,7 +2912,7 @@ var bashTool = {
2769
2912
  const timers = [];
2770
2913
  const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
2771
2914
  function killWithTimeout(child2, timeoutMs2) {
2772
- if (isWin3) {
2915
+ if (isWin4) {
2773
2916
  if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
2774
2917
  const fallback = setTimeout(() => {
2775
2918
  if (child2.exitCode === null) {
@@ -2789,34 +2932,22 @@ var bashTool = {
2789
2932
  }
2790
2933
  return;
2791
2934
  }
2792
- try {
2793
- if (typeof child2.pid === "number") {
2794
- try {
2795
- process.kill(-child2.pid, "SIGTERM");
2796
- } catch {
2797
- child2.kill("SIGTERM");
2798
- }
2799
- } else {
2935
+ if (typeof child2.pid === "number") {
2936
+ registry.kill(child2.pid, { graceMs: timeoutMs2 });
2937
+ } else {
2938
+ try {
2800
2939
  child2.kill("SIGTERM");
2940
+ } catch {
2801
2941
  }
2802
- } catch {
2803
- }
2804
- const killTimer = setTimeout(() => {
2805
- try {
2806
- if (typeof child2.pid === "number") {
2807
- try {
2808
- process.kill(-child2.pid, "SIGKILL");
2809
- } catch {
2810
- child2.kill("SIGKILL");
2811
- }
2812
- } else {
2942
+ const killTimer = setTimeout(() => {
2943
+ try {
2813
2944
  child2.kill("SIGKILL");
2945
+ } catch {
2814
2946
  }
2815
- } catch {
2816
- }
2817
- }, timeoutMs2);
2818
- timers.push(killTimer);
2819
- killTimer.unref?.();
2947
+ }, timeoutMs2);
2948
+ timers.push(killTimer);
2949
+ killTimer.unref?.();
2950
+ }
2820
2951
  }
2821
2952
  const timer = setTimeout(() => {
2822
2953
  timedOut = true;
@@ -2825,7 +2956,7 @@ var bashTool = {
2825
2956
  timers.push(timer);
2826
2957
  timer.unref?.();
2827
2958
  const onAbort = () => killWithTimeout(child, 2e3);
2828
- if (isWin3) {
2959
+ if (isWin4) {
2829
2960
  if (opts.signal.aborted) onAbort();
2830
2961
  else opts.signal.addEventListener("abort", onAbort, { once: true });
2831
2962
  }
@@ -2913,6 +3044,14 @@ ${hint}` : ""),
2913
3044
  timed_out: timedOut
2914
3045
  }
2915
3046
  };
3047
+ ctx.recordSideEffect?.({
3048
+ toolUseId: `bash-${Date.now()}`,
3049
+ toolName: "bash",
3050
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3051
+ input: { command: redactCommand(input.command) },
3052
+ outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
3053
+ risk: "shell"
3054
+ });
2916
3055
  return;
2917
3056
  }
2918
3057
  const now2 = Date.now();
@@ -2924,7 +3063,7 @@ ${hint}` : ""),
2924
3063
  } finally {
2925
3064
  for (const t of timers) clearTimeout(t);
2926
3065
  spool.finalize();
2927
- if (isWin3) opts.signal.removeEventListener("abort", onAbort);
3066
+ if (isWin4) opts.signal.removeEventListener("abort", onAbort);
2928
3067
  child.stdout?.off("data", onData);
2929
3068
  child.stderr?.off("data", onData);
2930
3069
  child.stdout?.destroy();
@@ -2934,6 +3073,38 @@ ${hint}` : ""),
2934
3073
  else killWithTimeout(child, 2e3);
2935
3074
  }
2936
3075
  }
3076
+ },
3077
+ /**
3078
+ * Tool-level teardown fired by `ToolExecutor.runToolCleanup()` when the
3079
+ * tool's run is aborted/timeout'd. The generator's `finally` block above
3080
+ * already force-kills the direct child, but that only runs if the
3081
+ * executor closes the async iterator (via `iter.return()`). When the
3082
+ * executor tears down without iterating — or a re-entrant abort races
3083
+ * with the generator — a bash-spawned process tree can survive in the
3084
+ * ProcessRegistry with `killed === false`, continuing to write files,
3085
+ * consume CPU, or hold inherited stdio pipes open for the rest of the
3086
+ * session.
3087
+ *
3088
+ * This is the defensive layer the executor calls via `tool.cleanup()`
3089
+ * (see `types/tool.ts`): kill every bash-owned process still tracked
3090
+ * for this session that hasn't exited yet. `registry.kill()` already
3091
+ * handles process-group / taskkill tree-kill and the SIGTERM→SIGKILL
3092
+ * grace window, so this just scopes the registry's existing kill path
3093
+ * to "this session's runaway bash children". Idempotent — a process
3094
+ * that already exited is skipped by `kill()` (it returns false), and a
3095
+ * `protected` infrastructure process (dev server the user intentionally
3096
+ * backgrounded) is left alone by design.
3097
+ */
3098
+ async cleanup(_input, ctx) {
3099
+ const registry = getProcessRegistry();
3100
+ const sessionId = ctx.session?.id;
3101
+ if (!sessionId) return;
3102
+ for (const entry of registry.bySession(sessionId)) {
3103
+ if (entry.name !== "bash") continue;
3104
+ if (entry.child.exitCode !== null) continue;
3105
+ if (entry.protected) continue;
3106
+ registry.kill(entry.pid, { force: true });
3107
+ }
2937
3108
  }
2938
3109
  };
2939
3110
 
@@ -2945,8 +3116,8 @@ function normalizeShell(value) {
2945
3116
  if (v === "pwsh" || v === "pwsh.exe") return "pwsh";
2946
3117
  return void 0;
2947
3118
  }
2948
- function resolveSessionShell(platform4, env, deps = {}) {
2949
- if (platform4 !== "win32") return void 0;
3119
+ function resolveSessionShell(platform5, env, deps = {}) {
3120
+ if (platform5 !== "win32") return void 0;
2950
3121
  const override = normalizeShell(env.get("WRONGSTACK_SHELL"));
2951
3122
  if (override) return override;
2952
3123
  const hasBinary = deps.hasBinary ?? ((bin) => resolveWin32Command(bin) !== bin);
@@ -2956,15 +3127,15 @@ function resolveSessionShell(platform4, env, deps = {}) {
2956
3127
  }
2957
3128
  function ensureSessionShell(opts = {}) {
2958
3129
  const env = opts.env ?? process.env;
2959
- const platform4 = opts.platform ?? process.platform;
2960
- if (platform4 !== "win32") return void 0;
3130
+ const platform5 = opts.platform ?? process.platform;
3131
+ if (platform5 !== "win32") return void 0;
2961
3132
  const existing = normalizeShell(env["WRONGSTACK_SHELL"]);
2962
3133
  if (existing) return existing;
2963
- const chosen = resolveSessionShell(platform4, { get: (k) => env[k] }, { hasBinary: opts.hasBinary }) ?? "cmd";
3134
+ const chosen = resolveSessionShell(platform5, { get: (k) => env[k] }, { hasBinary: opts.hasBinary }) ?? "cmd";
2964
3135
  env["WRONGSTACK_SHELL"] = chosen;
2965
3136
  return chosen;
2966
3137
  }
2967
- var isWin = process.platform === "win32";
3138
+ var isWin2 = process.platform === "win32";
2968
3139
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
2969
3140
  // JS / TS toolchain
2970
3141
  "node",
@@ -2974,12 +3145,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
2974
3145
  "npx",
2975
3146
  "bun",
2976
3147
  "deno",
3148
+ "corepack",
2977
3149
  "tsc",
3150
+ "tsx",
3151
+ "ts-node",
3152
+ "vite",
2978
3153
  "vitest",
2979
3154
  "jest",
2980
3155
  "biome",
2981
3156
  "eslint",
2982
3157
  "prettier",
3158
+ "turbo",
3159
+ "nx",
3160
+ "webpack",
3161
+ "rollup",
3162
+ "parcel",
3163
+ "next",
3164
+ "astro",
3165
+ "playwright",
3166
+ "cypress",
2983
3167
  // version control
2984
3168
  "git",
2985
3169
  // Rust
@@ -2992,10 +3176,22 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
2992
3176
  "python3",
2993
3177
  "pip",
2994
3178
  "pip3",
3179
+ "pytest",
3180
+ "ruff",
3181
+ "mypy",
3182
+ "uv",
3183
+ "uvx",
3184
+ "poetry",
3185
+ "hatch",
3186
+ "tox",
2995
3187
  // Ruby
2996
3188
  "ruby",
2997
3189
  "gem",
2998
3190
  "bundle",
3191
+ // PHP
3192
+ "php",
3193
+ "composer",
3194
+ "phpunit",
2999
3195
  // JVM
3000
3196
  "java",
3001
3197
  "javac",
@@ -3007,18 +3203,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3007
3203
  // C / C++ / native build
3008
3204
  "make",
3009
3205
  "cmake",
3010
- // containers / orchestration (read-only subcommands; see BLOCKED_ARG_PATTERNS)
3206
+ // containers / orchestration
3011
3207
  "docker",
3208
+ "podman",
3012
3209
  "kubectl",
3013
3210
  // common POSIX file/text utilities
3211
+ "pwd",
3014
3212
  "ls",
3015
3213
  "cat",
3016
3214
  "head",
3017
3215
  "tail",
3018
3216
  "wc",
3019
3217
  "grep",
3218
+ "rg",
3020
3219
  "find",
3021
3220
  "echo",
3221
+ "sort",
3222
+ "uniq",
3223
+ "sed",
3224
+ "awk",
3022
3225
  "mkdir",
3023
3226
  "cp",
3024
3227
  "mv",
@@ -3049,8 +3252,7 @@ var MAX_ARGS = 20;
3049
3252
  var MAX_OUTPUT2 = 2e5;
3050
3253
  var DEFAULT_TIMEOUT_MS2 = 3e4;
3051
3254
  var BLOCKED_ARG_PATTERNS = {
3052
- // python -c/--command executes arbitrary code; python -m runs modules
3053
- python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
3255
+ python: [],
3054
3256
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
3055
3257
  // -C <dir> changes working directory, bypassing cwd sandbox;
3056
3258
  // -c/--config <k>=<v> injects config that runs commands
@@ -3066,15 +3268,10 @@ var BLOCKED_ARG_PATTERNS = {
3066
3268
  /^--config=/,
3067
3269
  /^--config-env=/
3068
3270
  ],
3069
- // node -r/--require preloads arbitrary modules; --eval executes code
3070
- node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
3071
- // go run could execute arbitrary .go files; -ldflags could inject build-time code
3072
- go: [/^-ldflags$/],
3073
- // bun --preload is similar to node --require
3074
- bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
3075
- // docker build/run can create containers with host access;
3076
- // only allow read-only commands (ps, images, version)
3077
- docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
3271
+ node: [],
3272
+ go: [],
3273
+ bun: [],
3274
+ docker: [],
3078
3275
  // find -exec/-ok/-execdir execute arbitrary commands
3079
3276
  find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
3080
3277
  // rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
@@ -3082,15 +3279,49 @@ var BLOCKED_ARG_PATTERNS = {
3082
3279
  // `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
3083
3280
  // `rm -rf /*` targets the filesystem root. All are blocked.
3084
3281
  rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
3085
- // npm run/exec/create/pack/publish can execute arbitrary scripts or publish malware
3086
- npm: [/^run$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
3087
- // pnpm run/dlx/exec/create can execute arbitrary scripts
3088
- pnpm: [/^run$/, /^dlx$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
3089
- // npx should only be used for --version; any package name is a vector for
3090
- // malicious package execution (typosquatting, dependency confusion)
3091
- npx: [/^[^\s]+$/]
3282
+ // npm/pnpm subcommands are checked separately below. Matching every arg here
3283
+ // over-blocked normal dev flows such as `pnpm vitest run ...`.
3284
+ npm: [],
3285
+ pnpm: [],
3286
+ npx: []
3287
+ };
3288
+ var BLOCKED_SUBCOMMANDS = {
3289
+ docker: /* @__PURE__ */ new Set(["push"]),
3290
+ podman: /* @__PURE__ */ new Set(["push"]),
3291
+ npm: /* @__PURE__ */ new Set(["publish", "deploy"]),
3292
+ pnpm: /* @__PURE__ */ new Set(["publish", "deploy"]),
3293
+ yarn: /* @__PURE__ */ new Set(["publish"])
3294
+ };
3295
+ var BLOCKED_SUBCOMMAND_SEQUENCES = {
3296
+ yarn: [["npm", "publish"]]
3092
3297
  };
3298
+ function firstSubcommand(args) {
3299
+ for (const arg of args) {
3300
+ if (arg === "--") return null;
3301
+ if (!arg.startsWith("-")) return arg;
3302
+ }
3303
+ return null;
3304
+ }
3305
+ function subcommandArgs(args) {
3306
+ const out = [];
3307
+ for (const arg of args) {
3308
+ if (arg === "--") break;
3309
+ if (!arg.startsWith("-")) out.push(arg);
3310
+ }
3311
+ return out;
3312
+ }
3093
3313
  function validateArgs(cmd, args) {
3314
+ const blockedSubcommands = BLOCKED_SUBCOMMANDS[cmd];
3315
+ const subcommand = firstSubcommand(args);
3316
+ if (blockedSubcommands && subcommand && blockedSubcommands.has(subcommand)) {
3317
+ return `Blocked subcommand "${subcommand}" for command "${cmd}"`;
3318
+ }
3319
+ const blockedSequences = BLOCKED_SUBCOMMAND_SEQUENCES[cmd];
3320
+ if (blockedSequences) {
3321
+ const actual = subcommandArgs(args);
3322
+ const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
3323
+ if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
3324
+ }
3094
3325
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
3095
3326
  if (!blocked) return null;
3096
3327
  for (const arg of args) {
@@ -3217,7 +3448,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3217
3448
  const startedAt = Date.now();
3218
3449
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
3219
3450
  const resolved = resolveWin32Command(cmd);
3220
- const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3451
+ const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3221
3452
  const spawnCmd = needsShell ? cmd : resolved;
3222
3453
  if (needsShell) assertSafeWin32ShellArgs(args);
3223
3454
  let child;
@@ -3227,7 +3458,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3227
3458
  env: buildChildEnv(sessionId),
3228
3459
  stdio: ["ignore", "pipe", "pipe"],
3229
3460
  windowsHide: true,
3230
- ...isWin ? {} : { signal },
3461
+ ...isWin2 ? {} : { signal },
3231
3462
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3232
3463
  });
3233
3464
  } catch (err) {
@@ -3247,7 +3478,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3247
3478
  const isAbort = err && err.code === "ABORT_ERR";
3248
3479
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
3249
3480
  clearTimeout(timer);
3250
- if (isWin) signal.removeEventListener("abort", onAbort);
3481
+ if (isWin2) signal.removeEventListener("abort", onAbort);
3251
3482
  if (typeof pid === "number") registry.unregister(pid);
3252
3483
  registry.afterCall(Date.now() - startedAt, true);
3253
3484
  spool.finalize();
@@ -3277,7 +3508,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3277
3508
  if (typeof pid === "number") registry.kill(pid, { force: true });
3278
3509
  else child.kill("SIGTERM");
3279
3510
  };
3280
- if (isWin) {
3511
+ if (isWin2) {
3281
3512
  if (signal.aborted) onAbort();
3282
3513
  else signal.addEventListener("abort", onAbort, { once: true });
3283
3514
  }
@@ -3293,7 +3524,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3293
3524
  });
3294
3525
  child.on("close", (code) => {
3295
3526
  clearTimeout(timer);
3296
- if (isWin) signal.removeEventListener("abort", onAbort);
3527
+ if (isWin2) signal.removeEventListener("abort", onAbort);
3297
3528
  if (typeof pid === "number") registry.unregister(pid);
3298
3529
  const durationMs = Date.now() - startedAt;
3299
3530
  const exitCode = killed ? 124 : code ?? 1;
@@ -3457,7 +3688,7 @@ var fetchTool = {
3457
3688
  if (!final) throw new Error("fetch: stream ended without final event");
3458
3689
  return final;
3459
3690
  },
3460
- async *executeStream(input, _ctx, opts) {
3691
+ async *executeStream(input, ctx, opts) {
3461
3692
  if (!input?.url) throw new Error("fetch: url is required");
3462
3693
  const u = new URL(input.url);
3463
3694
  if (u.protocol !== "https:" && u.protocol !== "http:") {
@@ -3529,6 +3760,14 @@ var fetchTool = {
3529
3760
  url: res.url
3530
3761
  }
3531
3762
  };
3763
+ ctx.recordSideEffect?.({
3764
+ toolUseId: `fetch-${Date.now()}`,
3765
+ toolName: "fetch",
3766
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3767
+ input: { url: input.url, format: input.format },
3768
+ outcome: `HTTP ${res.status} (${ct})`,
3769
+ risk: "network"
3770
+ });
3532
3771
  } finally {
3533
3772
  clearTimeout(timer);
3534
3773
  }
@@ -4161,13 +4400,13 @@ ${formatTaskList(taskFile.tasks)}`
4161
4400
  }
4162
4401
  };
4163
4402
  function mkResult(plan, ok, message, todos) {
4164
- const open = plan.items.filter((i) => i.status !== "done").length;
4403
+ const open2 = plan.items.filter((i) => i.status !== "done").length;
4165
4404
  const result = {
4166
4405
  ok,
4167
4406
  message,
4168
4407
  plan: formatPlan(plan),
4169
4408
  count: plan.items.length,
4170
- open
4409
+ open: open2
4171
4410
  };
4172
4411
  if (todos !== void 0) result.todos = todos;
4173
4412
  return result;
@@ -4636,8 +4875,8 @@ var jsonTool = {
4636
4875
  };
4637
4876
  }
4638
4877
  };
4639
- function query(data, path21) {
4640
- const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
4878
+ function query(data, path22) {
4879
+ const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
4641
4880
  let current = data;
4642
4881
  for (const part of parts) {
4643
4882
  if (current === null || current === void 0) return void 0;
@@ -5009,7 +5248,7 @@ async function walkDir(dir, depth, opts) {
5009
5248
  }
5010
5249
  }
5011
5250
  }
5012
- var isWin2 = process.platform === "win32";
5251
+ var isWin3 = process.platform === "win32";
5013
5252
  async function* spawnStream(opts) {
5014
5253
  const max = opts.maxBytes ?? 2e5;
5015
5254
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -5020,7 +5259,7 @@ async function* spawnStream(opts) {
5020
5259
  let error;
5021
5260
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
5022
5261
  const resolved = resolveWin32Command(opts.cmd);
5023
- const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5262
+ const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5024
5263
  const cmd = needsShell ? opts.cmd : resolved;
5025
5264
  if (needsShell) assertSafeWin32ShellArgs(opts.args);
5026
5265
  const child = spawn(cmd, opts.args, {
@@ -5028,7 +5267,7 @@ async function* spawnStream(opts) {
5028
5267
  env: buildChildEnv(),
5029
5268
  stdio: ["ignore", "pipe", "pipe"],
5030
5269
  windowsHide: true,
5031
- ...isWin2 ? {} : { signal: opts.signal },
5270
+ ...isWin3 ? {} : { signal: opts.signal },
5032
5271
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
5033
5272
  });
5034
5273
  const registry = getProcessRegistry();
@@ -5107,7 +5346,7 @@ async function* spawnStream(opts) {
5107
5346
  queue.push({ kind: "close", data: "", code: 124 });
5108
5347
  wake();
5109
5348
  };
5110
- if (isWin2) {
5349
+ if (isWin3) {
5111
5350
  if (opts.signal.aborted) onAbort();
5112
5351
  else opts.signal.addEventListener("abort", onAbort, { once: true });
5113
5352
  }
@@ -5154,7 +5393,7 @@ async function* spawnStream(opts) {
5154
5393
  };
5155
5394
  } finally {
5156
5395
  spool.finalize();
5157
- if (isWin2) opts.signal.removeEventListener("abort", onAbort);
5396
+ if (isWin3) opts.signal.removeEventListener("abort", onAbort);
5158
5397
  child.stdout?.off("data", onOut);
5159
5398
  child.stderr?.off("data", onErr);
5160
5399
  child.stdout?.destroy();
@@ -5740,6 +5979,14 @@ var installTool = {
5740
5979
  }
5741
5980
  }
5742
5981
  }
5982
+ ctx.recordSideEffect?.({
5983
+ toolUseId: `install-${Date.now()}`,
5984
+ toolName: "install",
5985
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5986
+ input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
5987
+ outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
5988
+ risk: "package"
5989
+ });
5743
5990
  yield { type: "final", output };
5744
5991
  }
5745
5992
  };
@@ -6106,7 +6353,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6106
6353
  }
6107
6354
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
6108
6355
  var MAX_TAIL_LINES = 1e5;
6109
- async function fileLogs(path21, lines, filterRe, stream) {
6356
+ async function fileLogs(path22, lines, filterRe, stream) {
6110
6357
  const { createInterface } = await import('node:readline');
6111
6358
  const { createReadStream } = await import('node:fs');
6112
6359
  const entries = [];
@@ -6115,7 +6362,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
6115
6362
  let writeIdx = 0;
6116
6363
  let totalLines = 0;
6117
6364
  const rl = createInterface({
6118
- input: createReadStream(path21),
6365
+ input: createReadStream(path22),
6119
6366
  crlfDelay: Number.POSITIVE_INFINITY
6120
6367
  });
6121
6368
  for await (const line of rl) {
@@ -6136,7 +6383,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
6136
6383
  if (parsed) entries.push(parsed);
6137
6384
  }
6138
6385
  return {
6139
- source: path21,
6386
+ source: path22,
6140
6387
  entries,
6141
6388
  total: entries.length,
6142
6389
  truncated: totalLines > effLines,
@@ -6508,6 +6755,229 @@ function substituteVars(content, name, vars) {
6508
6755
  }
6509
6756
  return result;
6510
6757
  }
6758
+ function normalizeOverrides(set) {
6759
+ const out = {};
6760
+ if (set && typeof set === "object") {
6761
+ for (const [k, v] of Object.entries(set)) {
6762
+ if (typeof v === "string") out[k] = v;
6763
+ }
6764
+ }
6765
+ return out;
6766
+ }
6767
+ var designTool = {
6768
+ name: "design",
6769
+ category: "Design",
6770
+ description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
6771
+ usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
6772
+ permission: "auto",
6773
+ mutating: false,
6774
+ capabilities: [],
6775
+ timeoutMs: 15e3,
6776
+ inputSchema: {
6777
+ type: "object",
6778
+ properties: {
6779
+ action: {
6780
+ type: "string",
6781
+ enum: ["list", "use", "foundations", "set", "materialize", "verify"],
6782
+ description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
6783
+ },
6784
+ kit: {
6785
+ type: "string",
6786
+ description: 'Kit id (required for "use"), e.g. "minimal-clarity", "neo-brutalist".'
6787
+ },
6788
+ stack: {
6789
+ type: "string",
6790
+ enum: ["web", "react-native", "flutter", "swiftui", "compose"],
6791
+ description: "Target stack \u2014 narrows guidance + materialize format. Default: web."
6792
+ },
6793
+ set: {
6794
+ type: "object",
6795
+ description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
6796
+ additionalProperties: { type: "string" }
6797
+ },
6798
+ out: {
6799
+ type: "string",
6800
+ description: "Materialize output path (project-relative). Defaults to a per-stack convention."
6801
+ },
6802
+ force: {
6803
+ type: "boolean",
6804
+ description: "Materialize: overwrite an existing file (default false \u2014 refuses to clobber)."
6805
+ },
6806
+ files: {
6807
+ type: "array",
6808
+ items: { type: "string" },
6809
+ description: "Verify: explicit project-relative files to scan. Default: a bounded UI-file walk."
6810
+ }
6811
+ },
6812
+ required: []
6813
+ },
6814
+ async execute(input, ctx) {
6815
+ const loader = getDesignKitLoader(ctx.projectRoot);
6816
+ const action = input.action ?? "list";
6817
+ const stack = input.stack && isDesignStack(input.stack) ? input.stack : void 0;
6818
+ if (action === "foundations") {
6819
+ const text = await loader.foundationsText(stack);
6820
+ return { action, stack, output: text || "No foundations document is installed." };
6821
+ }
6822
+ if (action === "use") {
6823
+ const kitId = input.kit?.trim();
6824
+ if (!kitId) {
6825
+ const menu2 = await loader.menuText();
6826
+ return { action, output: `No kit id provided.
6827
+
6828
+ ${menu2}` };
6829
+ }
6830
+ const manifest = await loader.find(kitId);
6831
+ if (!manifest) {
6832
+ const menu2 = await loader.menuText();
6833
+ return { action, kit: kitId, output: `Kit "${kitId}" not found.
6834
+
6835
+ ${menu2}` };
6836
+ }
6837
+ const resolvedStack = stack ?? manifest.stacks[0] ?? "web";
6838
+ const body = await loader.readBody(manifest.id, resolvedStack);
6839
+ const rawTokens = await loader.readTokens(manifest.id);
6840
+ const persisted = await loadActiveKit(ctx.projectRoot);
6841
+ const keepOverrides = persisted?.kit === manifest.id ? persisted.overrides ?? {} : {};
6842
+ const overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };
6843
+ const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
6844
+ setActiveKit(ctx, manifest.id, resolvedStack, overrides);
6845
+ await recordKitChoice(
6846
+ ctx.projectRoot,
6847
+ manifest.id,
6848
+ resolvedStack,
6849
+ "design-tool",
6850
+ (/* @__PURE__ */ new Date()).toISOString(),
6851
+ Object.keys(overrides).length ? overrides : void 0
6852
+ );
6853
+ const ovLine = Object.keys(overrides).length ? `
6854
+ Active color overrides: ${Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join(", ")}
6855
+ ` : "";
6856
+ const header = `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}
6857
+ ${manifest.aesthetic}
6858
+ ${ovLine}
6859
+ Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.
6860
+ `;
6861
+ const tokenBlock = tokens ? `
6862
+ ## Token snapshot (overrides applied)
6863
+ \`\`\`json
6864
+ ${JSON.stringify(tokens, null, 2)}
6865
+ \`\`\`
6866
+ Tip: run \`design {action:"materialize"}\` to write these tokens to a real theme file.
6867
+ ` : "";
6868
+ return {
6869
+ action,
6870
+ kit: manifest.id,
6871
+ stack: resolvedStack,
6872
+ output: `${header}${tokenBlock}
6873
+ ${body}`
6874
+ };
6875
+ }
6876
+ if (action === "set") {
6877
+ const patch = normalizeOverrides(input.set);
6878
+ if (Object.keys(patch).length === 0) {
6879
+ return { action, output: 'No overrides given. Pass set:{ "primary": "oklch(\u2026)" }.' };
6880
+ }
6881
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
6882
+ if (!merged) {
6883
+ return {
6884
+ action,
6885
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
6886
+ };
6887
+ }
6888
+ setDesignOverrides(ctx, merged);
6889
+ return {
6890
+ action,
6891
+ output: `Overrides updated. Active overrides: ${Object.entries(merged).map(([k, v]) => `${k}=${v}`).join(", ")}
6892
+ These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
6893
+ };
6894
+ }
6895
+ if (action === "materialize") {
6896
+ const active = await loadActiveKit(ctx.projectRoot);
6897
+ if (!active) {
6898
+ return {
6899
+ action,
6900
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
6901
+ };
6902
+ }
6903
+ const resolvedStack = stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : "web");
6904
+ const rawTokens = await loader.readTokens(active.kit);
6905
+ if (!rawTokens) {
6906
+ return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
6907
+ }
6908
+ const tokens = applyTokenOverrides(rawTokens, active.overrides);
6909
+ const result = materializeTokens({
6910
+ tokens,
6911
+ stack: resolvedStack,
6912
+ kitId: active.kit,
6913
+ outPath: input.out
6914
+ });
6915
+ const abs = path.join(ctx.projectRoot, result.path);
6916
+ let exists = false;
6917
+ try {
6918
+ await fs7.access(abs);
6919
+ exists = true;
6920
+ } catch {
6921
+ }
6922
+ if (exists && !input.force) {
6923
+ return {
6924
+ action,
6925
+ kit: active.kit,
6926
+ stack: resolvedStack,
6927
+ path: result.path,
6928
+ output: `${result.path} already exists. Re-run with force:true to overwrite, or write this ${result.format} yourself:
6929
+
6930
+ \`\`\`
6931
+ ${result.content}
6932
+ \`\`\``
6933
+ };
6934
+ }
6935
+ await fs7.mkdir(path.dirname(abs), { recursive: true });
6936
+ await fs7.writeFile(abs, result.content);
6937
+ return {
6938
+ action,
6939
+ kit: active.kit,
6940
+ stack: resolvedStack,
6941
+ path: result.path,
6942
+ output: `Wrote ${result.format} to ${result.path}. Import these tokens in your UI so the kit palette is the source of truth. ${exists ? "(overwrote existing file)" : ""}`
6943
+ };
6944
+ }
6945
+ if (action === "verify") {
6946
+ const active = await loadActiveKit(ctx.projectRoot);
6947
+ if (!active) {
6948
+ return {
6949
+ action,
6950
+ output: 'No active kit to verify against. Pick one: `design {action:"use", kit:"<id>"}`.'
6951
+ };
6952
+ }
6953
+ const rawTokens = await loader.readTokens(active.kit);
6954
+ if (!rawTokens) {
6955
+ return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
6956
+ }
6957
+ const tokens = applyTokenOverrides(rawTokens, active.overrides);
6958
+ const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);
6959
+ const pct = Math.round(report.score * 100);
6960
+ const top = report.violations.slice(0, 25).map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`).join("\n");
6961
+ const summary = `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ${report.violations.length} violation(s).` + (report.violations.length ? `
6962
+ ${top}${report.violations.length > 25 ? `
6963
+ \u2026and ${report.violations.length - 25} more` : ""}
6964
+
6965
+ Replace off-palette colors with kit tokens (or the materialized CSS vars / token utilities).` : "\nNo off-palette colors found \u2014 UI adheres to the kit palette.");
6966
+ return {
6967
+ action,
6968
+ kit: active.kit,
6969
+ output: summary,
6970
+ score: report.score,
6971
+ violations: report.violations.length
6972
+ };
6973
+ }
6974
+ const menu = await loader.menuText();
6975
+ return {
6976
+ action: "list",
6977
+ output: (menu || "No design kits are installed.") + '\n\nLoad one with `design {action:"use", kit:"<id>", stack:"<stack>"}`.'
6978
+ };
6979
+ }
6980
+ };
6511
6981
 
6512
6982
  // src/tool-search.ts
6513
6983
  var toolSearchTool = {
@@ -8039,6 +8509,8 @@ var IndexStore = class {
8039
8509
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)");
8040
8510
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)");
8041
8511
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
8512
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
8513
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
8042
8514
  this.db.exec(`
8043
8515
  CREATE TABLE IF NOT EXISTS refs (
8044
8516
  id INTEGER PRIMARY KEY,
@@ -8951,9 +9423,9 @@ async function syncGoParse(filePath, content, lang) {
8951
9423
  }
8952
9424
  }
8953
9425
  async function parseSymbols3(opts) {
8954
- const { file, lang } = opts;
9426
+ const { file, content, lang } = opts;
8955
9427
  try {
8956
- return await syncPyParse(file, lang);
9428
+ return await syncPyParse(file, content, lang);
8957
9429
  } catch {
8958
9430
  return { file, lang, symbols: [], mtimeMs: Date.now() };
8959
9431
  }
@@ -9021,8 +9493,7 @@ syms = []
9021
9493
  errors = []
9022
9494
 
9023
9495
  try:
9024
- with open(sys.argv[1], "r", encoding="utf-8") as f:
9025
- source = f.read()
9496
+ source = sys.stdin.read()
9026
9497
  tree = ast.parse(source, filename=sys.argv[1])
9027
9498
  except Exception as e:
9028
9499
  errors.append(str(e))
@@ -9162,16 +9633,21 @@ visitor.visit(tree)
9162
9633
 
9163
9634
  print(json.dumps([s.to_dict() for s in syms]))
9164
9635
  `;
9165
- async function syncPyParse(filePath, lang) {
9636
+ var _cachedScriptPath = null;
9637
+ async function syncPyParse(filePath, content, lang) {
9166
9638
  try {
9167
- const tmpDir = path.join(os2.tmpdir(), "ws-py-parse");
9168
- await fs7.mkdir(tmpDir, { recursive: true });
9169
- const scriptPath = path.join(tmpDir, "parse.py");
9170
- await fs7.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
9171
- const proc = spawn("python", [scriptPath, filePath], {
9639
+ if (!_cachedScriptPath) {
9640
+ const tmpDir = path.join(os2.tmpdir(), "ws-py-parse");
9641
+ await fs7.mkdir(tmpDir, { recursive: true });
9642
+ _cachedScriptPath = path.join(tmpDir, "parse.py");
9643
+ await fs7.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9644
+ }
9645
+ const proc = spawn("python", [_cachedScriptPath, filePath], {
9172
9646
  stdio: ["pipe", "pipe", "pipe"],
9173
9647
  windowsHide: true
9174
9648
  });
9649
+ proc.stdin?.write(content);
9650
+ proc.stdin?.end();
9175
9651
  let stdout = "";
9176
9652
  proc.stdout?.on("data", (chunk) => {
9177
9653
  stdout += chunk.toString();
@@ -9838,7 +10314,7 @@ async function parseFile(file, content, lang) {
9838
10314
  case "go":
9839
10315
  return parseSymbols2({ file, content, lang: "go" });
9840
10316
  case "py":
9841
- return parseSymbols3({ file, lang: "py" });
10317
+ return parseSymbols3({ file, content, lang: "py" });
9842
10318
  case "rs":
9843
10319
  return parseSymbols4({ file, content, lang: "rs" });
9844
10320
  case "json":
@@ -9869,10 +10345,12 @@ async function runIndexerWithStore(store, opts) {
9869
10345
  let symbolsIndexed = 0;
9870
10346
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
9871
10347
  let files;
10348
+ let discoveredFiles = null;
9872
10349
  if (opts.files && opts.files.length > 0) {
9873
10350
  files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
9874
10351
  } else {
9875
10352
  files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10353
+ discoveredFiles = new Set(files);
9876
10354
  }
9877
10355
  if (langs && langs.length > 0) {
9878
10356
  const langSet = new Set(langs);
@@ -9990,11 +10468,11 @@ async function runIndexerWithStore(store, opts) {
9990
10468
  filesIndexed++;
9991
10469
  }
9992
10470
  }
9993
- for (const [file_] of existingMeta) {
9994
- try {
9995
- await fs7.stat(file_);
9996
- } catch {
9997
- store.deleteFile(file_);
10471
+ if (discoveredFiles) {
10472
+ for (const [file_] of existingMeta) {
10473
+ if (!discoveredFiles.has(file_)) {
10474
+ store.deleteFile(file_);
10475
+ }
9998
10476
  }
9999
10477
  }
10000
10478
  const durationMs = Date.now() - startMs;
@@ -11031,7 +11509,8 @@ var TIER2_TOOLS = [
11031
11509
  planTool,
11032
11510
  taskTool,
11033
11511
  installTool,
11034
- auditTool
11512
+ auditTool,
11513
+ designTool
11035
11514
  ];
11036
11515
  var TIER3_TOOLS = [
11037
11516
  outdatedTool,
@@ -11076,6 +11555,7 @@ var builtinTools = [
11076
11555
  logsTool,
11077
11556
  documentTool,
11078
11557
  scaffoldTool,
11558
+ designTool,
11079
11559
  toolSearchTool,
11080
11560
  toolUseTool,
11081
11561
  batchToolUseTool,
@@ -11249,6 +11729,6 @@ var TOOL_ICON_CONFIG = {
11249
11729
  };
11250
11730
  var FALLBACK_ICON = "fallback";
11251
11731
 
11252
- export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, configureExecPolicy, createGlobalPsSlashCommand, createModeTool, diffTool, documentTool, editTool, enqueueReindex, ensureSessionShell, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getExecAllowlist, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isExecCommandAllowed, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, normalizeShell, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetExecPolicy, resetIndexCircuitBreaker, resetPersistentProcessRegistry, resolveSessionShell, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
11732
+ export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, configureExecPolicy, createGlobalPsSlashCommand, createModeTool, designTool, diffTool, documentTool, editTool, enqueueReindex, ensureSessionShell, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getExecAllowlist, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isExecCommandAllowed, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, normalizeShell, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetExecPolicy, resetIndexCircuitBreaker, resetPersistentProcessRegistry, resolveSessionShell, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
11253
11733
  //# sourceMappingURL=index.js.map
11254
11734
  //# sourceMappingURL=index.js.map