@wrongstack/tools 0.270.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/builtin.js CHANGED
@@ -1,13 +1,13 @@
1
- import { spawn, execFileSync, spawnSync } from 'node:child_process';
1
+ import { spawn, execFileSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
- import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
6
- import * as fs14 from 'node:fs/promises';
5
+ import { statSync, mkdirSync, createWriteStream } from 'node:fs';
6
+ import * as fs2 from 'node:fs/promises';
7
7
  import * as path3 from 'node:path';
8
8
  import { resolve, sep, dirname, join } from 'node:path';
9
- import * as os from 'node:os';
10
- import { toErrorMessage } from '@wrongstack/core/utils';
9
+ import * as os2 from 'node:os';
10
+ import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils';
11
11
  import { createRequire } from 'node:module';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { Worker } from 'node:worker_threads';
@@ -31,12 +31,12 @@ function sweepOldSpoolFiles(dir) {
31
31
  void (async () => {
32
32
  try {
33
33
  const now = Date.now();
34
- for (const name of await fs14.readdir(dir)) {
34
+ for (const name of await fs2.readdir(dir)) {
35
35
  if (!name.endsWith(".log")) continue;
36
36
  const p = path3.join(dir, name);
37
37
  try {
38
- const st = await fs14.stat(p);
39
- if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs14.unlink(p);
38
+ const st = await fs2.stat(p);
39
+ if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs2.unlink(p);
40
40
  } catch {
41
41
  }
42
42
  }
@@ -345,10 +345,13 @@ function redactCommand(cmd) {
345
345
  var DEFAULT_GRACE_MS = 2e3;
346
346
  function killWin32Tree(pid) {
347
347
  try {
348
- spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
348
+ const child = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
349
349
  stdio: "ignore",
350
350
  windowsHide: true
351
- }).unref();
351
+ });
352
+ child.on("error", () => {
353
+ });
354
+ child.unref();
352
355
  return true;
353
356
  } catch {
354
357
  return false;
@@ -550,7 +553,7 @@ var ProcessRegistryImpl = class {
550
553
  if (p.killed) return true;
551
554
  if (p.protected) return false;
552
555
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
553
- const isWin3 = os.platform() === "win32";
556
+ const isWin3 = os2.platform() === "win32";
554
557
  if (isWin3) {
555
558
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
556
559
  if (liveRealChild && killWin32Tree(pid)) {
@@ -756,8 +759,10 @@ async function* spawnStream(opts) {
756
759
  queue.push({ kind: "close", data: "", code: 124 });
757
760
  wake();
758
761
  };
759
- if (opts.signal.aborted) onAbort();
760
- else opts.signal.addEventListener("abort", onAbort, { once: true });
762
+ if (isWin) {
763
+ if (opts.signal.aborted) onAbort();
764
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
765
+ }
761
766
  let exitCode = 0;
762
767
  let spawnFailed = false;
763
768
  try {
@@ -801,7 +806,7 @@ async function* spawnStream(opts) {
801
806
  };
802
807
  } finally {
803
808
  spool.finalize();
804
- opts.signal.removeEventListener("abort", onAbort);
809
+ if (isWin) opts.signal.removeEventListener("abort", onAbort);
805
810
  child.stdout?.off("data", onOut);
806
811
  child.stderr?.off("data", onErr);
807
812
  child.stdout?.destroy();
@@ -856,13 +861,13 @@ function safeResolve(input, ctx) {
856
861
  async function assertRealInsideRoot(absPath, ctx) {
857
862
  if (ctx.allowOutsideProjectRoot) return;
858
863
  const realRoots = await Promise.all(
859
- allowedRoots(ctx).map((r) => fs14.realpath(r).catch(() => path3.resolve(r)))
864
+ allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path3.resolve(r)))
860
865
  );
861
866
  let probe = absPath;
862
867
  for (; ; ) {
863
868
  let real;
864
869
  try {
865
- real = await fs14.realpath(probe);
870
+ real = await fs2.realpath(probe);
866
871
  } catch (err) {
867
872
  if (err.code === "ENOENT") {
868
873
  const parent = path3.dirname(probe);
@@ -1067,6 +1072,560 @@ function parseAuditOutput(json, exitCode) {
1067
1072
  };
1068
1073
  }
1069
1074
  }
1075
+ var REGISTRY_FILE = ".wrongstack/process-registry.json";
1076
+ var HEARTBEAT_INTERVAL_MS = 5e3;
1077
+ var STALE_THRESHOLD_MS = 3e4;
1078
+ var LOCKFILE = ".wrongstack/.process-registry.lock";
1079
+ function generateInstanceId() {
1080
+ const hostname2 = os2.hostname();
1081
+ const pid = process.pid;
1082
+ const random = Math.random().toString(36).slice(2, 8);
1083
+ return `${hostname2}:${pid}:${random}`;
1084
+ }
1085
+ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1086
+ const start = Date.now();
1087
+ const pidStr = String(process.pid);
1088
+ const hostStr = os2.hostname();
1089
+ while (Date.now() - start < timeoutMs) {
1090
+ try {
1091
+ await fs2.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
1092
+ return async () => {
1093
+ try {
1094
+ await fs2.unlink(lockfilePath);
1095
+ } catch {
1096
+ }
1097
+ };
1098
+ } catch (err) {
1099
+ if (err.code === "EEXIST") {
1100
+ try {
1101
+ const content = await fs2.readFile(lockfilePath, "utf-8");
1102
+ const parts = content.split(":");
1103
+ const lockPidStr = parts[0] ?? "0";
1104
+ const lockPid = parseInt(lockPidStr, 10);
1105
+ if (process.platform !== "win32") {
1106
+ try {
1107
+ process.kill(lockPid, 0);
1108
+ } catch {
1109
+ await fs2.unlink(lockfilePath);
1110
+ continue;
1111
+ }
1112
+ }
1113
+ } catch {
1114
+ try {
1115
+ await fs2.unlink(lockfilePath);
1116
+ } catch {
1117
+ }
1118
+ }
1119
+ await new Promise((r) => setTimeout(r, 100));
1120
+ continue;
1121
+ }
1122
+ throw err;
1123
+ }
1124
+ }
1125
+ throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
1126
+ }
1127
+ async function readRegistryFile(filePath) {
1128
+ try {
1129
+ const content = await fs2.readFile(filePath, "utf-8");
1130
+ const parsed = JSON.parse(content);
1131
+ if (parsed.instances && Array.isArray(parsed.instances)) {
1132
+ parsed.instances = new Map(parsed.instances);
1133
+ }
1134
+ return parsed;
1135
+ } catch (err) {
1136
+ if (err.code === "ENOENT") {
1137
+ return {
1138
+ version: 1,
1139
+ instances: /* @__PURE__ */ new Map(),
1140
+ protectedPatterns: ["wrongstack", "node"],
1141
+ lastCleanup: Date.now()
1142
+ };
1143
+ }
1144
+ throw err;
1145
+ }
1146
+ }
1147
+ async function writeRegistryFile(filePath, data) {
1148
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
1149
+ const content = JSON.stringify(data, (_k, v) => {
1150
+ if (v instanceof Map) {
1151
+ return Array.from(v.entries());
1152
+ }
1153
+ return v;
1154
+ }, 2);
1155
+ await fs2.writeFile(tmpPath, content, "utf-8");
1156
+ await fs2.rename(tmpPath, filePath);
1157
+ }
1158
+ var PersistentProcessRegistry = class {
1159
+ instanceId;
1160
+ registryPath;
1161
+ lockPath;
1162
+ baseRegistry;
1163
+ heartbeatInterval = null;
1164
+ isShuttingDown = false;
1165
+ constructor(baseRegistry) {
1166
+ this.instanceId = generateInstanceId();
1167
+ const homeDir = os2.homedir();
1168
+ this.registryPath = path3.join(homeDir, REGISTRY_FILE);
1169
+ this.lockPath = path3.join(homeDir, LOCKFILE);
1170
+ this.baseRegistry = baseRegistry ?? getProcessRegistry();
1171
+ this.ensureDirectory().catch((err) => {
1172
+ console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
1173
+ });
1174
+ }
1175
+ async ensureDirectory() {
1176
+ const dir = path3.dirname(this.registryPath);
1177
+ try {
1178
+ await fs2.mkdir(dir, { recursive: true });
1179
+ } catch (err) {
1180
+ if (err.code !== "EEXIST") throw err;
1181
+ }
1182
+ }
1183
+ /**
1184
+ * Start the heartbeat and periodic cleanup tasks.
1185
+ */
1186
+ start() {
1187
+ if (this.heartbeatInterval) return;
1188
+ this.syncToPersistent();
1189
+ this.heartbeatInterval = setInterval(() => {
1190
+ this.heartbeat();
1191
+ }, HEARTBEAT_INTERVAL_MS);
1192
+ this.heartbeatInterval.unref?.();
1193
+ setInterval(() => {
1194
+ this.cleanupStaleEntries();
1195
+ }, STALE_THRESHOLD_MS).unref?.();
1196
+ this.registerMainProcess();
1197
+ process.on("exit", () => this.syncToPersistent());
1198
+ }
1199
+ /**
1200
+ * Stop the heartbeat and clean up.
1201
+ */
1202
+ stop() {
1203
+ this.isShuttingDown = true;
1204
+ if (this.heartbeatInterval) {
1205
+ clearInterval(this.heartbeatInterval);
1206
+ this.heartbeatInterval = null;
1207
+ }
1208
+ this.syncToPersistent();
1209
+ }
1210
+ /**
1211
+ * Register the main WrongStack process as protected.
1212
+ */
1213
+ registerMainProcess() {
1214
+ const mainPid = process.pid;
1215
+ this.updatePersistentEntry({
1216
+ pid: mainPid,
1217
+ name: "wrongstack-main",
1218
+ command: process.argv.slice(0, 3).join(" "),
1219
+ startedAt: Date.now(),
1220
+ lastHeartbeat: Date.now(),
1221
+ instanceId: this.instanceId,
1222
+ hostname: os2.hostname(),
1223
+ protected: true,
1224
+ spawnMode: "main",
1225
+ parentPid: process.ppid,
1226
+ platform: process.platform
1227
+ });
1228
+ }
1229
+ /**
1230
+ * Register a spawned child process with the persistent registry.
1231
+ */
1232
+ registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
1233
+ const entry = {
1234
+ pid,
1235
+ name,
1236
+ command,
1237
+ startedAt: Date.now(),
1238
+ lastHeartbeat: Date.now(),
1239
+ instanceId: this.instanceId,
1240
+ hostname: os2.hostname(),
1241
+ protected: true,
1242
+ // All WrongStack child processes are protected by default
1243
+ spawnMode,
1244
+ parentPid: process.pid,
1245
+ platform: process.platform
1246
+ };
1247
+ if (sessionId) {
1248
+ entry.sessionId = sessionId;
1249
+ }
1250
+ this.updatePersistentEntry(entry);
1251
+ }
1252
+ /**
1253
+ * Update or add an entry in the persistent registry.
1254
+ */
1255
+ async updatePersistentEntry(entry) {
1256
+ const release = await acquireLock(this.lockPath);
1257
+ try {
1258
+ const data = await readRegistryFile(this.registryPath);
1259
+ data.instances.set(String(entry.pid), entry);
1260
+ this.baseRegistry.register({
1261
+ pid: entry.pid,
1262
+ name: entry.name,
1263
+ command: entry.command,
1264
+ startedAt: entry.startedAt,
1265
+ sessionId: entry.sessionId,
1266
+ protected: entry.protected,
1267
+ child: null
1268
+ // Main process has no child handle
1269
+ });
1270
+ await writeRegistryFile(this.registryPath, data);
1271
+ } finally {
1272
+ await release();
1273
+ }
1274
+ }
1275
+ /**
1276
+ * Unregister a process from the persistent registry.
1277
+ */
1278
+ async unregister(pid) {
1279
+ const release = await acquireLock(this.lockPath);
1280
+ try {
1281
+ const data = await readRegistryFile(this.registryPath);
1282
+ data.instances.delete(String(pid));
1283
+ await writeRegistryFile(this.registryPath, data);
1284
+ } finally {
1285
+ await release();
1286
+ }
1287
+ }
1288
+ /**
1289
+ * Send heartbeat to mark all this instance's processes as alive.
1290
+ */
1291
+ heartbeat() {
1292
+ if (this.isShuttingDown) return;
1293
+ this.syncToPersistent();
1294
+ }
1295
+ /**
1296
+ * Sync this instance's processes to the persistent registry.
1297
+ */
1298
+ async syncToPersistent() {
1299
+ const release = await acquireLock(this.lockPath);
1300
+ try {
1301
+ const data = await readRegistryFile(this.registryPath);
1302
+ const now = Date.now();
1303
+ const updatedInstances = /* @__PURE__ */ new Map();
1304
+ for (const [_pidStr, entry] of data.instances) {
1305
+ if (entry.instanceId === this.instanceId) {
1306
+ entry.lastHeartbeat = now;
1307
+ }
1308
+ if (entry.instanceId === this.instanceId || now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
1309
+ updatedInstances.set(_pidStr, entry);
1310
+ }
1311
+ }
1312
+ data.instances = updatedInstances;
1313
+ data.lastCleanup = now;
1314
+ await writeRegistryFile(this.registryPath, data);
1315
+ } catch (err) {
1316
+ console.error("PersistentProcessRegistry: sync failed", err);
1317
+ } finally {
1318
+ await release();
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Remove entries for processes that are no longer running.
1323
+ */
1324
+ async cleanupStaleEntries() {
1325
+ const release = await acquireLock(this.lockPath);
1326
+ try {
1327
+ const data = await readRegistryFile(this.registryPath);
1328
+ const now = Date.now();
1329
+ const stalePids = [];
1330
+ for (const [_pidStr, entry] of data.instances) {
1331
+ const age = now - entry.lastHeartbeat;
1332
+ if (age > STALE_THRESHOLD_MS) {
1333
+ try {
1334
+ if (process.platform !== "win32") {
1335
+ process.kill(entry.pid, 0);
1336
+ } else {
1337
+ console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
1338
+ }
1339
+ } catch {
1340
+ stalePids.push(_pidStr);
1341
+ }
1342
+ }
1343
+ }
1344
+ if (stalePids.length > 0) {
1345
+ for (const pidStr of stalePids) {
1346
+ data.instances.delete(pidStr);
1347
+ }
1348
+ await writeRegistryFile(this.registryPath, data);
1349
+ }
1350
+ } catch (err) {
1351
+ console.error("PersistentProcessRegistry: cleanup failed", err);
1352
+ } finally {
1353
+ await release();
1354
+ }
1355
+ }
1356
+ /**
1357
+ * Check if a PID belongs to a WrongStack process and should be protected.
1358
+ */
1359
+ async isProtectedPid(pid) {
1360
+ const release = await acquireLock(this.lockPath);
1361
+ try {
1362
+ const data = await readRegistryFile(this.registryPath);
1363
+ const entry = data.instances.get(String(pid));
1364
+ if (!entry) return false;
1365
+ if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
1366
+ return false;
1367
+ }
1368
+ return entry.protected;
1369
+ } finally {
1370
+ await release();
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Get all protected PIDs from all WrongStack instances.
1375
+ */
1376
+ async getAllProtectedPids() {
1377
+ const release = await acquireLock(this.lockPath);
1378
+ try {
1379
+ const data = await readRegistryFile(this.registryPath);
1380
+ const now = Date.now();
1381
+ const protectedPids = [];
1382
+ for (const [_pidStr, entry] of data.instances) {
1383
+ if (entry.protected && now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
1384
+ protectedPids.push(entry.pid);
1385
+ }
1386
+ }
1387
+ return protectedPids;
1388
+ } finally {
1389
+ await release();
1390
+ }
1391
+ }
1392
+ /**
1393
+ * Get complete status of all tracked processes across all instances.
1394
+ */
1395
+ async getGlobalStatus() {
1396
+ const release = await acquireLock(this.lockPath);
1397
+ try {
1398
+ const data = await readRegistryFile(this.registryPath);
1399
+ const now = Date.now();
1400
+ const instances = /* @__PURE__ */ new Map();
1401
+ let protectedCount = 0;
1402
+ let staleCount = 0;
1403
+ for (const [_pidStr, entry] of data.instances) {
1404
+ const instanceEntries = instances.get(entry.instanceId) ?? [];
1405
+ instanceEntries.push(entry);
1406
+ instances.set(entry.instanceId, instanceEntries);
1407
+ if (entry.protected) protectedCount++;
1408
+ if (now - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
1409
+ }
1410
+ return {
1411
+ instances,
1412
+ totalProcesses: data.instances.size,
1413
+ protectedCount,
1414
+ staleCount
1415
+ };
1416
+ } finally {
1417
+ await release();
1418
+ }
1419
+ }
1420
+ /**
1421
+ * Get the instance ID for this process.
1422
+ */
1423
+ getInstanceId() {
1424
+ return this.instanceId;
1425
+ }
1426
+ /**
1427
+ * Check if a kill command should be blocked.
1428
+ * Returns true if the kill should be blocked (target is a WrongStack process).
1429
+ */
1430
+ async shouldBlockKill(pid) {
1431
+ const protectedPids = await this.getAllProtectedPids();
1432
+ return protectedPids.includes(pid);
1433
+ }
1434
+ /**
1435
+ * Add a pattern-based protection rule.
1436
+ * Processes whose command matches any protected pattern are protected.
1437
+ */
1438
+ async addProtectedPattern(pattern) {
1439
+ const release = await acquireLock(this.lockPath);
1440
+ try {
1441
+ const data = await readRegistryFile(this.registryPath);
1442
+ if (!data.protectedPatterns.includes(pattern)) {
1443
+ data.protectedPatterns.push(pattern);
1444
+ await writeRegistryFile(this.registryPath, data);
1445
+ }
1446
+ } finally {
1447
+ await release();
1448
+ }
1449
+ }
1450
+ };
1451
+ var _persistentRegistry;
1452
+ function getPersistentProcessRegistry() {
1453
+ if (!_persistentRegistry) {
1454
+ _persistentRegistry = new PersistentProcessRegistry();
1455
+ }
1456
+ return _persistentRegistry;
1457
+ }
1458
+
1459
+ // src/bash-kill-guard.ts
1460
+ function extractKillCommand(command) {
1461
+ const normalized = command.replace(/\s+/g, " ").trim();
1462
+ const shellCMatch = normalized.match(
1463
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
1464
+ );
1465
+ if (shellCMatch?.[1]) {
1466
+ const inner = shellCMatch[1].trim();
1467
+ return isKillRelatedCommand(inner) ? inner : null;
1468
+ }
1469
+ const shellCUnquoted = normalized.match(
1470
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
1471
+ );
1472
+ if (shellCUnquoted?.[1]) {
1473
+ return shellCUnquoted[1];
1474
+ }
1475
+ return null;
1476
+ }
1477
+ function isKillRelatedCommand(cmd) {
1478
+ const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
1479
+ if (/^kill(\s|$)/.test(normalized)) return true;
1480
+ if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
1481
+ if (/^taskkill\s/i.test(normalized)) return true;
1482
+ if (/^tskill\s/i.test(normalized)) return true;
1483
+ if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
1484
+ return false;
1485
+ }
1486
+ function parseKillCommand(command) {
1487
+ const normalized = command.replace(/\s+/g, " ").trim();
1488
+ const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
1489
+ if (simpleMatch) {
1490
+ const signal = simpleMatch[1] ?? "-TERM";
1491
+ const pidOrGroup = simpleMatch[2];
1492
+ if (!pidOrGroup) return null;
1493
+ const isGroupKill = pidOrGroup.startsWith("-");
1494
+ const pid = isGroupKill ? parseInt(pidOrGroup.slice(1), 10) : parseInt(pidOrGroup, 10);
1495
+ return {
1496
+ pid,
1497
+ signal: signal.slice(1),
1498
+ isGroupKill,
1499
+ isAllKill: false,
1500
+ originalCommand: command
1501
+ };
1502
+ }
1503
+ const pkillMatch = normalized.match(/^pkill\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
1504
+ if (pkillMatch?.[2]) {
1505
+ const name = pkillMatch[2];
1506
+ const signalMatch = pkillMatch[1];
1507
+ return {
1508
+ name,
1509
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
1510
+ isGroupKill: false,
1511
+ isAllKill: false,
1512
+ originalCommand: command
1513
+ };
1514
+ }
1515
+ const killallMatch = normalized.match(/^killall\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
1516
+ if (killallMatch?.[2]) {
1517
+ const name = killallMatch[2];
1518
+ const signalMatch = killallMatch[1];
1519
+ return {
1520
+ name,
1521
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
1522
+ isGroupKill: false,
1523
+ isAllKill: false,
1524
+ originalCommand: command
1525
+ };
1526
+ }
1527
+ const pgrepMatch = normalized.match(/^pgrep\s+(.+)$/);
1528
+ if (pgrepMatch) {
1529
+ return null;
1530
+ }
1531
+ const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
1532
+ if (taskkillMatch?.[1]) {
1533
+ const pidStr = taskkillMatch[1];
1534
+ return {
1535
+ pid: parseInt(pidStr, 10),
1536
+ signal: normalized.includes("/F") ? "FORCE" : "TERM",
1537
+ isGroupKill: false,
1538
+ isAllKill: false,
1539
+ originalCommand: command
1540
+ };
1541
+ }
1542
+ const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
1543
+ if (tskillMatch?.[1]) {
1544
+ const pidStr = tskillMatch[1];
1545
+ return {
1546
+ pid: parseInt(pidStr, 10),
1547
+ signal: "TERM",
1548
+ isGroupKill: false,
1549
+ isAllKill: false,
1550
+ originalCommand: command
1551
+ };
1552
+ }
1553
+ return null;
1554
+ }
1555
+ async function getProtectedEntries() {
1556
+ const registry = getPersistentProcessRegistry();
1557
+ const status = await registry.getGlobalStatus();
1558
+ const entries = [];
1559
+ for (const instanceEntries of status.instances.values()) {
1560
+ for (const entry of instanceEntries) {
1561
+ if (entry.protected && Date.now() - entry.lastHeartbeat < 3e4) {
1562
+ entries.push(entry);
1563
+ }
1564
+ }
1565
+ }
1566
+ return entries;
1567
+ }
1568
+ async function isKillProtected(kill) {
1569
+ const registry = getPersistentProcessRegistry();
1570
+ if (kill.name) {
1571
+ const entries = await getProtectedEntries();
1572
+ const killNameLower = kill.name.toLowerCase();
1573
+ for (const entry of entries) {
1574
+ if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
1575
+ return true;
1576
+ }
1577
+ }
1578
+ if (killNameLower.includes("wrongstack")) {
1579
+ return true;
1580
+ }
1581
+ if (killNameLower.includes("node") && entries.length > 0) {
1582
+ return true;
1583
+ }
1584
+ return false;
1585
+ }
1586
+ if (kill.isGroupKill) {
1587
+ const protectedPids = await registry.getAllProtectedPids();
1588
+ return protectedPids.length > 0;
1589
+ }
1590
+ if (kill.pid !== void 0) {
1591
+ return registry.shouldBlockKill(kill.pid);
1592
+ }
1593
+ return false;
1594
+ }
1595
+ async function checkAndBlockKillCommand(command) {
1596
+ const normalized = command.replace(/\s+/g, " ").trim();
1597
+ const killCmd = extractKillCommand(normalized) || (isKillRelatedCommand(normalized) ? normalized : null);
1598
+ if (!killCmd) {
1599
+ return { blocked: false };
1600
+ }
1601
+ const parsed = parseKillCommand(killCmd);
1602
+ if (!parsed) {
1603
+ if (killCmd.includes("kill") && /kill\s+.*\|/.test(killCmd)) {
1604
+ return {
1605
+ blocked: true,
1606
+ reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
1607
+ };
1608
+ }
1609
+ return { blocked: false };
1610
+ }
1611
+ if (await isKillProtected(parsed)) {
1612
+ let target;
1613
+ if (parsed.name) {
1614
+ target = `process name "${parsed.name}"`;
1615
+ } else if (parsed.pid !== void 0) {
1616
+ target = `PID ${parsed.pid}`;
1617
+ } else {
1618
+ target = "(unknown target)";
1619
+ }
1620
+ const signal = parsed.signal ? ` (${parsed.signal})` : "";
1621
+ const groupNote = parsed.isGroupKill ? " (process group)" : "";
1622
+ return {
1623
+ blocked: true,
1624
+ reason: `Blocked: kill${signal} ${target}${groupNote} targets a protected WrongStack process.`
1625
+ };
1626
+ }
1627
+ return { blocked: false };
1628
+ }
1070
1629
 
1071
1630
  // src/bash.ts
1072
1631
  var MAX_OUTPUT = 32768;
@@ -1136,6 +1695,20 @@ var bashTool = {
1136
1695
  };
1137
1696
  return;
1138
1697
  }
1698
+ const killCheck = await checkAndBlockKillCommand(input.command);
1699
+ if (killCheck.blocked) {
1700
+ yield {
1701
+ type: "final",
1702
+ output: {
1703
+ output: "",
1704
+ exit_code: 1,
1705
+ timed_out: false,
1706
+ pid: null,
1707
+ error: killCheck.reason || "Kill command blocked: targets a protected WrongStack process."
1708
+ }
1709
+ };
1710
+ return;
1711
+ }
1139
1712
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
1140
1713
  if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
1141
1714
  console.warn(JSON.stringify({
@@ -1148,7 +1721,7 @@ var bashTool = {
1148
1721
  }));
1149
1722
  }
1150
1723
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1151
- const isWin3 = os.platform() === "win32";
1724
+ const isWin3 = os2.platform() === "win32";
1152
1725
  const shell = (() => {
1153
1726
  const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
1154
1727
  if (explicit) return explicit;
@@ -1762,7 +2335,7 @@ function loadDatabaseSync() {
1762
2335
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
1763
2336
  } catch (err) {
1764
2337
  throw new Error(
1765
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
2338
+ `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)}`
1766
2339
  );
1767
2340
  }
1768
2341
  return DatabaseSyncCtor;
@@ -1911,33 +2484,53 @@ var IndexStore = class {
1911
2484
  }
1912
2485
  }
1913
2486
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
1914
- insertSymbols(symbols, nextId) {
2487
+ /**
2488
+ * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
2489
+ * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
2490
+ * the same transaction, preventing UNIQUE constraint violations when two
2491
+ * processes index concurrently (each would see a different `MAX(id)` and
2492
+ * neither can insert with the other's IDs).
2493
+ *
2494
+ * @returns The symbols array with `id` fields populated so the caller can
2495
+ * use them for refs without re-reading from the DB.
2496
+ */
2497
+ insertSymbols(symbols) {
1915
2498
  return this.runWithRetry(() => {
1916
- const stmt = this.db.prepare(
1917
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
1918
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1919
- );
1920
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
1921
- let id = nextId;
1922
- for (const s of symbols) {
1923
- stmt.run(
1924
- id,
1925
- s.lang,
1926
- s.kind,
1927
- s.name,
1928
- s.file,
1929
- s.line,
1930
- s.col,
1931
- s.signature,
1932
- s.docComment,
1933
- s.scope,
1934
- s.text,
1935
- s.file
2499
+ this.db.exec("BEGIN IMMEDIATE");
2500
+ try {
2501
+ const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
2502
+ let nextId = (maxRows[0]?.m ?? 0) + 1;
2503
+ const stmt = this.db.prepare(
2504
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
2505
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1936
2506
  );
1937
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
1938
- id++;
2507
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
2508
+ const result = [];
2509
+ for (const s of symbols) {
2510
+ const id = nextId++;
2511
+ stmt.run(
2512
+ id,
2513
+ s.lang,
2514
+ s.kind,
2515
+ s.name,
2516
+ s.file,
2517
+ s.line,
2518
+ s.col,
2519
+ s.signature,
2520
+ s.docComment,
2521
+ s.scope,
2522
+ s.text,
2523
+ s.file
2524
+ );
2525
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
2526
+ result.push({ ...s, id });
2527
+ }
2528
+ this.db.exec("COMMIT");
2529
+ return result;
2530
+ } catch (err) {
2531
+ this.db.exec("ROLLBACK");
2532
+ throw err;
1939
2533
  }
1940
- return id;
1941
2534
  });
1942
2535
  }
1943
2536
  deleteSymbolsForFile(file) {
@@ -2491,10 +3084,10 @@ function detectLang(file) {
2491
3084
  if (idx < 0) return null;
2492
3085
  return extToLang(file.slice(idx));
2493
3086
  }
2494
- function parseSymbols2(opts) {
3087
+ async function parseSymbols2(opts) {
2495
3088
  const { file, content, lang } = opts;
2496
3089
  try {
2497
- return syncGoParse(file, content, lang);
3090
+ return await syncGoParse(file, content, lang);
2498
3091
  } catch {
2499
3092
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2500
3093
  }
@@ -2731,19 +3324,34 @@ func formatType(t ast.Expr) string {
2731
3324
  }
2732
3325
  }
2733
3326
  `;
2734
- function syncGoParse(filePath, content, lang) {
2735
- const tmpDir = path3.join(os.tmpdir(), "ws-go-parse");
3327
+ async function syncGoParse(filePath, content, lang) {
3328
+ const tmpDir = path3.join(os2.tmpdir(), "ws-go-parse");
2736
3329
  try {
2737
- mkdirSync(tmpDir, { recursive: true });
3330
+ await fs2.mkdir(tmpDir, { recursive: true });
2738
3331
  const scriptPath = path3.join(tmpDir, "parse.go");
2739
- writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
2740
- const stdout = execFileSync("go", ["run", scriptPath], {
2741
- input: content,
2742
- timeout: 15e3,
2743
- encoding: "utf8",
3332
+ await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
3333
+ const proc = spawn("go", ["run", scriptPath], {
3334
+ stdio: ["pipe", "pipe", "pipe"],
2744
3335
  windowsHide: true
2745
3336
  });
2746
- if (!stdout.trim()) {
3337
+ let stdout = "";
3338
+ proc.stdout?.on("data", (chunk) => {
3339
+ stdout += chunk.toString();
3340
+ });
3341
+ proc.stdin?.write(content);
3342
+ proc.stdin?.end();
3343
+ const { code } = await Promise.race([
3344
+ new Promise((resolve6) => {
3345
+ proc.on("close", (c) => resolve6({ code: c }));
3346
+ }),
3347
+ new Promise(
3348
+ (_, reject) => setTimeout(() => {
3349
+ proc.kill("SIGKILL");
3350
+ reject(new Error("timeout"));
3351
+ }, 15e3)
3352
+ )
3353
+ ]).catch(() => ({ code: -1 }));
3354
+ if (code !== 0 || !stdout.trim()) {
2747
3355
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2748
3356
  }
2749
3357
  const raw = JSON.parse(stdout.trim());
@@ -2765,10 +3373,10 @@ function syncGoParse(filePath, content, lang) {
2765
3373
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2766
3374
  }
2767
3375
  }
2768
- function parseSymbols3(opts) {
3376
+ async function parseSymbols3(opts) {
2769
3377
  const { file, lang } = opts;
2770
3378
  try {
2771
- return syncPyParse(file, lang);
3379
+ return await syncPyParse(file, lang);
2772
3380
  } catch {
2773
3381
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2774
3382
  }
@@ -2977,18 +3585,32 @@ visitor.visit(tree)
2977
3585
 
2978
3586
  print(json.dumps([s.to_dict() for s in syms]))
2979
3587
  `;
2980
- function syncPyParse(filePath, lang) {
3588
+ async function syncPyParse(filePath, lang) {
2981
3589
  try {
2982
- const tmpDir = path3.join(os.tmpdir(), "ws-py-parse");
2983
- mkdirSync(tmpDir, { recursive: true });
3590
+ const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
3591
+ await fs2.mkdir(tmpDir, { recursive: true });
2984
3592
  const scriptPath = path3.join(tmpDir, "parse.py");
2985
- writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
2986
- const stdout = execFileSync("python", [scriptPath, filePath], {
2987
- timeout: 15e3,
2988
- encoding: "utf8",
3593
+ await fs2.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
3594
+ const proc = spawn("python", [scriptPath, filePath], {
3595
+ stdio: ["pipe", "pipe", "pipe"],
2989
3596
  windowsHide: true
2990
3597
  });
2991
- if (!stdout.trim()) {
3598
+ let stdout = "";
3599
+ proc.stdout?.on("data", (chunk) => {
3600
+ stdout += chunk.toString();
3601
+ });
3602
+ const { code } = await Promise.race([
3603
+ new Promise((resolve6) => {
3604
+ proc.on("close", (c) => resolve6({ code: c }));
3605
+ }),
3606
+ new Promise(
3607
+ (_, reject) => setTimeout(() => {
3608
+ proc.kill("SIGKILL");
3609
+ reject(new Error("timeout"));
3610
+ }, 15e3)
3611
+ )
3612
+ ]).catch(() => ({ code: -1 }));
3613
+ if (code !== 0 || !stdout.trim()) {
2992
3614
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2993
3615
  }
2994
3616
  const raw = JSON.parse(stdout.trim());
@@ -3010,11 +3632,11 @@ function syncPyParse(filePath, lang) {
3010
3632
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
3011
3633
  }
3012
3634
  }
3013
- function parseSymbols4(opts) {
3635
+ async function parseSymbols4(opts) {
3014
3636
  const { file, content, lang } = opts;
3015
3637
  const nativeAvailable = checkNativeParser();
3016
3638
  if (nativeAvailable) {
3017
- const result = tryNativeParse(file, content);
3639
+ const result = await tryNativeParse(file, content);
3018
3640
  if (result) return result;
3019
3641
  }
3020
3642
  return regexParse({ file, content, lang });
@@ -3044,25 +3666,34 @@ function checkNativeParser() {
3044
3666
  return false;
3045
3667
  }
3046
3668
  }
3047
- function tryNativeParse(file, content) {
3669
+ async function tryNativeParse(file, content) {
3048
3670
  try {
3049
3671
  const toolsDir = path3.join(process.cwd(), "tools");
3050
3672
  const crateDir = path3.join(toolsDir, "syn-parser");
3051
3673
  const tmpFile = path3.join(crateDir, "src", "input.rs");
3052
- writeFileSync(tmpFile, content, "utf8");
3053
- const result = spawnSync(
3054
- "cargo",
3055
- ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")],
3056
- {
3057
- cwd: process.cwd(),
3058
- encoding: "utf8",
3059
- timeout: 15e3,
3060
- stdio: ["pipe", "pipe", "pipe"],
3061
- windowsHide: true
3062
- }
3063
- );
3064
- if (result.status === 0 && result.stdout) {
3065
- const symbols = JSON.parse(result.stdout);
3674
+ await fs2.writeFile(tmpFile, content, "utf8");
3675
+ const proc = spawn("cargo", ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")], {
3676
+ cwd: process.cwd(),
3677
+ stdio: ["pipe", "pipe", "pipe"],
3678
+ windowsHide: true
3679
+ });
3680
+ let stdout = "";
3681
+ proc.stdout?.on("data", (chunk) => {
3682
+ stdout += chunk.toString();
3683
+ });
3684
+ const { code } = await Promise.race([
3685
+ new Promise((resolve6) => {
3686
+ proc.on("close", (c) => resolve6({ code: c }));
3687
+ }),
3688
+ new Promise(
3689
+ (_, reject) => setTimeout(() => {
3690
+ proc.kill("SIGKILL");
3691
+ reject(new Error("timeout"));
3692
+ }, 15e3)
3693
+ )
3694
+ ]).catch(() => ({ code: -1 }));
3695
+ if (code === 0 && stdout.trim()) {
3696
+ const symbols = JSON.parse(stdout.trim());
3066
3697
  return {
3067
3698
  file,
3068
3699
  lang: "rs",
@@ -3531,7 +4162,7 @@ function compileGitignore(lines) {
3531
4162
  async function loadGitignoreMatcher(projectRoot) {
3532
4163
  let lines = [];
3533
4164
  try {
3534
- const raw = await fs14.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
4165
+ const raw = await fs2.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
3535
4166
  lines = raw.split("\n");
3536
4167
  } catch {
3537
4168
  }
@@ -3589,7 +4220,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
3589
4220
  }
3590
4221
  let entries;
3591
4222
  try {
3592
- entries = await fs14.readdir(dir, { withFileTypes: true });
4223
+ entries = await fs2.readdir(dir, { withFileTypes: true });
3593
4224
  } catch {
3594
4225
  return;
3595
4226
  }
@@ -3687,7 +4318,7 @@ async function runIndexerWithStore(store, opts) {
3687
4318
  batchFiles.map(async (file) => {
3688
4319
  let stat11;
3689
4320
  try {
3690
- stat11 = await fs14.stat(file, statOpts);
4321
+ stat11 = await fs2.stat(file, statOpts);
3691
4322
  } catch (e) {
3692
4323
  if (isAbortError(e)) throw e;
3693
4324
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
@@ -3701,7 +4332,7 @@ async function runIndexerWithStore(store, opts) {
3701
4332
  }
3702
4333
  let content;
3703
4334
  try {
3704
- content = await fs14.readFile(file, { encoding: "utf8", signal });
4335
+ content = await fs2.readFile(file, { encoding: "utf8", signal });
3705
4336
  } catch (e) {
3706
4337
  if (isAbortError(e)) throw e;
3707
4338
  return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
@@ -3751,9 +4382,7 @@ async function runIndexerWithStore(store, opts) {
3751
4382
  filesIndexed++;
3752
4383
  continue;
3753
4384
  }
3754
- const nextId = store.getMaxSymbolId() + 1;
3755
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
3756
- store.insertSymbols(symbolsWithIds, nextId);
4385
+ const symbolsWithIds = store.insertSymbols(parsed.symbols);
3757
4386
  const count = symbolsWithIds.length;
3758
4387
  symbolsIndexed += count;
3759
4388
  langStats[lang] = (langStats[lang] ?? 0) + count;
@@ -3782,7 +4411,7 @@ async function runIndexerWithStore(store, opts) {
3782
4411
  }
3783
4412
  for (const [file_] of existingMeta) {
3784
4413
  try {
3785
- await fs14.stat(file_);
4414
+ await fs2.stat(file_);
3786
4415
  } catch {
3787
4416
  store.deleteFile(file_);
3788
4417
  }
@@ -4032,10 +4661,10 @@ function circuitOpenError() {
4032
4661
  "Codebase indexing is temporarily paused after repeated failures" + (c.lastFailure ? ` (last: ${c.lastFailure})` : "") + (c.cooldownRemainingMs > 0 ? `; auto-retry in ${Math.ceil(c.cooldownRemainingMs / 1e3)}s` : "") + ". Use /codebase-reindex to retry now."
4033
4662
  );
4034
4663
  }
4035
- function isUniqueConstraintError(err) {
4664
+ function isRecoverableConstraintError(err) {
4036
4665
  if (err instanceof Error) {
4037
4666
  const msg = err.message.toLowerCase();
4038
- return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
4667
+ return msg.includes("unique constraint") || msg.includes("constraint failed");
4039
4668
  }
4040
4669
  return false;
4041
4670
  }
@@ -4068,7 +4697,7 @@ async function runStartupIndex(opts) {
4068
4697
  return result;
4069
4698
  } catch (err) {
4070
4699
  _lastError = err instanceof Error ? err.message : String(err);
4071
- if (isUniqueConstraintError(err) && !opts.force) {
4700
+ if (isRecoverableConstraintError(err) && !opts.force) {
4072
4701
  _lastError = null;
4073
4702
  const rebuildResult = await runStartupIndex({
4074
4703
  ...opts,
@@ -4428,9 +5057,9 @@ async function fileDiff(input, ctx, _signal) {
4428
5057
  const results = [];
4429
5058
  for (const file of files) {
4430
5059
  const absPath = safeResolve(file, ctx);
4431
- const stat11 = await fs14.stat(absPath).catch(() => null);
5060
+ const stat11 = await fs2.stat(absPath).catch(() => null);
4432
5061
  if (!stat11?.isFile()) continue;
4433
- const content = await fs14.readFile(absPath, "utf8");
5062
+ const content = await fs2.readFile(absPath, "utf8");
4434
5063
  const lines = content.split(/\r?\n/);
4435
5064
  results.push(formatWithLineNumbers(file, lines));
4436
5065
  }
@@ -4490,7 +5119,7 @@ var documentTool = {
4490
5119
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
4491
5120
  for (const absPath of fileList) {
4492
5121
  try {
4493
- const content = await fs14.readFile(absPath, "utf8");
5122
+ const content = await fs2.readFile(absPath, "utf8");
4494
5123
  filesProcessed++;
4495
5124
  const processed = processFile(
4496
5125
  content,
@@ -4526,7 +5155,7 @@ async function resolveFiles(filesInput, cwd) {
4526
5155
  for (const f of files) {
4527
5156
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
4528
5157
  try {
4529
- const stat11 = await fs14.stat(absPath);
5158
+ const stat11 = await fs2.stat(absPath);
4530
5159
  if (stat11.isFile()) resolved.push(absPath);
4531
5160
  } catch {
4532
5161
  }
@@ -4619,7 +5248,7 @@ var editTool = {
4619
5248
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
4620
5249
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
4621
5250
  const absPath = await safeResolveReal(input.path, ctx);
4622
- const stat11 = await fs14.stat(absPath).catch((err) => {
5251
+ const stat11 = await fs2.stat(absPath).catch((err) => {
4623
5252
  if (err.code === "ENOENT") {
4624
5253
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
4625
5254
  }
@@ -4627,8 +5256,8 @@ var editTool = {
4627
5256
  });
4628
5257
  if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
4629
5258
  const autoRead = !ctx.hasRead(absPath);
4630
- const original = await fs14.readFile(absPath, "utf8");
4631
- const updated = await fs14.stat(absPath);
5259
+ const original = await fs2.readFile(absPath, "utf8");
5260
+ const updated = await fs2.stat(absPath);
4632
5261
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
4633
5262
  const lastReadMtime = ctx.lastReadMtime(absPath);
4634
5263
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -4674,7 +5303,7 @@ var editTool = {
4674
5303
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
4675
5304
  const newFile = toStyle(newFileLf, style);
4676
5305
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
4677
- const written = await fs14.stat(absPath);
5306
+ const written = await fs2.stat(absPath);
4678
5307
  ctx.recordRead(absPath, written.mtimeMs);
4679
5308
  ctx.session.recordFileChange({
4680
5309
  path: absPath,
@@ -5023,8 +5652,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
5023
5652
  );
5024
5653
  }
5025
5654
  var combineSignals = (signals) => AbortSignal.any(signals);
5026
- function guardedLookup(hostname, options, callback) {
5027
- dns.lookup(hostname, { all: true }).then((records) => {
5655
+ function guardedLookup(hostname2, options, callback) {
5656
+ dns.lookup(hostname2, { all: true }).then((records) => {
5028
5657
  const family = options?.family;
5029
5658
  const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
5030
5659
  const list = byFamily.length > 0 ? byFamily : records;
@@ -5051,7 +5680,7 @@ function guardedLookup(hostname, options, callback) {
5051
5680
  const first = list.at(0);
5052
5681
  if (!first) {
5053
5682
  callback(
5054
- Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
5683
+ Object.assign(new Error(`fetch: no address for ${hostname2}`), { code: "ENOTFOUND" })
5055
5684
  );
5056
5685
  return;
5057
5686
  }
@@ -5165,7 +5794,13 @@ var fetchTool = {
5165
5794
  const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
5166
5795
  const combined = combineSignals([opts.signal, ctrl.signal]);
5167
5796
  try {
5168
- const res = await guardedFetch(input.url, 5, combined);
5797
+ let res;
5798
+ try {
5799
+ res = await guardedFetch(input.url, 5, combined);
5800
+ } catch (err) {
5801
+ if (opts.signal.aborted) throw err;
5802
+ throw describeFetchError(err, input.url, ctrl.signal.aborted);
5803
+ }
5169
5804
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
5170
5805
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
5171
5806
  throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
@@ -5221,9 +5856,9 @@ var fetchTool = {
5221
5856
  }
5222
5857
  }
5223
5858
  };
5224
- async function assertNotPrivate(hostname) {
5859
+ async function assertNotPrivate(hostname2) {
5225
5860
  if (ALLOW_PRIVATE) return;
5226
- const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
5861
+ const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
5227
5862
  if (host === "localhost" || host.endsWith(".localhost")) {
5228
5863
  throw new Error("fetch: blocked localhost target");
5229
5864
  }
@@ -5250,6 +5885,23 @@ async function assertNotPrivate(hostname) {
5250
5885
  }
5251
5886
  }
5252
5887
  }
5888
+ function describeFetchError(err, url, timedOut) {
5889
+ if (timedOut) {
5890
+ return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
5891
+ }
5892
+ const parts = [];
5893
+ const seen = /* @__PURE__ */ new Set();
5894
+ let cur = err;
5895
+ while (cur instanceof Error && !seen.has(cur)) {
5896
+ seen.add(cur);
5897
+ const code = cur.code;
5898
+ const label = code ? `${code}: ${cur.message}` : cur.message;
5899
+ if (label && label !== "fetch failed" && !parts.includes(label)) parts.push(label);
5900
+ cur = cur.cause;
5901
+ }
5902
+ const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
5903
+ return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
5904
+ }
5253
5905
  function prettyJson(s) {
5254
5906
  try {
5255
5907
  return JSON.stringify(JSON.parse(s), null, 2);
@@ -5681,7 +6333,7 @@ var globTool = {
5681
6333
  }
5682
6334
  let entries;
5683
6335
  try {
5684
- entries = await fs14.readdir(dir, { withFileTypes: true });
6336
+ entries = await fs2.readdir(dir, { withFileTypes: true });
5685
6337
  } catch {
5686
6338
  return;
5687
6339
  }
@@ -5697,7 +6349,7 @@ var globTool = {
5697
6349
  } else if (e.isFile()) {
5698
6350
  if (re.test(rel) || re.test(name)) {
5699
6351
  try {
5700
- const st = await fs14.stat(full);
6352
+ const st = await fs2.stat(full);
5701
6353
  results.push({ rel: full, mtime: st.mtimeMs });
5702
6354
  if (results.length >= limit) {
5703
6355
  truncated = true;
@@ -5716,7 +6368,7 @@ var globTool = {
5716
6368
  };
5717
6369
  async function readGitignore(dir) {
5718
6370
  try {
5719
- const raw = await fs14.readFile(path3.join(dir, ".gitignore"), "utf8");
6371
+ const raw = await fs2.readFile(path3.join(dir, ".gitignore"), "utf8");
5720
6372
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
5721
6373
  } catch {
5722
6374
  return [];
@@ -5770,6 +6422,7 @@ function capSubject(line) {
5770
6422
 
5771
6423
  // src/grep.ts
5772
6424
  var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
6425
+ var NATIVE_SCAN_CONCURRENCY = 32;
5773
6426
  var grepTool = {
5774
6427
  name: "grep",
5775
6428
  category: "Search",
@@ -5997,14 +6650,52 @@ async function runNative(input, base, mode, limit, signal) {
5997
6650
  const fileMatches = /* @__PURE__ */ new Map();
5998
6651
  let total = 0;
5999
6652
  let stopped = false;
6653
+ const scanFile = async (full, name) => {
6654
+ if (stopped || signal.aborted) return;
6655
+ if (globRe && !globRe.test(name) && !globRe.test(full)) return;
6656
+ if (globRe) globRe.lastIndex = 0;
6657
+ try {
6658
+ const stat11 = await fs2.stat(full);
6659
+ if (stat11.size > 1e6 || stopped || signal.aborted) return;
6660
+ const head = await fs2.readFile(full);
6661
+ if (isBinaryBuffer(head) || stopped || signal.aborted) return;
6662
+ const text = head.toString("utf8");
6663
+ const lines = text.split(/\r?\n/);
6664
+ let fileHits = 0;
6665
+ for (let i = 0; i < lines.length; i++) {
6666
+ if (stopped || signal.aborted) break;
6667
+ const ln = capSubject(lines[i] ?? "");
6668
+ re.lastIndex = 0;
6669
+ if (re.test(ln)) {
6670
+ fileHits++;
6671
+ total++;
6672
+ if (mode === "content" && matches.length < limit) {
6673
+ matches.push(`${full}:${i + 1}:${ln}`);
6674
+ }
6675
+ }
6676
+ }
6677
+ if (fileHits > 0) {
6678
+ fileMatches.set(full, fileHits);
6679
+ if (mode === "files_with_matches" && matches.length < limit) {
6680
+ matches.push(full);
6681
+ }
6682
+ if (mode === "count" && matches.length < limit) {
6683
+ matches.push(`${full}:${fileHits}`);
6684
+ }
6685
+ }
6686
+ if (matches.length >= limit) stopped = true;
6687
+ } catch {
6688
+ }
6689
+ };
6000
6690
  const walk = async (dir) => {
6001
6691
  if (stopped || signal.aborted) return;
6002
6692
  let entries;
6003
6693
  try {
6004
- entries = await fs14.readdir(dir, { withFileTypes: true });
6694
+ entries = await fs2.readdir(dir, { withFileTypes: true });
6005
6695
  } catch {
6006
6696
  return;
6007
6697
  }
6698
+ const files = [];
6008
6699
  for (const e of entries) {
6009
6700
  if (stopped) return;
6010
6701
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
@@ -6013,41 +6704,10 @@ async function runNative(input, base, mode, limit, signal) {
6013
6704
  if (e.isDirectory()) {
6014
6705
  await walk(full);
6015
6706
  } else if (e.isFile()) {
6016
- if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
6017
- if (globRe) globRe.lastIndex = 0;
6018
- try {
6019
- const stat11 = await fs14.stat(full);
6020
- if (stat11.size > 1e6) continue;
6021
- const head = await fs14.readFile(full);
6022
- if (isBinaryBuffer(head)) continue;
6023
- const text = head.toString("utf8");
6024
- const lines = text.split(/\r?\n/);
6025
- let fileHits = 0;
6026
- for (let i = 0; i < lines.length; i++) {
6027
- const ln = capSubject(lines[i] ?? "");
6028
- re.lastIndex = 0;
6029
- if (re.test(ln)) {
6030
- fileHits++;
6031
- total++;
6032
- if (mode === "content" && matches.length < limit) {
6033
- matches.push(`${full}:${i + 1}:${ln}`);
6034
- }
6035
- }
6036
- }
6037
- if (fileHits > 0) {
6038
- fileMatches.set(full, fileHits);
6039
- if (mode === "files_with_matches" && matches.length < limit) {
6040
- matches.push(full);
6041
- }
6042
- if (mode === "count" && matches.length < limit) {
6043
- matches.push(`${full}:${fileHits}`);
6044
- }
6045
- }
6046
- if (matches.length >= limit) stopped = true;
6047
- } catch {
6048
- }
6707
+ files.push({ full, name: e.name });
6049
6708
  }
6050
6709
  }
6710
+ await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
6051
6711
  };
6052
6712
  await walk(base);
6053
6713
  return {
@@ -6057,6 +6717,20 @@ async function runNative(input, base, mode, limit, signal) {
6057
6717
  used: "native"
6058
6718
  };
6059
6719
  }
6720
+ async function mapWithConcurrency(items, concurrency, fn) {
6721
+ if (items.length === 0) return;
6722
+ let next = 0;
6723
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
6724
+ const workers = Array.from({ length: workerCount }, async () => {
6725
+ for (; ; ) {
6726
+ const idx = next++;
6727
+ if (idx >= items.length) return;
6728
+ const item = items[idx];
6729
+ if (item !== void 0) await fn(item);
6730
+ }
6731
+ });
6732
+ await Promise.all(workers);
6733
+ }
6060
6734
  var installTool = {
6061
6735
  name: "install",
6062
6736
  category: "Package Management",
@@ -6231,7 +6905,7 @@ var jsonTool = {
6231
6905
  let raw;
6232
6906
  if (input.file) {
6233
6907
  try {
6234
- raw = await fs14.readFile(input.file, "utf8");
6908
+ raw = await fs2.readFile(input.file, "utf8");
6235
6909
  } catch {
6236
6910
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
6237
6911
  }
@@ -6270,8 +6944,8 @@ var jsonTool = {
6270
6944
  };
6271
6945
  }
6272
6946
  };
6273
- function query(data, path20) {
6274
- const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
6947
+ function query(data, path21) {
6948
+ const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
6275
6949
  let current = data;
6276
6950
  for (const part of parts) {
6277
6951
  if (current === null || current === void 0) return void 0;
@@ -6548,7 +7222,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6548
7222
  }
6549
7223
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
6550
7224
  var MAX_TAIL_LINES = 1e5;
6551
- async function fileLogs(path20, lines, filterRe, stream) {
7225
+ async function fileLogs(path21, lines, filterRe, stream) {
6552
7226
  const { createInterface } = await import('node:readline');
6553
7227
  const { createReadStream } = await import('node:fs');
6554
7228
  const entries = [];
@@ -6557,7 +7231,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6557
7231
  let writeIdx = 0;
6558
7232
  let totalLines = 0;
6559
7233
  const rl = createInterface({
6560
- input: createReadStream(path20),
7234
+ input: createReadStream(path21),
6561
7235
  crlfDelay: Number.POSITIVE_INFINITY
6562
7236
  });
6563
7237
  for await (const line of rl) {
@@ -6578,7 +7252,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6578
7252
  if (parsed) entries.push(parsed);
6579
7253
  }
6580
7254
  return {
6581
- source: path20,
7255
+ source: path21,
6582
7256
  entries,
6583
7257
  total: entries.length,
6584
7258
  truncated: totalLines > effLines,
@@ -6779,12 +7453,12 @@ var patchTool = {
6779
7453
  };
6780
7454
  }
6781
7455
  }
6782
- const tmpDir = await fs14.mkdtemp(path3.join(os.tmpdir(), ".wstack_patch_"));
7456
+ const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), ".wstack_patch_"));
6783
7457
  try {
6784
- await fs14.chmod(tmpDir, 448).catch(() => {
7458
+ await fs2.chmod(tmpDir, 448).catch(() => {
6785
7459
  });
6786
7460
  const patchFile = path3.join(tmpDir, "in.diff");
6787
- await fs14.writeFile(patchFile, input.patch, { mode: 384 });
7461
+ await fs2.writeFile(patchFile, input.patch, { mode: 384 });
6788
7462
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
6789
7463
  const result = await runPatch(args, dir, opts.signal);
6790
7464
  if (result.exitCode !== 0 && !dryRun) {
@@ -6805,7 +7479,7 @@ var patchTool = {
6805
7479
  message: result.stdout || "patch applied"
6806
7480
  };
6807
7481
  } finally {
6808
- await fs14.rm(tmpDir, { recursive: true, force: true }).catch(() => {
7482
+ await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
6809
7483
  });
6810
7484
  }
6811
7485
  }
@@ -7151,7 +7825,7 @@ var readTool = {
7151
7825
  const absPath = await safeResolveReal(input.path, ctx);
7152
7826
  let stat11;
7153
7827
  try {
7154
- stat11 = await fs14.stat(absPath);
7828
+ stat11 = await fs2.stat(absPath);
7155
7829
  } catch (err) {
7156
7830
  const code = err.code;
7157
7831
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -7176,7 +7850,7 @@ var readTool = {
7176
7850
  note: "Repeated read suppressed to save tokens."
7177
7851
  };
7178
7852
  }
7179
- const buf = await fs14.readFile(absPath);
7853
+ const buf = await fs2.readFile(absPath);
7180
7854
  if (isBinaryBuffer(buf)) {
7181
7855
  throw new Error(`read: "${input.path}" appears to be binary`);
7182
7856
  }
@@ -7322,11 +7996,11 @@ var replaceTool = {
7322
7996
  const dryRun = input.dry_run ?? false;
7323
7997
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
7324
7998
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
7325
- const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
7999
+ const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
7326
8000
  const results = [];
7327
8001
  let totalReplacements = 0;
7328
8002
  for (const absPath of fileList) {
7329
- const lstat2 = await fs14.lstat(absPath).catch((err) => {
8003
+ const lstat2 = await fs2.lstat(absPath).catch((err) => {
7330
8004
  if (err.code === "ENOENT") return null;
7331
8005
  throw err;
7332
8006
  });
@@ -7334,17 +8008,17 @@ var replaceTool = {
7334
8008
  if (lstat2.isSymbolicLink()) continue;
7335
8009
  let realPath;
7336
8010
  try {
7337
- realPath = await fs14.realpath(absPath);
8011
+ realPath = await fs2.realpath(absPath);
7338
8012
  } catch {
7339
8013
  continue;
7340
8014
  }
7341
8015
  const rel = path3.relative(realRoot, realPath);
7342
8016
  if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
7343
- const stat11 = await fs14.stat(realPath).catch(() => null);
8017
+ const stat11 = await fs2.stat(realPath).catch(() => null);
7344
8018
  if (!stat11 || !stat11.isFile()) continue;
7345
8019
  let content;
7346
8020
  try {
7347
- const buf = await fs14.readFile(realPath);
8021
+ const buf = await fs2.readFile(realPath);
7348
8022
  if (isBinaryBuffer(buf)) continue;
7349
8023
  content = buf.toString("utf8");
7350
8024
  } catch {
@@ -7396,7 +8070,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
7396
8070
  const resolved = [];
7397
8071
  for (const p of parts) {
7398
8072
  const absPath = safeResolve(p, ctx);
7399
- const stat11 = await fs14.stat(absPath).catch(() => null);
8073
+ const stat11 = await fs2.stat(absPath).catch(() => null);
7400
8074
  if (stat11?.isFile()) {
7401
8075
  resolved.push(absPath);
7402
8076
  }
@@ -7452,7 +8126,7 @@ async function globNative(pattern, base, extraGlob) {
7452
8126
  const walk = async (dir) => {
7453
8127
  let entries;
7454
8128
  try {
7455
- entries = await fs14.readdir(dir, { withFileTypes: true });
8129
+ entries = await fs2.readdir(dir, { withFileTypes: true });
7456
8130
  } catch {
7457
8131
  return;
7458
8132
  }
@@ -7460,7 +8134,7 @@ async function globNative(pattern, base, extraGlob) {
7460
8134
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
7461
8135
  const full = path3.join(dir, e.name);
7462
8136
  try {
7463
- const stat11 = await fs14.lstat(full);
8137
+ const stat11 = await fs2.lstat(full);
7464
8138
  if (stat11.isSymbolicLink()) continue;
7465
8139
  } catch {
7466
8140
  continue;
@@ -7638,7 +8312,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
7638
8312
  }
7639
8313
  const fullPath = target;
7640
8314
  if (!dryRun) {
7641
- await fs14.mkdir(path3.dirname(fullPath), { recursive: true });
8315
+ await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
7642
8316
  await atomicWrite(fullPath, substituteVars(content, name, vars));
7643
8317
  }
7644
8318
  files.push(resolvedPath);
@@ -7751,7 +8425,7 @@ async function duckduckgoSearch(query2, num, signal) {
7751
8425
  truncated: results.length >= num
7752
8426
  };
7753
8427
  } catch (err) {
7754
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
8428
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
7755
8429
  return {
7756
8430
  query: query2,
7757
8431
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -7915,11 +8589,11 @@ var setWorkingDirTool = {
7915
8589
  } catch (err) {
7916
8590
  return {
7917
8591
  current: ctx.workingDir,
7918
- error: toErrorMessage(err)
8592
+ error: toErrorMessage$1(err)
7919
8593
  };
7920
8594
  }
7921
8595
  try {
7922
- await fs14.access(resolved);
8596
+ await fs2.access(resolved);
7923
8597
  } catch {
7924
8598
  try {
7925
8599
  ctx.setWorkingDir(previous);
@@ -8975,7 +9649,7 @@ var treeTool = {
8975
9649
  }
8976
9650
  };
8977
9651
  async function walkDir(dir, depth, opts) {
8978
- const entries = await fs14.readdir(dir, { withFileTypes: true }).catch(() => []);
9652
+ const entries = await fs2.readdir(dir, { withFileTypes: true }).catch(() => []);
8979
9653
  const filtered = entries.filter((e) => {
8980
9654
  if (!opts.showHidden && e.name.startsWith(".")) return false;
8981
9655
  if (opts.exclude.has(e.name)) return false;
@@ -9133,14 +9807,14 @@ var writeTool = {
9133
9807
  let existed = false;
9134
9808
  let prev = "";
9135
9809
  try {
9136
- const stat12 = await fs14.stat(absPath);
9810
+ const stat12 = await fs2.stat(absPath);
9137
9811
  existed = stat12.isFile();
9138
9812
  if (existed) {
9139
9813
  if (!ctx.hasRead(absPath)) {
9140
- prev = await fs14.readFile(absPath, "utf8");
9814
+ prev = await fs2.readFile(absPath, "utf8");
9141
9815
  ctx.recordRead(absPath, stat12.mtimeMs);
9142
9816
  } else {
9143
- prev = await fs14.readFile(absPath, "utf8");
9817
+ prev = await fs2.readFile(absPath, "utf8");
9144
9818
  }
9145
9819
  }
9146
9820
  } catch (err) {
@@ -9151,7 +9825,7 @@ var writeTool = {
9151
9825
  await atomicWrite(absPath, input.content);
9152
9826
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
9153
9827
  + (new file, ${input.content.split("\n").length} lines)`;
9154
- const stat11 = await fs14.stat(absPath);
9828
+ const stat11 = await fs2.stat(absPath);
9155
9829
  ctx.recordRead(absPath, stat11.mtimeMs);
9156
9830
  ctx.session.recordFileChange({
9157
9831
  path: absPath,