@wrongstack/tools 0.269.0 → 0.272.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.
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
- import * as fs4 from 'node:fs/promises';
2
- import { toErrorMessage } from '@wrongstack/core/utils';
1
+ import * as fs7 from 'node:fs/promises';
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
4
  import * as path from 'node:path';
4
5
  import { resolve, sep, dirname, join } from 'node:path';
5
- import * as Core from '@wrongstack/core';
6
- import { 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';
7
- import { spawn, execFileSync, spawnSync } from 'node:child_process';
8
- import * as os from 'node:os';
9
- import * as fs7 from 'node:fs';
10
- import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
6
+ import { spawn, execFileSync } from 'node:child_process';
7
+ import * as os2 from 'node:os';
8
+ import * as fs8 from 'node:fs';
9
+ import { statSync, mkdirSync, createWriteStream } from 'node:fs';
11
10
  import * as dns from 'node:dns/promises';
12
11
  import * as net from 'node:net';
13
12
  import { Agent } from 'undici';
14
13
  import TurndownService from 'turndown';
14
+ import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils';
15
15
  import { randomUUID } from 'node:crypto';
16
16
  import { createRequire } from 'node:module';
17
17
  import { fileURLToPath } from 'node:url';
@@ -57,13 +57,13 @@ function safeResolve(input, ctx) {
57
57
  async function assertRealInsideRoot(absPath, ctx) {
58
58
  if (ctx.allowOutsideProjectRoot) return;
59
59
  const realRoots = await Promise.all(
60
- allowedRoots(ctx).map((r) => fs4.realpath(r).catch(() => path.resolve(r)))
60
+ allowedRoots(ctx).map((r) => fs7.realpath(r).catch(() => path.resolve(r)))
61
61
  );
62
62
  let probe = absPath;
63
63
  for (; ; ) {
64
64
  let real;
65
65
  try {
66
- real = await fs4.realpath(probe);
66
+ real = await fs7.realpath(probe);
67
67
  } catch (err) {
68
68
  if (err.code === "ENOENT") {
69
69
  const parent = path.dirname(probe);
@@ -210,7 +210,7 @@ var readTool = {
210
210
  const absPath = await safeResolveReal(input.path, ctx);
211
211
  let stat11;
212
212
  try {
213
- stat11 = await fs4.stat(absPath);
213
+ stat11 = await fs7.stat(absPath);
214
214
  } catch (err) {
215
215
  const code = err.code;
216
216
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -235,7 +235,7 @@ var readTool = {
235
235
  note: "Repeated read suppressed to save tokens."
236
236
  };
237
237
  }
238
- const buf = await fs4.readFile(absPath);
238
+ const buf = await fs7.readFile(absPath);
239
239
  if (isBinaryBuffer(buf)) {
240
240
  throw new Error(`read: "${input.path}" appears to be binary`);
241
241
  }
@@ -369,14 +369,14 @@ var writeTool = {
369
369
  let existed = false;
370
370
  let prev = "";
371
371
  try {
372
- const stat12 = await fs4.stat(absPath);
372
+ const stat12 = await fs7.stat(absPath);
373
373
  existed = stat12.isFile();
374
374
  if (existed) {
375
375
  if (!ctx.hasRead(absPath)) {
376
- prev = await fs4.readFile(absPath, "utf8");
376
+ prev = await fs7.readFile(absPath, "utf8");
377
377
  ctx.recordRead(absPath, stat12.mtimeMs);
378
378
  } else {
379
- prev = await fs4.readFile(absPath, "utf8");
379
+ prev = await fs7.readFile(absPath, "utf8");
380
380
  }
381
381
  }
382
382
  } catch (err) {
@@ -387,7 +387,7 @@ var writeTool = {
387
387
  await atomicWrite(absPath, input.content);
388
388
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
389
389
  + (new file, ${input.content.split("\n").length} lines)`;
390
- const stat11 = await fs4.stat(absPath);
390
+ const stat11 = await fs7.stat(absPath);
391
391
  ctx.recordRead(absPath, stat11.mtimeMs);
392
392
  ctx.session.recordFileChange({
393
393
  path: absPath,
@@ -429,7 +429,7 @@ var editTool = {
429
429
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
430
430
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
431
431
  const absPath = await safeResolveReal(input.path, ctx);
432
- const stat11 = await fs4.stat(absPath).catch((err) => {
432
+ const stat11 = await fs7.stat(absPath).catch((err) => {
433
433
  if (err.code === "ENOENT") {
434
434
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
435
435
  }
@@ -437,8 +437,8 @@ var editTool = {
437
437
  });
438
438
  if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
439
439
  const autoRead = !ctx.hasRead(absPath);
440
- const original = await fs4.readFile(absPath, "utf8");
441
- const updated = await fs4.stat(absPath);
440
+ const original = await fs7.readFile(absPath, "utf8");
441
+ const updated = await fs7.stat(absPath);
442
442
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
443
443
  const lastReadMtime = ctx.lastReadMtime(absPath);
444
444
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -484,7 +484,7 @@ var editTool = {
484
484
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
485
485
  const newFile = toStyle(newFileLf, style);
486
486
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
487
- const written = await fs4.stat(absPath);
487
+ const written = await fs7.stat(absPath);
488
488
  ctx.recordRead(absPath, written.mtimeMs);
489
489
  ctx.session.recordFileChange({
490
490
  path: absPath,
@@ -618,11 +618,11 @@ var replaceTool = {
618
618
  const dryRun = input.dry_run ?? false;
619
619
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
620
620
  const fileList = await resolveFiles(filesInput, ctx, globRe);
621
- const realRoot = await fs4.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
621
+ const realRoot = await fs7.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
622
622
  const results = [];
623
623
  let totalReplacements = 0;
624
624
  for (const absPath of fileList) {
625
- const lstat2 = await fs4.lstat(absPath).catch((err) => {
625
+ const lstat2 = await fs7.lstat(absPath).catch((err) => {
626
626
  if (err.code === "ENOENT") return null;
627
627
  throw err;
628
628
  });
@@ -630,17 +630,17 @@ var replaceTool = {
630
630
  if (lstat2.isSymbolicLink()) continue;
631
631
  let realPath;
632
632
  try {
633
- realPath = await fs4.realpath(absPath);
633
+ realPath = await fs7.realpath(absPath);
634
634
  } catch {
635
635
  continue;
636
636
  }
637
637
  const rel = path.relative(realRoot, realPath);
638
638
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
639
- const stat11 = await fs4.stat(realPath).catch(() => null);
639
+ const stat11 = await fs7.stat(realPath).catch(() => null);
640
640
  if (!stat11 || !stat11.isFile()) continue;
641
641
  let content;
642
642
  try {
643
- const buf = await fs4.readFile(realPath);
643
+ const buf = await fs7.readFile(realPath);
644
644
  if (isBinaryBuffer(buf)) continue;
645
645
  content = buf.toString("utf8");
646
646
  } catch {
@@ -692,7 +692,7 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
692
692
  const resolved = [];
693
693
  for (const p of parts) {
694
694
  const absPath = safeResolve(p, ctx);
695
- const stat11 = await fs4.stat(absPath).catch(() => null);
695
+ const stat11 = await fs7.stat(absPath).catch(() => null);
696
696
  if (stat11?.isFile()) {
697
697
  resolved.push(absPath);
698
698
  }
@@ -748,7 +748,7 @@ async function globNative(pattern, base, extraGlob) {
748
748
  const walk = async (dir) => {
749
749
  let entries;
750
750
  try {
751
- entries = await fs4.readdir(dir, { withFileTypes: true });
751
+ entries = await fs7.readdir(dir, { withFileTypes: true });
752
752
  } catch {
753
753
  return;
754
754
  }
@@ -756,7 +756,7 @@ async function globNative(pattern, base, extraGlob) {
756
756
  if (DEFAULT_IGNORE.includes(e.name)) continue;
757
757
  const full = path.join(dir, e.name);
758
758
  try {
759
- const stat11 = await fs4.lstat(full);
759
+ const stat11 = await fs7.lstat(full);
760
760
  if (stat11.isSymbolicLink()) continue;
761
761
  } catch {
762
762
  continue;
@@ -822,7 +822,7 @@ var globTool = {
822
822
  }
823
823
  let entries;
824
824
  try {
825
- entries = await fs4.readdir(dir, { withFileTypes: true });
825
+ entries = await fs7.readdir(dir, { withFileTypes: true });
826
826
  } catch {
827
827
  return;
828
828
  }
@@ -838,7 +838,7 @@ var globTool = {
838
838
  } else if (e.isFile()) {
839
839
  if (re.test(rel) || re.test(name)) {
840
840
  try {
841
- const st = await fs4.stat(full);
841
+ const st = await fs7.stat(full);
842
842
  results.push({ rel: full, mtime: st.mtimeMs });
843
843
  if (results.length >= limit) {
844
844
  truncated = true;
@@ -857,13 +857,14 @@ var globTool = {
857
857
  };
858
858
  async function readGitignore(dir) {
859
859
  try {
860
- const raw = await fs4.readFile(path.join(dir, ".gitignore"), "utf8");
860
+ const raw = await fs7.readFile(path.join(dir, ".gitignore"), "utf8");
861
861
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
862
862
  } catch {
863
863
  return [];
864
864
  }
865
865
  }
866
866
  var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
867
+ var NATIVE_SCAN_CONCURRENCY = 32;
867
868
  var grepTool = {
868
869
  name: "grep",
869
870
  category: "Search",
@@ -1091,14 +1092,52 @@ async function runNative(input, base, mode, limit, signal) {
1091
1092
  const fileMatches = /* @__PURE__ */ new Map();
1092
1093
  let total = 0;
1093
1094
  let stopped = false;
1095
+ const scanFile = async (full, name) => {
1096
+ if (stopped || signal.aborted) return;
1097
+ if (globRe && !globRe.test(name) && !globRe.test(full)) return;
1098
+ if (globRe) globRe.lastIndex = 0;
1099
+ try {
1100
+ const stat11 = await fs7.stat(full);
1101
+ if (stat11.size > 1e6 || stopped || signal.aborted) return;
1102
+ const head = await fs7.readFile(full);
1103
+ if (isBinaryBuffer(head) || stopped || signal.aborted) return;
1104
+ const text = head.toString("utf8");
1105
+ const lines = text.split(/\r?\n/);
1106
+ let fileHits = 0;
1107
+ for (let i = 0; i < lines.length; i++) {
1108
+ if (stopped || signal.aborted) break;
1109
+ const ln = capSubject(lines[i] ?? "");
1110
+ re.lastIndex = 0;
1111
+ if (re.test(ln)) {
1112
+ fileHits++;
1113
+ total++;
1114
+ if (mode === "content" && matches.length < limit) {
1115
+ matches.push(`${full}:${i + 1}:${ln}`);
1116
+ }
1117
+ }
1118
+ }
1119
+ if (fileHits > 0) {
1120
+ fileMatches.set(full, fileHits);
1121
+ if (mode === "files_with_matches" && matches.length < limit) {
1122
+ matches.push(full);
1123
+ }
1124
+ if (mode === "count" && matches.length < limit) {
1125
+ matches.push(`${full}:${fileHits}`);
1126
+ }
1127
+ }
1128
+ if (matches.length >= limit) stopped = true;
1129
+ } catch {
1130
+ }
1131
+ };
1094
1132
  const walk = async (dir) => {
1095
1133
  if (stopped || signal.aborted) return;
1096
1134
  let entries;
1097
1135
  try {
1098
- entries = await fs4.readdir(dir, { withFileTypes: true });
1136
+ entries = await fs7.readdir(dir, { withFileTypes: true });
1099
1137
  } catch {
1100
1138
  return;
1101
1139
  }
1140
+ const files = [];
1102
1141
  for (const e of entries) {
1103
1142
  if (stopped) return;
1104
1143
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
@@ -1107,41 +1146,10 @@ async function runNative(input, base, mode, limit, signal) {
1107
1146
  if (e.isDirectory()) {
1108
1147
  await walk(full);
1109
1148
  } else if (e.isFile()) {
1110
- if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
1111
- if (globRe) globRe.lastIndex = 0;
1112
- try {
1113
- const stat11 = await fs4.stat(full);
1114
- if (stat11.size > 1e6) continue;
1115
- const head = await fs4.readFile(full);
1116
- if (isBinaryBuffer(head)) continue;
1117
- const text = head.toString("utf8");
1118
- const lines = text.split(/\r?\n/);
1119
- let fileHits = 0;
1120
- for (let i = 0; i < lines.length; i++) {
1121
- const ln = capSubject(lines[i] ?? "");
1122
- re.lastIndex = 0;
1123
- if (re.test(ln)) {
1124
- fileHits++;
1125
- total++;
1126
- if (mode === "content" && matches.length < limit) {
1127
- matches.push(`${full}:${i + 1}:${ln}`);
1128
- }
1129
- }
1130
- }
1131
- if (fileHits > 0) {
1132
- fileMatches.set(full, fileHits);
1133
- if (mode === "files_with_matches" && matches.length < limit) {
1134
- matches.push(full);
1135
- }
1136
- if (mode === "count" && matches.length < limit) {
1137
- matches.push(`${full}:${fileHits}`);
1138
- }
1139
- }
1140
- if (matches.length >= limit) stopped = true;
1141
- } catch {
1142
- }
1149
+ files.push({ full, name: e.name });
1143
1150
  }
1144
1151
  }
1152
+ await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
1145
1153
  };
1146
1154
  await walk(base);
1147
1155
  return {
@@ -1151,6 +1159,20 @@ async function runNative(input, base, mode, limit, signal) {
1151
1159
  used: "native"
1152
1160
  };
1153
1161
  }
1162
+ async function mapWithConcurrency(items, concurrency, fn) {
1163
+ if (items.length === 0) return;
1164
+ let next = 0;
1165
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
1166
+ const workers = Array.from({ length: workerCount }, async () => {
1167
+ for (; ; ) {
1168
+ const idx = next++;
1169
+ if (idx >= items.length) return;
1170
+ const item = items[idx];
1171
+ if (item !== void 0) await fn(item);
1172
+ }
1173
+ });
1174
+ await Promise.all(workers);
1175
+ }
1154
1176
  var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1155
1177
  var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
1156
1178
  var sweepStarted = false;
@@ -1162,13 +1184,13 @@ function sweepOldSpoolFiles(dir) {
1162
1184
  sweepStarted = true;
1163
1185
  void (async () => {
1164
1186
  try {
1165
- const now = Date.now();
1166
- for (const name of await fs4.readdir(dir)) {
1187
+ const now2 = Date.now();
1188
+ for (const name of await fs7.readdir(dir)) {
1167
1189
  if (!name.endsWith(".log")) continue;
1168
1190
  const p = path.join(dir, name);
1169
1191
  try {
1170
- const st = await fs4.stat(p);
1171
- if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs4.unlink(p);
1192
+ const st = await fs7.stat(p);
1193
+ if (now2 - st.mtimeMs > SPOOL_RETENTION_MS) await fs7.unlink(p);
1172
1194
  } catch {
1173
1195
  }
1174
1196
  }
@@ -1323,10 +1345,10 @@ var CircuitBreaker = class {
1323
1345
  */
1324
1346
  snapshot() {
1325
1347
  this._checkStateTransition();
1326
- const now = Date.now();
1348
+ const now2 = Date.now();
1327
1349
  let cooldownRemaining = null;
1328
1350
  if (this.openedAt !== null && this.state === "open") {
1329
- const elapsed = now - this.openedAt;
1351
+ const elapsed = now2 - this.openedAt;
1330
1352
  cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
1331
1353
  }
1332
1354
  return {
@@ -1366,7 +1388,7 @@ var CircuitBreaker = class {
1366
1388
  */
1367
1389
  afterCall(durationMs, failed, bypass = false) {
1368
1390
  if (bypass || !this.enabled) return;
1369
- const now = Date.now();
1391
+ const now2 = Date.now();
1370
1392
  if (this.state === "half-open") {
1371
1393
  if (failed) {
1372
1394
  this._trip();
@@ -1375,12 +1397,12 @@ var CircuitBreaker = class {
1375
1397
  this._reset();
1376
1398
  return;
1377
1399
  }
1378
- this._pruneWindow(now);
1400
+ this._pruneWindow(now2);
1379
1401
  const slow = durationMs >= this.slowCallThresholdMs;
1380
- this.window.push({ at: now, failed, slow });
1402
+ this.window.push({ at: now2, failed, slow });
1381
1403
  if (failed) {
1382
1404
  this.consecutiveFailures++;
1383
- this.lastFailureAt = now;
1405
+ this.lastFailureAt = now2;
1384
1406
  if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
1385
1407
  this._trip();
1386
1408
  }
@@ -1388,7 +1410,7 @@ var CircuitBreaker = class {
1388
1410
  }
1389
1411
  this.consecutiveFailures = 0;
1390
1412
  if (slow) {
1391
- this.lastSlowAt = now;
1413
+ this.lastSlowAt = now2;
1392
1414
  const slowCount = this.window.filter((c) => c.slow).length;
1393
1415
  if (slowCount >= this.maxSlowCalls) {
1394
1416
  this._trip();
@@ -1438,8 +1460,8 @@ var CircuitBreaker = class {
1438
1460
  this.openedAt = null;
1439
1461
  }
1440
1462
  }
1441
- _pruneWindow(now) {
1442
- const cutoff = now - this.windowMs;
1463
+ _pruneWindow(now2) {
1464
+ const cutoff = now2 - this.windowMs;
1443
1465
  this.window = this.window.filter((c) => c.at >= cutoff);
1444
1466
  }
1445
1467
  };
@@ -1477,10 +1499,13 @@ function redactCommand(cmd) {
1477
1499
  var DEFAULT_GRACE_MS = 2e3;
1478
1500
  function killWin32Tree(pid) {
1479
1501
  try {
1480
- spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
1502
+ const child = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
1481
1503
  stdio: "ignore",
1482
1504
  windowsHide: true
1483
- }).unref();
1505
+ });
1506
+ child.on("error", () => {
1507
+ });
1508
+ child.unref();
1484
1509
  return true;
1485
1510
  } catch {
1486
1511
  return false;
@@ -1682,7 +1707,7 @@ var ProcessRegistryImpl = class {
1682
1707
  if (p.killed) return true;
1683
1708
  if (p.protected) return false;
1684
1709
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
1685
- const isWin3 = os.platform() === "win32";
1710
+ const isWin3 = os2.platform() === "win32";
1686
1711
  if (isWin3) {
1687
1712
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
1688
1713
  if (liveRealChild && killWin32Tree(pid)) {
@@ -1772,6 +1797,566 @@ function getProcessRegistry() {
1772
1797
  function _resetProcessRegistry() {
1773
1798
  _registry = void 0;
1774
1799
  }
1800
+ var REGISTRY_FILE = ".wrongstack/process-registry.json";
1801
+ var HEARTBEAT_INTERVAL_MS = 5e3;
1802
+ var STALE_THRESHOLD_MS = 3e4;
1803
+ var LOCKFILE = ".wrongstack/.process-registry.lock";
1804
+ function generateInstanceId() {
1805
+ const hostname4 = os2.hostname();
1806
+ const pid = process.pid;
1807
+ const random = Math.random().toString(36).slice(2, 8);
1808
+ return `${hostname4}:${pid}:${random}`;
1809
+ }
1810
+ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1811
+ const start = Date.now();
1812
+ const pidStr = String(process.pid);
1813
+ const hostStr = os2.hostname();
1814
+ while (Date.now() - start < timeoutMs) {
1815
+ try {
1816
+ await fs7.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
1817
+ return async () => {
1818
+ try {
1819
+ await fs7.unlink(lockfilePath);
1820
+ } catch {
1821
+ }
1822
+ };
1823
+ } catch (err) {
1824
+ if (err.code === "EEXIST") {
1825
+ try {
1826
+ const content = await fs7.readFile(lockfilePath, "utf-8");
1827
+ const parts = content.split(":");
1828
+ const lockPidStr = parts[0] ?? "0";
1829
+ const lockPid = parseInt(lockPidStr, 10);
1830
+ if (process.platform !== "win32") {
1831
+ try {
1832
+ process.kill(lockPid, 0);
1833
+ } catch {
1834
+ await fs7.unlink(lockfilePath);
1835
+ continue;
1836
+ }
1837
+ }
1838
+ } catch {
1839
+ try {
1840
+ await fs7.unlink(lockfilePath);
1841
+ } catch {
1842
+ }
1843
+ }
1844
+ await new Promise((r) => setTimeout(r, 100));
1845
+ continue;
1846
+ }
1847
+ throw err;
1848
+ }
1849
+ }
1850
+ throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
1851
+ }
1852
+ async function readRegistryFile(filePath) {
1853
+ try {
1854
+ const content = await fs7.readFile(filePath, "utf-8");
1855
+ const parsed = JSON.parse(content);
1856
+ if (parsed.instances && Array.isArray(parsed.instances)) {
1857
+ parsed.instances = new Map(parsed.instances);
1858
+ }
1859
+ return parsed;
1860
+ } catch (err) {
1861
+ if (err.code === "ENOENT") {
1862
+ return {
1863
+ version: 1,
1864
+ instances: /* @__PURE__ */ new Map(),
1865
+ protectedPatterns: ["wrongstack", "node"],
1866
+ lastCleanup: Date.now()
1867
+ };
1868
+ }
1869
+ throw err;
1870
+ }
1871
+ }
1872
+ async function writeRegistryFile(filePath, data) {
1873
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
1874
+ const content = JSON.stringify(data, (_k, v) => {
1875
+ if (v instanceof Map) {
1876
+ return Array.from(v.entries());
1877
+ }
1878
+ return v;
1879
+ }, 2);
1880
+ await fs7.writeFile(tmpPath, content, "utf-8");
1881
+ await fs7.rename(tmpPath, filePath);
1882
+ }
1883
+ var PersistentProcessRegistry = class {
1884
+ instanceId;
1885
+ registryPath;
1886
+ lockPath;
1887
+ baseRegistry;
1888
+ heartbeatInterval = null;
1889
+ isShuttingDown = false;
1890
+ constructor(baseRegistry) {
1891
+ this.instanceId = generateInstanceId();
1892
+ const homeDir = os2.homedir();
1893
+ this.registryPath = path.join(homeDir, REGISTRY_FILE);
1894
+ this.lockPath = path.join(homeDir, LOCKFILE);
1895
+ this.baseRegistry = baseRegistry ?? getProcessRegistry();
1896
+ this.ensureDirectory().catch((err) => {
1897
+ console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
1898
+ });
1899
+ }
1900
+ async ensureDirectory() {
1901
+ const dir = path.dirname(this.registryPath);
1902
+ try {
1903
+ await fs7.mkdir(dir, { recursive: true });
1904
+ } catch (err) {
1905
+ if (err.code !== "EEXIST") throw err;
1906
+ }
1907
+ }
1908
+ /**
1909
+ * Start the heartbeat and periodic cleanup tasks.
1910
+ */
1911
+ start() {
1912
+ if (this.heartbeatInterval) return;
1913
+ this.syncToPersistent();
1914
+ this.heartbeatInterval = setInterval(() => {
1915
+ this.heartbeat();
1916
+ }, HEARTBEAT_INTERVAL_MS);
1917
+ this.heartbeatInterval.unref?.();
1918
+ setInterval(() => {
1919
+ this.cleanupStaleEntries();
1920
+ }, STALE_THRESHOLD_MS).unref?.();
1921
+ this.registerMainProcess();
1922
+ process.on("exit", () => this.syncToPersistent());
1923
+ }
1924
+ /**
1925
+ * Stop the heartbeat and clean up.
1926
+ */
1927
+ stop() {
1928
+ this.isShuttingDown = true;
1929
+ if (this.heartbeatInterval) {
1930
+ clearInterval(this.heartbeatInterval);
1931
+ this.heartbeatInterval = null;
1932
+ }
1933
+ this.syncToPersistent();
1934
+ }
1935
+ /**
1936
+ * Register the main WrongStack process as protected.
1937
+ */
1938
+ registerMainProcess() {
1939
+ const mainPid = process.pid;
1940
+ this.updatePersistentEntry({
1941
+ pid: mainPid,
1942
+ name: "wrongstack-main",
1943
+ command: process.argv.slice(0, 3).join(" "),
1944
+ startedAt: Date.now(),
1945
+ lastHeartbeat: Date.now(),
1946
+ instanceId: this.instanceId,
1947
+ hostname: os2.hostname(),
1948
+ protected: true,
1949
+ spawnMode: "main",
1950
+ parentPid: process.ppid,
1951
+ platform: process.platform
1952
+ });
1953
+ }
1954
+ /**
1955
+ * Register a spawned child process with the persistent registry.
1956
+ */
1957
+ registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
1958
+ const entry = {
1959
+ pid,
1960
+ name,
1961
+ command,
1962
+ startedAt: Date.now(),
1963
+ lastHeartbeat: Date.now(),
1964
+ instanceId: this.instanceId,
1965
+ hostname: os2.hostname(),
1966
+ protected: true,
1967
+ // All WrongStack child processes are protected by default
1968
+ spawnMode,
1969
+ parentPid: process.pid,
1970
+ platform: process.platform
1971
+ };
1972
+ if (sessionId) {
1973
+ entry.sessionId = sessionId;
1974
+ }
1975
+ this.updatePersistentEntry(entry);
1976
+ }
1977
+ /**
1978
+ * Update or add an entry in the persistent registry.
1979
+ */
1980
+ async updatePersistentEntry(entry) {
1981
+ const release = await acquireLock(this.lockPath);
1982
+ try {
1983
+ const data = await readRegistryFile(this.registryPath);
1984
+ data.instances.set(String(entry.pid), entry);
1985
+ this.baseRegistry.register({
1986
+ pid: entry.pid,
1987
+ name: entry.name,
1988
+ command: entry.command,
1989
+ startedAt: entry.startedAt,
1990
+ sessionId: entry.sessionId,
1991
+ protected: entry.protected,
1992
+ child: null
1993
+ // Main process has no child handle
1994
+ });
1995
+ await writeRegistryFile(this.registryPath, data);
1996
+ } finally {
1997
+ await release();
1998
+ }
1999
+ }
2000
+ /**
2001
+ * Unregister a process from the persistent registry.
2002
+ */
2003
+ async unregister(pid) {
2004
+ const release = await acquireLock(this.lockPath);
2005
+ try {
2006
+ const data = await readRegistryFile(this.registryPath);
2007
+ data.instances.delete(String(pid));
2008
+ await writeRegistryFile(this.registryPath, data);
2009
+ } finally {
2010
+ await release();
2011
+ }
2012
+ }
2013
+ /**
2014
+ * Send heartbeat to mark all this instance's processes as alive.
2015
+ */
2016
+ heartbeat() {
2017
+ if (this.isShuttingDown) return;
2018
+ this.syncToPersistent();
2019
+ }
2020
+ /**
2021
+ * Sync this instance's processes to the persistent registry.
2022
+ */
2023
+ async syncToPersistent() {
2024
+ const release = await acquireLock(this.lockPath);
2025
+ try {
2026
+ const data = await readRegistryFile(this.registryPath);
2027
+ const now2 = Date.now();
2028
+ const updatedInstances = /* @__PURE__ */ new Map();
2029
+ for (const [_pidStr, entry] of data.instances) {
2030
+ if (entry.instanceId === this.instanceId) {
2031
+ entry.lastHeartbeat = now2;
2032
+ }
2033
+ if (entry.instanceId === this.instanceId || now2 - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
2034
+ updatedInstances.set(_pidStr, entry);
2035
+ }
2036
+ }
2037
+ data.instances = updatedInstances;
2038
+ data.lastCleanup = now2;
2039
+ await writeRegistryFile(this.registryPath, data);
2040
+ } catch (err) {
2041
+ console.error("PersistentProcessRegistry: sync failed", err);
2042
+ } finally {
2043
+ await release();
2044
+ }
2045
+ }
2046
+ /**
2047
+ * Remove entries for processes that are no longer running.
2048
+ */
2049
+ async cleanupStaleEntries() {
2050
+ const release = await acquireLock(this.lockPath);
2051
+ try {
2052
+ const data = await readRegistryFile(this.registryPath);
2053
+ const now2 = Date.now();
2054
+ const stalePids = [];
2055
+ for (const [_pidStr, entry] of data.instances) {
2056
+ const age = now2 - entry.lastHeartbeat;
2057
+ if (age > STALE_THRESHOLD_MS) {
2058
+ try {
2059
+ if (process.platform !== "win32") {
2060
+ process.kill(entry.pid, 0);
2061
+ } else {
2062
+ console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
2063
+ }
2064
+ } catch {
2065
+ stalePids.push(_pidStr);
2066
+ }
2067
+ }
2068
+ }
2069
+ if (stalePids.length > 0) {
2070
+ for (const pidStr of stalePids) {
2071
+ data.instances.delete(pidStr);
2072
+ }
2073
+ await writeRegistryFile(this.registryPath, data);
2074
+ }
2075
+ } catch (err) {
2076
+ console.error("PersistentProcessRegistry: cleanup failed", err);
2077
+ } finally {
2078
+ await release();
2079
+ }
2080
+ }
2081
+ /**
2082
+ * Check if a PID belongs to a WrongStack process and should be protected.
2083
+ */
2084
+ async isProtectedPid(pid) {
2085
+ const release = await acquireLock(this.lockPath);
2086
+ try {
2087
+ const data = await readRegistryFile(this.registryPath);
2088
+ const entry = data.instances.get(String(pid));
2089
+ if (!entry) return false;
2090
+ if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
2091
+ return false;
2092
+ }
2093
+ return entry.protected;
2094
+ } finally {
2095
+ await release();
2096
+ }
2097
+ }
2098
+ /**
2099
+ * Get all protected PIDs from all WrongStack instances.
2100
+ */
2101
+ async getAllProtectedPids() {
2102
+ const release = await acquireLock(this.lockPath);
2103
+ try {
2104
+ const data = await readRegistryFile(this.registryPath);
2105
+ const now2 = Date.now();
2106
+ const protectedPids = [];
2107
+ for (const [_pidStr, entry] of data.instances) {
2108
+ if (entry.protected && now2 - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
2109
+ protectedPids.push(entry.pid);
2110
+ }
2111
+ }
2112
+ return protectedPids;
2113
+ } finally {
2114
+ await release();
2115
+ }
2116
+ }
2117
+ /**
2118
+ * Get complete status of all tracked processes across all instances.
2119
+ */
2120
+ async getGlobalStatus() {
2121
+ const release = await acquireLock(this.lockPath);
2122
+ try {
2123
+ const data = await readRegistryFile(this.registryPath);
2124
+ const now2 = Date.now();
2125
+ const instances = /* @__PURE__ */ new Map();
2126
+ let protectedCount = 0;
2127
+ let staleCount = 0;
2128
+ for (const [_pidStr, entry] of data.instances) {
2129
+ const instanceEntries = instances.get(entry.instanceId) ?? [];
2130
+ instanceEntries.push(entry);
2131
+ instances.set(entry.instanceId, instanceEntries);
2132
+ if (entry.protected) protectedCount++;
2133
+ if (now2 - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
2134
+ }
2135
+ return {
2136
+ instances,
2137
+ totalProcesses: data.instances.size,
2138
+ protectedCount,
2139
+ staleCount
2140
+ };
2141
+ } finally {
2142
+ await release();
2143
+ }
2144
+ }
2145
+ /**
2146
+ * Get the instance ID for this process.
2147
+ */
2148
+ getInstanceId() {
2149
+ return this.instanceId;
2150
+ }
2151
+ /**
2152
+ * Check if a kill command should be blocked.
2153
+ * Returns true if the kill should be blocked (target is a WrongStack process).
2154
+ */
2155
+ async shouldBlockKill(pid) {
2156
+ const protectedPids = await this.getAllProtectedPids();
2157
+ return protectedPids.includes(pid);
2158
+ }
2159
+ /**
2160
+ * Add a pattern-based protection rule.
2161
+ * Processes whose command matches any protected pattern are protected.
2162
+ */
2163
+ async addProtectedPattern(pattern) {
2164
+ const release = await acquireLock(this.lockPath);
2165
+ try {
2166
+ const data = await readRegistryFile(this.registryPath);
2167
+ if (!data.protectedPatterns.includes(pattern)) {
2168
+ data.protectedPatterns.push(pattern);
2169
+ await writeRegistryFile(this.registryPath, data);
2170
+ }
2171
+ } finally {
2172
+ await release();
2173
+ }
2174
+ }
2175
+ };
2176
+ var _persistentRegistry;
2177
+ function getPersistentProcessRegistry() {
2178
+ if (!_persistentRegistry) {
2179
+ _persistentRegistry = new PersistentProcessRegistry();
2180
+ }
2181
+ return _persistentRegistry;
2182
+ }
2183
+ function resetPersistentProcessRegistry() {
2184
+ if (_persistentRegistry) {
2185
+ _persistentRegistry.stop();
2186
+ _persistentRegistry = void 0;
2187
+ }
2188
+ }
2189
+
2190
+ // src/bash-kill-guard.ts
2191
+ function extractKillCommand(command) {
2192
+ const normalized = command.replace(/\s+/g, " ").trim();
2193
+ const shellCMatch = normalized.match(
2194
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
2195
+ );
2196
+ if (shellCMatch?.[1]) {
2197
+ const inner = shellCMatch[1].trim();
2198
+ return isKillRelatedCommand(inner) ? inner : null;
2199
+ }
2200
+ const shellCUnquoted = normalized.match(
2201
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
2202
+ );
2203
+ if (shellCUnquoted?.[1]) {
2204
+ return shellCUnquoted[1];
2205
+ }
2206
+ return null;
2207
+ }
2208
+ function isKillRelatedCommand(cmd) {
2209
+ const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
2210
+ if (/^kill(\s|$)/.test(normalized)) return true;
2211
+ if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
2212
+ if (/^taskkill\s/i.test(normalized)) return true;
2213
+ if (/^tskill\s/i.test(normalized)) return true;
2214
+ if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
2215
+ return false;
2216
+ }
2217
+ function parseKillCommand(command) {
2218
+ const normalized = command.replace(/\s+/g, " ").trim();
2219
+ const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
2220
+ if (simpleMatch) {
2221
+ const signal = simpleMatch[1] ?? "-TERM";
2222
+ const pidOrGroup = simpleMatch[2];
2223
+ if (!pidOrGroup) return null;
2224
+ const isGroupKill = pidOrGroup.startsWith("-");
2225
+ const pid = isGroupKill ? parseInt(pidOrGroup.slice(1), 10) : parseInt(pidOrGroup, 10);
2226
+ return {
2227
+ pid,
2228
+ signal: signal.slice(1),
2229
+ isGroupKill,
2230
+ isAllKill: false,
2231
+ originalCommand: command
2232
+ };
2233
+ }
2234
+ const pkillMatch = normalized.match(/^pkill\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
2235
+ if (pkillMatch?.[2]) {
2236
+ const name = pkillMatch[2];
2237
+ const signalMatch = pkillMatch[1];
2238
+ return {
2239
+ name,
2240
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
2241
+ isGroupKill: false,
2242
+ isAllKill: false,
2243
+ originalCommand: command
2244
+ };
2245
+ }
2246
+ const killallMatch = normalized.match(/^killall\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
2247
+ if (killallMatch?.[2]) {
2248
+ const name = killallMatch[2];
2249
+ const signalMatch = killallMatch[1];
2250
+ return {
2251
+ name,
2252
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
2253
+ isGroupKill: false,
2254
+ isAllKill: false,
2255
+ originalCommand: command
2256
+ };
2257
+ }
2258
+ const pgrepMatch = normalized.match(/^pgrep\s+(.+)$/);
2259
+ if (pgrepMatch) {
2260
+ return null;
2261
+ }
2262
+ const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
2263
+ if (taskkillMatch?.[1]) {
2264
+ const pidStr = taskkillMatch[1];
2265
+ return {
2266
+ pid: parseInt(pidStr, 10),
2267
+ signal: normalized.includes("/F") ? "FORCE" : "TERM",
2268
+ isGroupKill: false,
2269
+ isAllKill: false,
2270
+ originalCommand: command
2271
+ };
2272
+ }
2273
+ const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
2274
+ if (tskillMatch?.[1]) {
2275
+ const pidStr = tskillMatch[1];
2276
+ return {
2277
+ pid: parseInt(pidStr, 10),
2278
+ signal: "TERM",
2279
+ isGroupKill: false,
2280
+ isAllKill: false,
2281
+ originalCommand: command
2282
+ };
2283
+ }
2284
+ return null;
2285
+ }
2286
+ async function getProtectedEntries() {
2287
+ const registry = getPersistentProcessRegistry();
2288
+ const status = await registry.getGlobalStatus();
2289
+ const entries = [];
2290
+ for (const instanceEntries of status.instances.values()) {
2291
+ for (const entry of instanceEntries) {
2292
+ if (entry.protected && Date.now() - entry.lastHeartbeat < 3e4) {
2293
+ entries.push(entry);
2294
+ }
2295
+ }
2296
+ }
2297
+ return entries;
2298
+ }
2299
+ async function isKillProtected(kill) {
2300
+ const registry = getPersistentProcessRegistry();
2301
+ if (kill.name) {
2302
+ const entries = await getProtectedEntries();
2303
+ const killNameLower = kill.name.toLowerCase();
2304
+ for (const entry of entries) {
2305
+ if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
2306
+ return true;
2307
+ }
2308
+ }
2309
+ if (killNameLower.includes("wrongstack")) {
2310
+ return true;
2311
+ }
2312
+ if (killNameLower.includes("node") && entries.length > 0) {
2313
+ return true;
2314
+ }
2315
+ return false;
2316
+ }
2317
+ if (kill.isGroupKill) {
2318
+ const protectedPids = await registry.getAllProtectedPids();
2319
+ return protectedPids.length > 0;
2320
+ }
2321
+ if (kill.pid !== void 0) {
2322
+ return registry.shouldBlockKill(kill.pid);
2323
+ }
2324
+ return false;
2325
+ }
2326
+ async function checkAndBlockKillCommand(command) {
2327
+ const normalized = command.replace(/\s+/g, " ").trim();
2328
+ const killCmd = extractKillCommand(normalized) || (isKillRelatedCommand(normalized) ? normalized : null);
2329
+ if (!killCmd) {
2330
+ return { blocked: false };
2331
+ }
2332
+ const parsed = parseKillCommand(killCmd);
2333
+ if (!parsed) {
2334
+ if (killCmd.includes("kill") && /kill\s+.*\|/.test(killCmd)) {
2335
+ return {
2336
+ blocked: true,
2337
+ reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
2338
+ };
2339
+ }
2340
+ return { blocked: false };
2341
+ }
2342
+ if (await isKillProtected(parsed)) {
2343
+ let target;
2344
+ if (parsed.name) {
2345
+ target = `process name "${parsed.name}"`;
2346
+ } else if (parsed.pid !== void 0) {
2347
+ target = `PID ${parsed.pid}`;
2348
+ } else {
2349
+ target = "(unknown target)";
2350
+ }
2351
+ const signal = parsed.signal ? ` (${parsed.signal})` : "";
2352
+ const groupNote = parsed.isGroupKill ? " (process group)" : "";
2353
+ return {
2354
+ blocked: true,
2355
+ reason: `Blocked: kill${signal} ${target}${groupNote} targets a protected WrongStack process.`
2356
+ };
2357
+ }
2358
+ return { blocked: false };
2359
+ }
1775
2360
 
1776
2361
  // src/bash.ts
1777
2362
  var MAX_OUTPUT = 32768;
@@ -1841,6 +2426,20 @@ var bashTool = {
1841
2426
  };
1842
2427
  return;
1843
2428
  }
2429
+ const killCheck = await checkAndBlockKillCommand(input.command);
2430
+ if (killCheck.blocked) {
2431
+ yield {
2432
+ type: "final",
2433
+ output: {
2434
+ output: "",
2435
+ exit_code: 1,
2436
+ timed_out: false,
2437
+ pid: null,
2438
+ error: killCheck.reason || "Kill command blocked: targets a protected WrongStack process."
2439
+ }
2440
+ };
2441
+ return;
2442
+ }
1844
2443
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
1845
2444
  if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
1846
2445
  console.warn(JSON.stringify({
@@ -1853,7 +2452,7 @@ var bashTool = {
1853
2452
  }));
1854
2453
  }
1855
2454
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1856
- const isWin3 = os.platform() === "win32";
2455
+ const isWin3 = os2.platform() === "win32";
1857
2456
  const shell = (() => {
1858
2457
  const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
1859
2458
  if (explicit) return explicit;
@@ -1884,8 +2483,7 @@ var bashTool = {
1884
2483
  // Windows children survive parent exit either way. POSIX keeps
1885
2484
  // detached for the process-group kill semantics.
1886
2485
  detached: !isWin3,
1887
- windowsHide: true,
1888
- signal: opts.signal
2486
+ windowsHide: true
1889
2487
  });
1890
2488
  const pid2 = child2.pid;
1891
2489
  if (typeof pid2 === "number") {
@@ -1913,7 +2511,17 @@ var bashTool = {
1913
2511
  };
1914
2512
  child2.stdout?.on("data", onBgData);
1915
2513
  child2.stderr?.on("data", onBgData);
2514
+ const cleanupBackground = () => {
2515
+ child2.stdout?.off("data", onBgData);
2516
+ child2.stderr?.off("data", onBgData);
2517
+ };
2518
+ child2.on("error", () => {
2519
+ cleanupBackground();
2520
+ if (typeof pid2 === "number") registry.unregister(pid2);
2521
+ registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
2522
+ });
1916
2523
  child2.on("close", () => {
2524
+ cleanupBackground();
1917
2525
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
1918
2526
  });
1919
2527
  if (typeof pid2 === "number") child2.unref();
@@ -2096,8 +2704,8 @@ var bashTool = {
2096
2704
  };
2097
2705
  return;
2098
2706
  }
2099
- const now = Date.now();
2100
- if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
2707
+ const now2 = Date.now();
2708
+ if (pending2.length >= STREAM_FLUSH_BYTES || now2 - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
2101
2709
  const text = flush();
2102
2710
  if (text) yield { type: "partial_output", text };
2103
2711
  }
@@ -2129,7 +2737,7 @@ function resolveWin32Command(cmd) {
2129
2737
  for (const ext of pathext) {
2130
2738
  const full = `${base}${ext}`;
2131
2739
  try {
2132
- fs7.accessSync(full, fs7.constants.X_OK);
2740
+ fs8.accessSync(full, fs8.constants.X_OK);
2133
2741
  return full;
2134
2742
  } catch {
2135
2743
  }
@@ -2444,8 +3052,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
2444
3052
  );
2445
3053
  }
2446
3054
  var combineSignals = (signals) => AbortSignal.any(signals);
2447
- function guardedLookup(hostname, options, callback) {
2448
- dns.lookup(hostname, { all: true }).then((records) => {
3055
+ function guardedLookup(hostname4, options, callback) {
3056
+ dns.lookup(hostname4, { all: true }).then((records) => {
2449
3057
  const family = options?.family;
2450
3058
  const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
2451
3059
  const list = byFamily.length > 0 ? byFamily : records;
@@ -2472,7 +3080,7 @@ function guardedLookup(hostname, options, callback) {
2472
3080
  const first = list.at(0);
2473
3081
  if (!first) {
2474
3082
  callback(
2475
- Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
3083
+ Object.assign(new Error(`fetch: no address for ${hostname4}`), { code: "ENOTFOUND" })
2476
3084
  );
2477
3085
  return;
2478
3086
  }
@@ -2586,7 +3194,13 @@ var fetchTool = {
2586
3194
  const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
2587
3195
  const combined = combineSignals([opts.signal, ctrl.signal]);
2588
3196
  try {
2589
- const res = await guardedFetch(input.url, 5, combined);
3197
+ let res;
3198
+ try {
3199
+ res = await guardedFetch(input.url, 5, combined);
3200
+ } catch (err) {
3201
+ if (opts.signal.aborted) throw err;
3202
+ throw describeFetchError(err, input.url, ctrl.signal.aborted);
3203
+ }
2590
3204
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
2591
3205
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
2592
3206
  throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
@@ -2642,9 +3256,9 @@ var fetchTool = {
2642
3256
  }
2643
3257
  }
2644
3258
  };
2645
- async function assertNotPrivate(hostname) {
3259
+ async function assertNotPrivate(hostname4) {
2646
3260
  if (ALLOW_PRIVATE) return;
2647
- const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
3261
+ const host = hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
2648
3262
  if (host === "localhost" || host.endsWith(".localhost")) {
2649
3263
  throw new Error("fetch: blocked localhost target");
2650
3264
  }
@@ -2671,6 +3285,23 @@ async function assertNotPrivate(hostname) {
2671
3285
  }
2672
3286
  }
2673
3287
  }
3288
+ function describeFetchError(err, url, timedOut) {
3289
+ if (timedOut) {
3290
+ return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
3291
+ }
3292
+ const parts = [];
3293
+ const seen = /* @__PURE__ */ new Set();
3294
+ let cur = err;
3295
+ while (cur instanceof Error && !seen.has(cur)) {
3296
+ seen.add(cur);
3297
+ const code = cur.code;
3298
+ const label = code ? `${code}: ${cur.message}` : cur.message;
3299
+ if (label && label !== "fetch failed" && !parts.includes(label)) parts.push(label);
3300
+ cur = cur.cause;
3301
+ }
3302
+ const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
3303
+ return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
3304
+ }
2674
3305
  function prettyJson(s) {
2675
3306
  try {
2676
3307
  return JSON.stringify(JSON.parse(s), null, 2);
@@ -2764,7 +3395,7 @@ async function duckduckgoSearch(query2, num, signal) {
2764
3395
  truncated: results.length >= num
2765
3396
  };
2766
3397
  } catch (err) {
2767
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
3398
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
2768
3399
  return {
2769
3400
  query: query2,
2770
3401
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -3223,7 +3854,7 @@ var planTool = {
3223
3854
  const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
3224
3855
  taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
3225
3856
  }
3226
- const now = (/* @__PURE__ */ new Date()).toISOString();
3857
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3227
3858
  try {
3228
3859
  const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
3229
3860
  f.tasks.push({
@@ -3233,8 +3864,8 @@ var planTool = {
3233
3864
  type: "feature",
3234
3865
  priority: "medium",
3235
3866
  status: "pending",
3236
- createdAt: now,
3237
- updatedAt: now
3867
+ createdAt: now2,
3868
+ updatedAt: now2
3238
3869
  });
3239
3870
  return f;
3240
3871
  });
@@ -3580,12 +4211,12 @@ var patchTool = {
3580
4211
  };
3581
4212
  }
3582
4213
  }
3583
- const tmpDir = await fs4.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
4214
+ const tmpDir = await fs7.mkdtemp(path.join(os2.tmpdir(), ".wstack_patch_"));
3584
4215
  try {
3585
- await fs4.chmod(tmpDir, 448).catch(() => {
4216
+ await fs7.chmod(tmpDir, 448).catch(() => {
3586
4217
  });
3587
4218
  const patchFile = path.join(tmpDir, "in.diff");
3588
- await fs4.writeFile(patchFile, input.patch, { mode: 384 });
4219
+ await fs7.writeFile(patchFile, input.patch, { mode: 384 });
3589
4220
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
3590
4221
  const result = await runPatch(args, dir, opts.signal);
3591
4222
  if (result.exitCode !== 0 && !dryRun) {
@@ -3606,7 +4237,7 @@ var patchTool = {
3606
4237
  message: result.stdout || "patch applied"
3607
4238
  };
3608
4239
  } finally {
3609
- await fs4.rm(tmpDir, { recursive: true, force: true }).catch(() => {
4240
+ await fs7.rm(tmpDir, { recursive: true, force: true }).catch(() => {
3610
4241
  });
3611
4242
  }
3612
4243
  }
@@ -3688,7 +4319,7 @@ var jsonTool = {
3688
4319
  let raw;
3689
4320
  if (input.file) {
3690
4321
  try {
3691
- raw = await fs4.readFile(input.file, "utf8");
4322
+ raw = await fs7.readFile(input.file, "utf8");
3692
4323
  } catch {
3693
4324
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
3694
4325
  }
@@ -3727,8 +4358,8 @@ var jsonTool = {
3727
4358
  };
3728
4359
  }
3729
4360
  };
3730
- function query(data, path20) {
3731
- const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
4361
+ function query(data, path21) {
4362
+ const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
3732
4363
  let current = data;
3733
4364
  for (const part of parts) {
3734
4365
  if (current === null || current === void 0) return void 0;
@@ -3903,9 +4534,9 @@ async function fileDiff(input, ctx, _signal) {
3903
4534
  const results = [];
3904
4535
  for (const file of files) {
3905
4536
  const absPath = safeResolve(file, ctx);
3906
- const stat11 = await fs4.stat(absPath).catch(() => null);
4537
+ const stat11 = await fs7.stat(absPath).catch(() => null);
3907
4538
  if (!stat11?.isFile()) continue;
3908
- const content = await fs4.readFile(absPath, "utf8");
4539
+ const content = await fs7.readFile(absPath, "utf8");
3909
4540
  const lines = content.split(/\r?\n/);
3910
4541
  results.push(formatWithLineNumbers(file, lines));
3911
4542
  }
@@ -4062,7 +4693,7 @@ var treeTool = {
4062
4693
  }
4063
4694
  };
4064
4695
  async function walkDir(dir, depth, opts) {
4065
- const entries = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
4696
+ const entries = await fs7.readdir(dir, { withFileTypes: true }).catch(() => []);
4066
4697
  const filtered = entries.filter((e) => {
4067
4698
  if (!opts.showHidden && e.name.startsWith(".")) return false;
4068
4699
  if (opts.exclude.has(e.name)) return false;
@@ -4197,8 +4828,10 @@ async function* spawnStream(opts) {
4197
4828
  queue.push({ kind: "close", data: "", code: 124 });
4198
4829
  wake();
4199
4830
  };
4200
- if (opts.signal.aborted) onAbort();
4201
- else opts.signal.addEventListener("abort", onAbort, { once: true });
4831
+ if (isWin2) {
4832
+ if (opts.signal.aborted) onAbort();
4833
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
4834
+ }
4202
4835
  let exitCode = 0;
4203
4836
  let spawnFailed = false;
4204
4837
  try {
@@ -4242,7 +4875,7 @@ async function* spawnStream(opts) {
4242
4875
  };
4243
4876
  } finally {
4244
4877
  spool.finalize();
4245
- opts.signal.removeEventListener("abort", onAbort);
4878
+ if (isWin2) opts.signal.removeEventListener("abort", onAbort);
4246
4879
  child.stdout?.off("data", onOut);
4247
4880
  child.stderr?.off("data", onErr);
4248
4881
  child.stdout?.destroy();
@@ -5193,7 +5826,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5193
5826
  }
5194
5827
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
5195
5828
  var MAX_TAIL_LINES = 1e5;
5196
- async function fileLogs(path20, lines, filterRe, stream) {
5829
+ async function fileLogs(path21, lines, filterRe, stream) {
5197
5830
  const { createInterface } = await import('node:readline');
5198
5831
  const { createReadStream } = await import('node:fs');
5199
5832
  const entries = [];
@@ -5202,7 +5835,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
5202
5835
  let writeIdx = 0;
5203
5836
  let totalLines = 0;
5204
5837
  const rl = createInterface({
5205
- input: createReadStream(path20),
5838
+ input: createReadStream(path21),
5206
5839
  crlfDelay: Number.POSITIVE_INFINITY
5207
5840
  });
5208
5841
  for await (const line of rl) {
@@ -5223,7 +5856,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
5223
5856
  if (parsed) entries.push(parsed);
5224
5857
  }
5225
5858
  return {
5226
- source: path20,
5859
+ source: path21,
5227
5860
  entries,
5228
5861
  total: entries.length,
5229
5862
  truncated: totalLines > effLines,
@@ -5308,7 +5941,7 @@ var documentTool = {
5308
5941
  const fileList = input.files ? await resolveFiles2(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
5309
5942
  for (const absPath of fileList) {
5310
5943
  try {
5311
- const content = await fs4.readFile(absPath, "utf8");
5944
+ const content = await fs7.readFile(absPath, "utf8");
5312
5945
  filesProcessed++;
5313
5946
  const processed = processFile(
5314
5947
  content,
@@ -5344,7 +5977,7 @@ async function resolveFiles2(filesInput, cwd) {
5344
5977
  for (const f of files) {
5345
5978
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
5346
5979
  try {
5347
- const stat11 = await fs4.stat(absPath);
5980
+ const stat11 = await fs7.stat(absPath);
5348
5981
  if (stat11.isFile()) resolved.push(absPath);
5349
5982
  } catch {
5350
5983
  }
@@ -5568,7 +6201,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
5568
6201
  }
5569
6202
  const fullPath = target;
5570
6203
  if (!dryRun) {
5571
- await fs4.mkdir(path.dirname(fullPath), { recursive: true });
6204
+ await fs7.mkdir(path.dirname(fullPath), { recursive: true });
5572
6205
  await atomicWrite(fullPath, substituteVars(content, name, vars));
5573
6206
  }
5574
6207
  files.push(resolvedPath);
@@ -6236,6 +6869,531 @@ ${mode.description}`
6236
6869
  }
6237
6870
  };
6238
6871
  }
6872
+ var ProcessGuardian = class {
6873
+ registry;
6874
+ config;
6875
+ protectedProcesses = /* @__PURE__ */ new Map();
6876
+ heartbeatTimer = null;
6877
+ isRunning = false;
6878
+ instanceId;
6879
+ constructor(config = {}) {
6880
+ this.registry = getPersistentProcessRegistry();
6881
+ this.config = {
6882
+ heartbeatIntervalMs: config.heartbeatIntervalMs ?? 5e3,
6883
+ autoResurrect: config.autoResurrect ?? false,
6884
+ // Disabled by default
6885
+ maxResurrectionAttempts: config.maxResurrectionAttempts ?? 3,
6886
+ protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"]
6887
+ };
6888
+ this.instanceId = this.registry.getInstanceId();
6889
+ }
6890
+ /**
6891
+ * Start the guardian - begins monitoring and registration.
6892
+ */
6893
+ start() {
6894
+ if (this.isRunning) return;
6895
+ this.isRunning = true;
6896
+ this.registerProcess(process.pid, "wrongstack-main");
6897
+ this.registerExistingChildren();
6898
+ this.heartbeatTimer = setInterval(() => {
6899
+ this.heartbeat();
6900
+ }, this.config.heartbeatIntervalMs);
6901
+ this.heartbeatTimer.unref?.();
6902
+ this.setupProcessHandlers();
6903
+ console.log(JSON.stringify({
6904
+ level: "info",
6905
+ event: "process_guardian.started",
6906
+ instanceId: this.instanceId,
6907
+ mainPid: process.pid,
6908
+ hostname: os2.hostname(),
6909
+ platform: process.platform
6910
+ }));
6911
+ }
6912
+ /**
6913
+ * Stop the guardian gracefully.
6914
+ */
6915
+ stop() {
6916
+ if (!this.isRunning) return;
6917
+ this.isRunning = false;
6918
+ if (this.heartbeatTimer) {
6919
+ clearInterval(this.heartbeatTimer);
6920
+ this.heartbeatTimer = null;
6921
+ }
6922
+ this.registry.stop();
6923
+ console.log(JSON.stringify({
6924
+ level: "info",
6925
+ event: "process_guardian.stopped",
6926
+ instanceId: this.instanceId,
6927
+ mainPid: process.pid
6928
+ }));
6929
+ }
6930
+ /**
6931
+ * Register a process with the guardian.
6932
+ */
6933
+ registerProcess(pid, name) {
6934
+ this.protectedProcesses.set(pid, {
6935
+ pid,
6936
+ name,
6937
+ lastSeen: Date.now(),
6938
+ resurrectionAttempts: 0
6939
+ });
6940
+ this.registry.registerChildProcess(pid, name, name, void 0, "spawn");
6941
+ console.log(JSON.stringify({
6942
+ level: "info",
6943
+ event: "process_guardian.registered",
6944
+ pid,
6945
+ name,
6946
+ instanceId: this.instanceId
6947
+ }));
6948
+ }
6949
+ /**
6950
+ * Unregister a process (e.g., when it exits normally).
6951
+ */
6952
+ unregisterProcess(pid) {
6953
+ this.protectedProcesses.delete(pid);
6954
+ this.registry.unregister(pid).catch((err) => {
6955
+ console.error(JSON.stringify({
6956
+ level: "error",
6957
+ event: "process_guardian.unregister_failed",
6958
+ pid,
6959
+ error: err.message
6960
+ }));
6961
+ });
6962
+ }
6963
+ /**
6964
+ * Register all child processes that already exist.
6965
+ */
6966
+ registerExistingChildren() {
6967
+ this.syncWithProcessRegistry();
6968
+ }
6969
+ /**
6970
+ * Sync protected processes with the base ProcessRegistry.
6971
+ */
6972
+ syncWithProcessRegistry() {
6973
+ }
6974
+ /**
6975
+ * Heartbeat - updates timestamps and checks for anomalies.
6976
+ */
6977
+ heartbeat() {
6978
+ const now2 = Date.now();
6979
+ for (const [_pid, proc] of this.protectedProcesses) {
6980
+ proc.lastSeen = now2;
6981
+ }
6982
+ if (Math.random() < 0.1) {
6983
+ console.log(JSON.stringify({
6984
+ level: "debug",
6985
+ event: "process_guardian.heartbeat",
6986
+ protectedCount: this.protectedProcesses.size,
6987
+ instanceId: this.instanceId
6988
+ }));
6989
+ }
6990
+ }
6991
+ /**
6992
+ * Set up process-level event handlers.
6993
+ */
6994
+ setupProcessHandlers() {
6995
+ process.on("exit", (code) => {
6996
+ console.log(JSON.stringify({
6997
+ level: "info",
6998
+ event: "process_guardian.process_exiting",
6999
+ pid: process.pid,
7000
+ code,
7001
+ instanceId: this.instanceId
7002
+ }));
7003
+ this.stop();
7004
+ });
7005
+ process.on("uncaughtException", (err) => {
7006
+ console.error(JSON.stringify({
7007
+ level: "error",
7008
+ event: "process_guardian.uncaught_exception",
7009
+ error: err.message,
7010
+ stack: err.stack,
7011
+ instanceId: this.instanceId
7012
+ }));
7013
+ });
7014
+ process.on("unhandledRejection", (reason) => {
7015
+ console.error(JSON.stringify({
7016
+ level: "error",
7017
+ event: "process_guardian.unhandled_rejection",
7018
+ reason: String(reason),
7019
+ instanceId: this.instanceId
7020
+ }));
7021
+ });
7022
+ process.on("SIGTERM", (origin) => {
7023
+ console.log(JSON.stringify({
7024
+ level: "warn",
7025
+ event: "process_guardian.sigterm_received",
7026
+ origin,
7027
+ pid: process.pid,
7028
+ instanceId: this.instanceId,
7029
+ message: "SIGTERM received but ignored - use graceful shutdown instead"
7030
+ }));
7031
+ });
7032
+ process.on("SIGHUP", () => {
7033
+ console.log(JSON.stringify({
7034
+ level: "warn",
7035
+ event: "process_guardian.sighup_received",
7036
+ pid: process.pid,
7037
+ instanceId: this.instanceId,
7038
+ message: "SIGHUP received but ignored - WrongStack continues running"
7039
+ }));
7040
+ });
7041
+ }
7042
+ /**
7043
+ * Check if a PID is protected by this guardian.
7044
+ */
7045
+ isProtected(pid) {
7046
+ return this.protectedProcesses.has(pid);
7047
+ }
7048
+ /**
7049
+ * Get all PIDs protected by this guardian.
7050
+ */
7051
+ getProtectedPids() {
7052
+ return Array.from(this.protectedProcesses.keys());
7053
+ }
7054
+ /**
7055
+ * Get status information for monitoring.
7056
+ */
7057
+ getStatus() {
7058
+ return {
7059
+ instanceId: this.instanceId,
7060
+ mainPid: process.pid,
7061
+ protectedCount: this.protectedProcesses.size,
7062
+ platform: os2.platform(),
7063
+ hostname: os2.hostname(),
7064
+ uptime: process.uptime()
7065
+ };
7066
+ }
7067
+ };
7068
+ var _guardian;
7069
+ function getProcessGuardian() {
7070
+ if (!_guardian) {
7071
+ _guardian = new ProcessGuardian();
7072
+ }
7073
+ return _guardian;
7074
+ }
7075
+ function startProcessGuardian(config) {
7076
+ const guardian = new ProcessGuardian(config);
7077
+ guardian.start();
7078
+ _guardian = guardian;
7079
+ return guardian;
7080
+ }
7081
+ function stopProcessGuardian() {
7082
+ if (_guardian) {
7083
+ _guardian.stop();
7084
+ _guardian = void 0;
7085
+ }
7086
+ }
7087
+ var IDLE_THRESHOLD_MS = 2 * 6e4;
7088
+ var STALE_THRESHOLD_MS2 = 5 * 6e4;
7089
+ function now() {
7090
+ return Date.now();
7091
+ }
7092
+ function formatAge(ms) {
7093
+ if (ms < 1e3) return "0s";
7094
+ const seconds = Math.floor(ms / 1e3);
7095
+ if (seconds < 60) return `${seconds}s`;
7096
+ const minutes = Math.floor(seconds / 60);
7097
+ if (minutes < 60) return `${minutes}m`;
7098
+ const hours = Math.floor(minutes / 60);
7099
+ if (hours < 24) return `${hours}h`;
7100
+ const days = Math.floor(hours / 24);
7101
+ return `${days}d`;
7102
+ }
7103
+ function formatUptime(ms) {
7104
+ return formatAge(ms);
7105
+ }
7106
+ function matchGlob(pattern, value) {
7107
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
7108
+ try {
7109
+ const regex = new RegExp(`^${regexPattern}$`, "i");
7110
+ return regex.test(value);
7111
+ } catch {
7112
+ return false;
7113
+ }
7114
+ }
7115
+ async function listInstances(options = {}) {
7116
+ const { includeStale = false, hostname: hostname4, status } = options;
7117
+ const timestamp = now();
7118
+ const registry = getPersistentProcessRegistry();
7119
+ const globalStatus = await registry.getGlobalStatus();
7120
+ const instances = [];
7121
+ const instanceMap = globalStatus.instances;
7122
+ for (const [instanceId, processes] of instanceMap) {
7123
+ if (processes.length === 0) continue;
7124
+ const mainProc = processes.find((p) => p.spawnMode === "main");
7125
+ const firstProc = processes.at(0);
7126
+ const mainPid = mainProc?.pid ?? firstProc?.pid ?? 0;
7127
+ const hostname_ = firstProc?.hostname ?? os2.hostname();
7128
+ const startedAt = Math.min(...processes.map((p) => p.startedAt));
7129
+ const lastActivity = Math.max(...processes.map((p) => p.lastHeartbeat));
7130
+ const age = timestamp - lastActivity;
7131
+ let instanceStatus = "stale";
7132
+ if (age < IDLE_THRESHOLD_MS) instanceStatus = "active";
7133
+ else if (age < STALE_THRESHOLD_MS2) instanceStatus = "idle";
7134
+ const sessionIds = /* @__PURE__ */ new Set();
7135
+ for (const proc of processes) {
7136
+ if (proc.sessionId) {
7137
+ sessionIds.add(proc.sessionId);
7138
+ }
7139
+ }
7140
+ if (!includeStale && instanceStatus === "stale") continue;
7141
+ if (hostname4 && !matchGlob(hostname4, hostname_)) continue;
7142
+ if (status && status !== "all" && instanceStatus !== status) continue;
7143
+ instances.push({
7144
+ instanceId,
7145
+ hostname: hostname_,
7146
+ mainPid,
7147
+ startedAt,
7148
+ lastActivity,
7149
+ status: instanceStatus,
7150
+ processCount: processes.length,
7151
+ processes,
7152
+ sessionIds
7153
+ });
7154
+ }
7155
+ instances.sort((a, b) => b.lastActivity - a.lastActivity);
7156
+ return instances;
7157
+ }
7158
+ async function getInstanceCount() {
7159
+ const instances = await listInstances({ includeStale: true });
7160
+ const byHostname = /* @__PURE__ */ new Map();
7161
+ let active = 0;
7162
+ let idle = 0;
7163
+ let stale = 0;
7164
+ for (const inst of instances) {
7165
+ const current = byHostname.get(inst.hostname) ?? 0;
7166
+ byHostname.set(inst.hostname, current + 1);
7167
+ switch (inst.status) {
7168
+ case "active":
7169
+ active++;
7170
+ break;
7171
+ case "idle":
7172
+ idle++;
7173
+ break;
7174
+ case "stale":
7175
+ stale++;
7176
+ break;
7177
+ }
7178
+ }
7179
+ return {
7180
+ total: instances.length,
7181
+ active,
7182
+ idle,
7183
+ stale,
7184
+ byHostname
7185
+ };
7186
+ }
7187
+ async function getGlobalProcessStatus() {
7188
+ const timestamp = now();
7189
+ const registry = getPersistentProcessRegistry();
7190
+ const globalStatus = await registry.getGlobalStatus();
7191
+ const instances = await listInstances({ includeStale: true });
7192
+ const localInstanceId = registry.getInstanceId();
7193
+ const localInstance = instances.find((i) => i.instanceId === localInstanceId);
7194
+ let localProtectedCount = 0;
7195
+ if (localInstance) {
7196
+ localProtectedCount = localInstance.processes.filter((p) => p.protected).length;
7197
+ }
7198
+ let activeInstanceCount = 0;
7199
+ for (const inst of instances) {
7200
+ if (inst.status === "active") activeInstanceCount++;
7201
+ }
7202
+ return {
7203
+ localInstance: localInstance ? {
7204
+ instanceId: localInstance.instanceId,
7205
+ mainPid: localInstance.mainPid,
7206
+ protectedCount: localProtectedCount,
7207
+ platform: process.platform,
7208
+ hostname: localInstance.hostname,
7209
+ uptime: timestamp - localInstance.startedAt
7210
+ } : {
7211
+ instanceId: localInstanceId,
7212
+ mainPid: process.pid,
7213
+ protectedCount: 0,
7214
+ platform: process.platform,
7215
+ hostname: os2.hostname(),
7216
+ uptime: 0
7217
+ },
7218
+ allInstances: instances.map((inst) => ({
7219
+ instanceId: inst.instanceId,
7220
+ hostname: inst.hostname,
7221
+ mainPid: inst.mainPid,
7222
+ processes: inst.processes,
7223
+ startedAt: inst.startedAt,
7224
+ lastActivity: inst.lastActivity
7225
+ })),
7226
+ summary: {
7227
+ totalProcesses: globalStatus.totalProcesses,
7228
+ protectedCount: globalStatus.protectedCount,
7229
+ staleCount: globalStatus.staleCount,
7230
+ instanceCount: instances.length,
7231
+ activeInstanceCount
7232
+ },
7233
+ timestamp
7234
+ };
7235
+ }
7236
+ async function formatGlobalStatus() {
7237
+ const status = await getGlobalProcessStatus();
7238
+ const lines = [];
7239
+ lines.push("=== WrongStack Global Process Status ===");
7240
+ lines.push(`Updated: ${new Date(status.timestamp).toISOString()}`);
7241
+ lines.push("");
7242
+ lines.push("Summary:");
7243
+ lines.push(` Total processes: ${status.summary.totalProcesses}`);
7244
+ lines.push(` Protected: ${status.summary.protectedCount}`);
7245
+ lines.push(` Stale entries: ${status.summary.staleCount}`);
7246
+ lines.push(` Instances: ${status.summary.instanceCount} (${status.summary.activeInstanceCount} active)`);
7247
+ lines.push("");
7248
+ lines.push(`This instance (${status.localInstance.instanceId}):`);
7249
+ lines.push(` Main PID: ${status.localInstance.mainPid}`);
7250
+ lines.push(` Protected processes: ${status.localInstance.protectedCount}`);
7251
+ lines.push(` Platform: ${status.localInstance.platform} (${status.localInstance.hostname})`);
7252
+ lines.push(` Uptime: ${formatUptime(status.localInstance.uptime)}`);
7253
+ lines.push("");
7254
+ for (const instance of status.allInstances) {
7255
+ if (instance.instanceId === status.localInstance.instanceId) continue;
7256
+ const age = Math.round((status.timestamp - instance.lastActivity) / 1e3);
7257
+ lines.push(`Instance ${instance.instanceId} (${instance.hostname}):`);
7258
+ for (const proc of instance.processes) {
7259
+ const procAge = formatAge(status.timestamp - proc.startedAt);
7260
+ const heartbeatAge = formatAge(status.timestamp - proc.lastHeartbeat);
7261
+ const protected_ = proc.protected ? "[P]" : " ";
7262
+ lines.push(
7263
+ ` ${protected_} ${String(proc.pid).padStart(6)} ${proc.name.padEnd(20)} started ${procAge.padStart(8)} heartbeat ${heartbeatAge.padStart(6)} ${proc.spawnMode}`
7264
+ );
7265
+ }
7266
+ lines.push(` Last activity: ${age}s ago`);
7267
+ lines.push("");
7268
+ }
7269
+ lines.push("Legend:");
7270
+ lines.push(" [P] = Protected (cannot be killed via bash)");
7271
+ lines.push(" main = Main WrongStack process");
7272
+ lines.push(" spawn = Spawned child process");
7273
+ lines.push(" fork = Forked process (e.g., worker threads)");
7274
+ return lines.join("\n");
7275
+ }
7276
+ async function formatInstanceList(options = {}) {
7277
+ const instances = await listInstances(options);
7278
+ const count = await getInstanceCount();
7279
+ const lines = [];
7280
+ lines.push("=== WrongStack Instances ===");
7281
+ lines.push(`Total: ${count.total} instances (${count.active} active, ${count.idle} idle, ${count.stale} stale)`);
7282
+ lines.push("");
7283
+ if (count.byHostname.size > 1) {
7284
+ lines.push("By hostname:");
7285
+ for (const [host, num] of count.byHostname) {
7286
+ lines.push(` ${host}: ${num} instance${num !== 1 ? "s" : ""}`);
7287
+ }
7288
+ lines.push("");
7289
+ }
7290
+ if (instances.length === 0) {
7291
+ lines.push("No instances found matching the filter.");
7292
+ return lines.join("\n");
7293
+ }
7294
+ lines.push("INSTANCES:");
7295
+ lines.push(" " + [
7296
+ "STATUS".padEnd(7),
7297
+ "HOSTNAME".padEnd(16),
7298
+ "MAIN PID".padEnd(9),
7299
+ "PROCS".padEnd(6),
7300
+ "SESSIONS".padEnd(8),
7301
+ "UPTIME".padEnd(8),
7302
+ "LAST ACTIVITY"
7303
+ ].join(" "));
7304
+ lines.push(" " + "-".repeat(80));
7305
+ for (const inst of instances) {
7306
+ const uptime = formatAge(Date.now() - inst.startedAt);
7307
+ const lastAct = formatAge(Date.now() - inst.lastActivity);
7308
+ const statusIcon = inst.status === "active" ? "[*]" : inst.status === "idle" ? "[-]" : "[ ]";
7309
+ lines.push(
7310
+ " " + [
7311
+ `${statusIcon} ${inst.status}`.padEnd(7),
7312
+ inst.hostname.padEnd(16),
7313
+ String(inst.mainPid).padEnd(9),
7314
+ String(inst.processCount).padEnd(6),
7315
+ String(inst.sessionIds.size).padEnd(8),
7316
+ uptime.padEnd(8),
7317
+ `${lastAct} ago`
7318
+ ].join(" ")
7319
+ );
7320
+ }
7321
+ lines.push("");
7322
+ lines.push("Use /ps full for detailed process listing per instance.");
7323
+ return lines.join("\n");
7324
+ }
7325
+ async function formatInstanceSummary() {
7326
+ const count = await getInstanceCount();
7327
+ const instances = await listInstances({ includeStale: false });
7328
+ if (instances.length === 0) {
7329
+ return "No active WrongStack instances.";
7330
+ }
7331
+ const lines = [];
7332
+ lines.push(`${count.total} instance${count.total !== 1 ? "s" : ""}`);
7333
+ const byStatus = /* @__PURE__ */ new Map();
7334
+ for (const inst of instances) {
7335
+ byStatus.set(inst.status, (byStatus.get(inst.status) ?? 0) + 1);
7336
+ }
7337
+ const parts = [];
7338
+ if (byStatus.get("active")) parts.push(`${byStatus.get("active")} active`);
7339
+ if (byStatus.get("idle")) parts.push(`${byStatus.get("idle")} idle`);
7340
+ if (byStatus.get("stale")) parts.push(`${byStatus.get("stale")} stale`);
7341
+ lines.push(`(${parts.join(", ")})`);
7342
+ const totalProcs = instances.reduce((sum, inst) => sum + inst.processCount, 0);
7343
+ lines.push(`${totalProcs} total processes`);
7344
+ return lines.join(" ");
7345
+ }
7346
+ function createGlobalPsSlashCommand() {
7347
+ return {
7348
+ name: "ps",
7349
+ description: "List all WrongStack instances and their processes",
7350
+ async handler(input) {
7351
+ try {
7352
+ const trimmed = input.trim();
7353
+ const parts = trimmed.split(/\s+/);
7354
+ const sub = parts[0]?.toLowerCase() ?? "";
7355
+ if (sub === "list" || sub === "ls" || sub === "") {
7356
+ const output = await formatInstanceList();
7357
+ return { message: output };
7358
+ }
7359
+ if (sub === "summary" || sub === "sum") {
7360
+ const output = await formatInstanceSummary();
7361
+ return { message: output };
7362
+ }
7363
+ if (sub === "full" || sub === "detail") {
7364
+ const output = await formatGlobalStatus();
7365
+ return { message: output };
7366
+ }
7367
+ if (sub === "count" || sub === "num") {
7368
+ const count = await getInstanceCount();
7369
+ return {
7370
+ message: `${count.total} instance${count.total !== 1 ? "s" : ""} (${count.active} active, ${count.idle} idle, ${count.stale} stale)`
7371
+ };
7372
+ }
7373
+ if (sub === "hostname" || sub === "host") {
7374
+ const pattern = parts.slice(1).join(" ");
7375
+ if (!pattern) {
7376
+ return { message: "Usage: /ps hostname <pattern> (e.g., /ps hostname workstation*)" };
7377
+ }
7378
+ const output = await formatInstanceList({ hostname: pattern });
7379
+ return { message: output };
7380
+ }
7381
+ if (sub === "status" || sub === "state") {
7382
+ const filterStatus = parts[1]?.toLowerCase();
7383
+ if (!["active", "idle", "stale", "all"].includes(filterStatus ?? "")) {
7384
+ return { message: "Usage: /ps status <active|idle|stale|all>" };
7385
+ }
7386
+ const output = await formatInstanceList({ status: filterStatus });
7387
+ return { message: output };
7388
+ }
7389
+ return { message: "Usage: /ps [list|summary|count|full|hostname <pattern>|status <state>]" };
7390
+ } catch (err) {
7391
+ const message = err instanceof Error ? err.message : String(err);
7392
+ return { message: `Error getting process status: ${message}` };
7393
+ }
7394
+ }
7395
+ };
7396
+ }
6239
7397
 
6240
7398
  // src/codebase-index/circuit-breaker.ts
6241
7399
  var CircuitOpenError = class extends Error {
@@ -6474,7 +7632,7 @@ function loadDatabaseSync() {
6474
7632
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
6475
7633
  } catch (err) {
6476
7634
  throw new Error(
6477
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
7635
+ `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)}`
6478
7636
  );
6479
7637
  }
6480
7638
  return DatabaseSyncCtor;
@@ -6544,7 +7702,7 @@ var IndexStore = class {
6544
7702
  }
6545
7703
  constructor(projectRoot, opts = {}) {
6546
7704
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
6547
- fs7.mkdirSync(this.indexDir, { recursive: true });
7705
+ fs8.mkdirSync(this.indexDir, { recursive: true });
6548
7706
  const Database = loadDatabaseSync();
6549
7707
  this.db = new Database(path.join(this.indexDir, DB_FILE));
6550
7708
  try {
@@ -6623,33 +7781,53 @@ var IndexStore = class {
6623
7781
  }
6624
7782
  }
6625
7783
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
6626
- insertSymbols(symbols, nextId) {
7784
+ /**
7785
+ * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
7786
+ * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
7787
+ * the same transaction, preventing UNIQUE constraint violations when two
7788
+ * processes index concurrently (each would see a different `MAX(id)` and
7789
+ * neither can insert with the other's IDs).
7790
+ *
7791
+ * @returns The symbols array with `id` fields populated so the caller can
7792
+ * use them for refs without re-reading from the DB.
7793
+ */
7794
+ insertSymbols(symbols) {
6627
7795
  return this.runWithRetry(() => {
6628
- const stmt = this.db.prepare(
6629
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
6630
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
6631
- );
6632
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
6633
- let id = nextId;
6634
- for (const s of symbols) {
6635
- stmt.run(
6636
- id,
6637
- s.lang,
6638
- s.kind,
6639
- s.name,
6640
- s.file,
6641
- s.line,
6642
- s.col,
6643
- s.signature,
6644
- s.docComment,
6645
- s.scope,
6646
- s.text,
6647
- s.file
7796
+ this.db.exec("BEGIN IMMEDIATE");
7797
+ try {
7798
+ const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
7799
+ let nextId = (maxRows[0]?.m ?? 0) + 1;
7800
+ const stmt = this.db.prepare(
7801
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
7802
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
6648
7803
  );
6649
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
6650
- id++;
7804
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
7805
+ const result = [];
7806
+ for (const s of symbols) {
7807
+ const id = nextId++;
7808
+ stmt.run(
7809
+ id,
7810
+ s.lang,
7811
+ s.kind,
7812
+ s.name,
7813
+ s.file,
7814
+ s.line,
7815
+ s.col,
7816
+ s.signature,
7817
+ s.docComment,
7818
+ s.scope,
7819
+ s.text,
7820
+ s.file
7821
+ );
7822
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
7823
+ result.push({ ...s, id });
7824
+ }
7825
+ this.db.exec("COMMIT");
7826
+ return result;
7827
+ } catch (err) {
7828
+ this.db.exec("ROLLBACK");
7829
+ throw err;
6651
7830
  }
6652
- return id;
6653
7831
  });
6654
7832
  }
6655
7833
  deleteSymbolsForFile(file) {
@@ -7007,7 +8185,7 @@ var IndexStore = class {
7007
8185
  sizeBytes() {
7008
8186
  const dbPath = path.join(this.indexDir, DB_FILE);
7009
8187
  try {
7010
- return fs7.statSync(dbPath).size;
8188
+ return fs8.statSync(dbPath).size;
7011
8189
  } catch {
7012
8190
  return 0;
7013
8191
  }
@@ -7203,10 +8381,10 @@ function detectLang(file) {
7203
8381
  if (idx < 0) return null;
7204
8382
  return extToLang(file.slice(idx));
7205
8383
  }
7206
- function parseSymbols2(opts) {
8384
+ async function parseSymbols2(opts) {
7207
8385
  const { file, content, lang } = opts;
7208
8386
  try {
7209
- return syncGoParse(file, content, lang);
8387
+ return await syncGoParse(file, content, lang);
7210
8388
  } catch {
7211
8389
  return { file, lang, symbols: [], mtimeMs: Date.now() };
7212
8390
  }
@@ -7443,19 +8621,34 @@ func formatType(t ast.Expr) string {
7443
8621
  }
7444
8622
  }
7445
8623
  `;
7446
- function syncGoParse(filePath, content, lang) {
7447
- const tmpDir = path.join(os.tmpdir(), "ws-go-parse");
8624
+ async function syncGoParse(filePath, content, lang) {
8625
+ const tmpDir = path.join(os2.tmpdir(), "ws-go-parse");
7448
8626
  try {
7449
- mkdirSync(tmpDir, { recursive: true });
8627
+ await fs7.mkdir(tmpDir, { recursive: true });
7450
8628
  const scriptPath = path.join(tmpDir, "parse.go");
7451
- writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
7452
- const stdout = execFileSync("go", ["run", scriptPath], {
7453
- input: content,
7454
- timeout: 15e3,
7455
- encoding: "utf8",
8629
+ await fs7.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
8630
+ const proc = spawn("go", ["run", scriptPath], {
8631
+ stdio: ["pipe", "pipe", "pipe"],
7456
8632
  windowsHide: true
7457
8633
  });
7458
- if (!stdout.trim()) {
8634
+ let stdout = "";
8635
+ proc.stdout?.on("data", (chunk) => {
8636
+ stdout += chunk.toString();
8637
+ });
8638
+ proc.stdin?.write(content);
8639
+ proc.stdin?.end();
8640
+ const { code } = await Promise.race([
8641
+ new Promise((resolve6) => {
8642
+ proc.on("close", (c) => resolve6({ code: c }));
8643
+ }),
8644
+ new Promise(
8645
+ (_, reject) => setTimeout(() => {
8646
+ proc.kill("SIGKILL");
8647
+ reject(new Error("timeout"));
8648
+ }, 15e3)
8649
+ )
8650
+ ]).catch(() => ({ code: -1 }));
8651
+ if (code !== 0 || !stdout.trim()) {
7459
8652
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7460
8653
  }
7461
8654
  const raw = JSON.parse(stdout.trim());
@@ -7477,10 +8670,10 @@ function syncGoParse(filePath, content, lang) {
7477
8670
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7478
8671
  }
7479
8672
  }
7480
- function parseSymbols3(opts) {
8673
+ async function parseSymbols3(opts) {
7481
8674
  const { file, lang } = opts;
7482
8675
  try {
7483
- return syncPyParse(file, lang);
8676
+ return await syncPyParse(file, lang);
7484
8677
  } catch {
7485
8678
  return { file, lang, symbols: [], mtimeMs: Date.now() };
7486
8679
  }
@@ -7689,18 +8882,32 @@ visitor.visit(tree)
7689
8882
 
7690
8883
  print(json.dumps([s.to_dict() for s in syms]))
7691
8884
  `;
7692
- function syncPyParse(filePath, lang) {
8885
+ async function syncPyParse(filePath, lang) {
7693
8886
  try {
7694
- const tmpDir = path.join(os.tmpdir(), "ws-py-parse");
7695
- mkdirSync(tmpDir, { recursive: true });
8887
+ const tmpDir = path.join(os2.tmpdir(), "ws-py-parse");
8888
+ await fs7.mkdir(tmpDir, { recursive: true });
7696
8889
  const scriptPath = path.join(tmpDir, "parse.py");
7697
- writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
7698
- const stdout = execFileSync("python", [scriptPath, filePath], {
7699
- timeout: 15e3,
7700
- encoding: "utf8",
8890
+ await fs7.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
8891
+ const proc = spawn("python", [scriptPath, filePath], {
8892
+ stdio: ["pipe", "pipe", "pipe"],
7701
8893
  windowsHide: true
7702
8894
  });
7703
- if (!stdout.trim()) {
8895
+ let stdout = "";
8896
+ proc.stdout?.on("data", (chunk) => {
8897
+ stdout += chunk.toString();
8898
+ });
8899
+ const { code } = await Promise.race([
8900
+ new Promise((resolve6) => {
8901
+ proc.on("close", (c) => resolve6({ code: c }));
8902
+ }),
8903
+ new Promise(
8904
+ (_, reject) => setTimeout(() => {
8905
+ proc.kill("SIGKILL");
8906
+ reject(new Error("timeout"));
8907
+ }, 15e3)
8908
+ )
8909
+ ]).catch(() => ({ code: -1 }));
8910
+ if (code !== 0 || !stdout.trim()) {
7704
8911
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7705
8912
  }
7706
8913
  const raw = JSON.parse(stdout.trim());
@@ -7722,11 +8929,11 @@ function syncPyParse(filePath, lang) {
7722
8929
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7723
8930
  }
7724
8931
  }
7725
- function parseSymbols4(opts) {
8932
+ async function parseSymbols4(opts) {
7726
8933
  const { file, content, lang } = opts;
7727
8934
  const nativeAvailable = checkNativeParser();
7728
8935
  if (nativeAvailable) {
7729
- const result = tryNativeParse(file, content);
8936
+ const result = await tryNativeParse(file, content);
7730
8937
  if (result) return result;
7731
8938
  }
7732
8939
  return regexParse({ file, content, lang });
@@ -7756,25 +8963,34 @@ function checkNativeParser() {
7756
8963
  return false;
7757
8964
  }
7758
8965
  }
7759
- function tryNativeParse(file, content) {
8966
+ async function tryNativeParse(file, content) {
7760
8967
  try {
7761
8968
  const toolsDir = path.join(process.cwd(), "tools");
7762
8969
  const crateDir = path.join(toolsDir, "syn-parser");
7763
8970
  const tmpFile = path.join(crateDir, "src", "input.rs");
7764
- writeFileSync(tmpFile, content, "utf8");
7765
- const result = spawnSync(
7766
- "cargo",
7767
- ["run", "--manifest-path", path.join(toolsDir, "Cargo.toml")],
7768
- {
7769
- cwd: process.cwd(),
7770
- encoding: "utf8",
7771
- timeout: 15e3,
7772
- stdio: ["pipe", "pipe", "pipe"],
7773
- windowsHide: true
7774
- }
7775
- );
7776
- if (result.status === 0 && result.stdout) {
7777
- const symbols = JSON.parse(result.stdout);
8971
+ await fs7.writeFile(tmpFile, content, "utf8");
8972
+ const proc = spawn("cargo", ["run", "--manifest-path", path.join(toolsDir, "Cargo.toml")], {
8973
+ cwd: process.cwd(),
8974
+ stdio: ["pipe", "pipe", "pipe"],
8975
+ windowsHide: true
8976
+ });
8977
+ let stdout = "";
8978
+ proc.stdout?.on("data", (chunk) => {
8979
+ stdout += chunk.toString();
8980
+ });
8981
+ const { code } = await Promise.race([
8982
+ new Promise((resolve6) => {
8983
+ proc.on("close", (c) => resolve6({ code: c }));
8984
+ }),
8985
+ new Promise(
8986
+ (_, reject) => setTimeout(() => {
8987
+ proc.kill("SIGKILL");
8988
+ reject(new Error("timeout"));
8989
+ }, 15e3)
8990
+ )
8991
+ ]).catch(() => ({ code: -1 }));
8992
+ if (code === 0 && stdout.trim()) {
8993
+ const symbols = JSON.parse(stdout.trim());
7778
8994
  return {
7779
8995
  file,
7780
8996
  lang: "rs",
@@ -8243,7 +9459,7 @@ function compileGitignore(lines) {
8243
9459
  async function loadGitignoreMatcher(projectRoot) {
8244
9460
  let lines = [];
8245
9461
  try {
8246
- const raw = await fs4.readFile(path.join(projectRoot, ".gitignore"), "utf8");
9462
+ const raw = await fs7.readFile(path.join(projectRoot, ".gitignore"), "utf8");
8247
9463
  lines = raw.split("\n");
8248
9464
  } catch {
8249
9465
  }
@@ -8301,7 +9517,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
8301
9517
  }
8302
9518
  let entries;
8303
9519
  try {
8304
- entries = await fs4.readdir(dir, { withFileTypes: true });
9520
+ entries = await fs7.readdir(dir, { withFileTypes: true });
8305
9521
  } catch {
8306
9522
  return;
8307
9523
  }
@@ -8399,7 +9615,7 @@ async function runIndexerWithStore(store, opts) {
8399
9615
  batchFiles.map(async (file) => {
8400
9616
  let stat11;
8401
9617
  try {
8402
- stat11 = await fs4.stat(file, statOpts);
9618
+ stat11 = await fs7.stat(file, statOpts);
8403
9619
  } catch (e) {
8404
9620
  if (isAbortError(e)) throw e;
8405
9621
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
@@ -8413,7 +9629,7 @@ async function runIndexerWithStore(store, opts) {
8413
9629
  }
8414
9630
  let content;
8415
9631
  try {
8416
- content = await fs4.readFile(file, { encoding: "utf8", signal });
9632
+ content = await fs7.readFile(file, { encoding: "utf8", signal });
8417
9633
  } catch (e) {
8418
9634
  if (isAbortError(e)) throw e;
8419
9635
  return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
@@ -8463,9 +9679,7 @@ async function runIndexerWithStore(store, opts) {
8463
9679
  filesIndexed++;
8464
9680
  continue;
8465
9681
  }
8466
- const nextId = store.getMaxSymbolId() + 1;
8467
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
8468
- store.insertSymbols(symbolsWithIds, nextId);
9682
+ const symbolsWithIds = store.insertSymbols(parsed.symbols);
8469
9683
  const count = symbolsWithIds.length;
8470
9684
  symbolsIndexed += count;
8471
9685
  langStats[lang] = (langStats[lang] ?? 0) + count;
@@ -8494,7 +9708,7 @@ async function runIndexerWithStore(store, opts) {
8494
9708
  }
8495
9709
  for (const [file_] of existingMeta) {
8496
9710
  try {
8497
- await fs4.stat(file_);
9711
+ await fs7.stat(file_);
8498
9712
  } catch {
8499
9713
  store.deleteFile(file_);
8500
9714
  }
@@ -8609,7 +9823,7 @@ function resolveWorkerUrl() {
8609
9823
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8610
9824
  try {
8611
9825
  const url = new URL(rel, import.meta.url);
8612
- if (url.protocol === "file:" && fs7.existsSync(fileURLToPath(url))) return url;
9826
+ if (url.protocol === "file:" && fs8.existsSync(fileURLToPath(url))) return url;
8613
9827
  } catch {
8614
9828
  }
8615
9829
  }
@@ -8767,10 +9981,10 @@ function debounceKey(indexDir, file) {
8767
9981
  function isIndexableFile(filePath) {
8768
9982
  return detectLang(filePath) !== null;
8769
9983
  }
8770
- function isUniqueConstraintError(err) {
9984
+ function isRecoverableConstraintError(err) {
8771
9985
  if (err instanceof Error) {
8772
9986
  const msg = err.message.toLowerCase();
8773
- return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
9987
+ return msg.includes("unique constraint") || msg.includes("constraint failed");
8774
9988
  }
8775
9989
  return false;
8776
9990
  }
@@ -8803,7 +10017,7 @@ async function runStartupIndex(opts) {
8803
10017
  return result;
8804
10018
  } catch (err) {
8805
10019
  _lastError = err instanceof Error ? err.message : String(err);
8806
- if (isUniqueConstraintError(err) && !opts.force) {
10020
+ if (isRecoverableConstraintError(err) && !opts.force) {
8807
10021
  _lastError = null;
8808
10022
  const rebuildResult = await runStartupIndex({
8809
10023
  ...opts,
@@ -9105,11 +10319,11 @@ var setWorkingDirTool = {
9105
10319
  } catch (err) {
9106
10320
  return {
9107
10321
  current: ctx.workingDir,
9108
- error: toErrorMessage(err)
10322
+ error: toErrorMessage$1(err)
9109
10323
  };
9110
10324
  }
9111
10325
  try {
9112
- await fs4.access(resolved);
10326
+ await fs7.access(resolved);
9113
10327
  } catch {
9114
10328
  try {
9115
10329
  ctx.setWorkingDir(previous);
@@ -9280,11 +10494,11 @@ var taskTool = {
9280
10494
  }
9281
10495
  }
9282
10496
  }
9283
- const now = (/* @__PURE__ */ new Date()).toISOString();
10497
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
9284
10498
  f.tasks = input.tasks.map((t) => ({
9285
10499
  ...t,
9286
- createdAt: t.createdAt || now,
9287
- updatedAt: now
10500
+ createdAt: t.createdAt || now2,
10501
+ updatedAt: now2
9288
10502
  }));
9289
10503
  break;
9290
10504
  }
@@ -9308,7 +10522,7 @@ var taskTool = {
9308
10522
  return f;
9309
10523
  }
9310
10524
  }
9311
- const now = (/* @__PURE__ */ new Date()).toISOString();
10525
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
9312
10526
  const newTask = {
9313
10527
  id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
9314
10528
  title: t.title,
@@ -9320,8 +10534,8 @@ var taskTool = {
9320
10534
  assignee: t.assignee,
9321
10535
  estimateHours: t.estimateHours,
9322
10536
  tags: t.tags,
9323
- createdAt: now,
9324
- updatedAt: now
10537
+ createdAt: now2,
10538
+ updatedAt: now2
9325
10539
  };
9326
10540
  f.tasks.push(newTask);
9327
10541
  break;
@@ -9747,6 +10961,6 @@ var TOOL_ICON_CONFIG = {
9747
10961
  };
9748
10962
  var FALLBACK_ICON = "fallback";
9749
10963
 
9750
- 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, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
10964
+ 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, createGlobalPsSlashCommand, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatGlobalStatus, formatInstanceList, formatInstanceSummary, formatTool, getIndexState, getInstanceCount, getPersistentProcessRegistry, getProcessGuardian, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, listInstances, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, resetPersistentProcessRegistry, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, startProcessGuardian, stopProcessGuardian, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
9751
10965
  //# sourceMappingURL=index.js.map
9752
10966
  //# sourceMappingURL=index.js.map