@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/pack.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;
@@ -1179,8 +1752,7 @@ var bashTool = {
1179
1752
  // Windows children survive parent exit either way. POSIX keeps
1180
1753
  // detached for the process-group kill semantics.
1181
1754
  detached: !isWin3,
1182
- windowsHide: true,
1183
- signal: opts.signal
1755
+ windowsHide: true
1184
1756
  });
1185
1757
  const pid2 = child2.pid;
1186
1758
  if (typeof pid2 === "number") {
@@ -1208,7 +1780,17 @@ var bashTool = {
1208
1780
  };
1209
1781
  child2.stdout?.on("data", onBgData);
1210
1782
  child2.stderr?.on("data", onBgData);
1783
+ const cleanupBackground = () => {
1784
+ child2.stdout?.off("data", onBgData);
1785
+ child2.stderr?.off("data", onBgData);
1786
+ };
1787
+ child2.on("error", () => {
1788
+ cleanupBackground();
1789
+ if (typeof pid2 === "number") registry.unregister(pid2);
1790
+ registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
1791
+ });
1211
1792
  child2.on("close", () => {
1793
+ cleanupBackground();
1212
1794
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
1213
1795
  });
1214
1796
  if (typeof pid2 === "number") child2.unref();
@@ -1753,7 +2335,7 @@ function loadDatabaseSync() {
1753
2335
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
1754
2336
  } catch (err) {
1755
2337
  throw new Error(
1756
- `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)}`
1757
2339
  );
1758
2340
  }
1759
2341
  return DatabaseSyncCtor;
@@ -1902,33 +2484,53 @@ var IndexStore = class {
1902
2484
  }
1903
2485
  }
1904
2486
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
1905
- 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) {
1906
2498
  return this.runWithRetry(() => {
1907
- const stmt = this.db.prepare(
1908
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
1909
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1910
- );
1911
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
1912
- let id = nextId;
1913
- for (const s of symbols) {
1914
- stmt.run(
1915
- id,
1916
- s.lang,
1917
- s.kind,
1918
- s.name,
1919
- s.file,
1920
- s.line,
1921
- s.col,
1922
- s.signature,
1923
- s.docComment,
1924
- s.scope,
1925
- s.text,
1926
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1927
2506
  );
1928
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
1929
- 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;
1930
2533
  }
1931
- return id;
1932
2534
  });
1933
2535
  }
1934
2536
  deleteSymbolsForFile(file) {
@@ -2482,10 +3084,10 @@ function detectLang(file) {
2482
3084
  if (idx < 0) return null;
2483
3085
  return extToLang(file.slice(idx));
2484
3086
  }
2485
- function parseSymbols2(opts) {
3087
+ async function parseSymbols2(opts) {
2486
3088
  const { file, content, lang } = opts;
2487
3089
  try {
2488
- return syncGoParse(file, content, lang);
3090
+ return await syncGoParse(file, content, lang);
2489
3091
  } catch {
2490
3092
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2491
3093
  }
@@ -2722,19 +3324,34 @@ func formatType(t ast.Expr) string {
2722
3324
  }
2723
3325
  }
2724
3326
  `;
2725
- function syncGoParse(filePath, content, lang) {
2726
- 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");
2727
3329
  try {
2728
- mkdirSync(tmpDir, { recursive: true });
3330
+ await fs2.mkdir(tmpDir, { recursive: true });
2729
3331
  const scriptPath = path3.join(tmpDir, "parse.go");
2730
- writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
2731
- const stdout = execFileSync("go", ["run", scriptPath], {
2732
- input: content,
2733
- timeout: 15e3,
2734
- encoding: "utf8",
3332
+ await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
3333
+ const proc = spawn("go", ["run", scriptPath], {
3334
+ stdio: ["pipe", "pipe", "pipe"],
2735
3335
  windowsHide: true
2736
3336
  });
2737
- 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()) {
2738
3355
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2739
3356
  }
2740
3357
  const raw = JSON.parse(stdout.trim());
@@ -2756,10 +3373,10 @@ function syncGoParse(filePath, content, lang) {
2756
3373
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2757
3374
  }
2758
3375
  }
2759
- function parseSymbols3(opts) {
3376
+ async function parseSymbols3(opts) {
2760
3377
  const { file, lang } = opts;
2761
3378
  try {
2762
- return syncPyParse(file, lang);
3379
+ return await syncPyParse(file, lang);
2763
3380
  } catch {
2764
3381
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2765
3382
  }
@@ -2968,18 +3585,32 @@ visitor.visit(tree)
2968
3585
 
2969
3586
  print(json.dumps([s.to_dict() for s in syms]))
2970
3587
  `;
2971
- function syncPyParse(filePath, lang) {
3588
+ async function syncPyParse(filePath, lang) {
2972
3589
  try {
2973
- const tmpDir = path3.join(os.tmpdir(), "ws-py-parse");
2974
- mkdirSync(tmpDir, { recursive: true });
3590
+ const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
3591
+ await fs2.mkdir(tmpDir, { recursive: true });
2975
3592
  const scriptPath = path3.join(tmpDir, "parse.py");
2976
- writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
2977
- const stdout = execFileSync("python", [scriptPath, filePath], {
2978
- timeout: 15e3,
2979
- encoding: "utf8",
3593
+ await fs2.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
3594
+ const proc = spawn("python", [scriptPath, filePath], {
3595
+ stdio: ["pipe", "pipe", "pipe"],
2980
3596
  windowsHide: true
2981
3597
  });
2982
- 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()) {
2983
3614
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2984
3615
  }
2985
3616
  const raw = JSON.parse(stdout.trim());
@@ -3001,11 +3632,11 @@ function syncPyParse(filePath, lang) {
3001
3632
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
3002
3633
  }
3003
3634
  }
3004
- function parseSymbols4(opts) {
3635
+ async function parseSymbols4(opts) {
3005
3636
  const { file, content, lang } = opts;
3006
3637
  const nativeAvailable = checkNativeParser();
3007
3638
  if (nativeAvailable) {
3008
- const result = tryNativeParse(file, content);
3639
+ const result = await tryNativeParse(file, content);
3009
3640
  if (result) return result;
3010
3641
  }
3011
3642
  return regexParse({ file, content, lang });
@@ -3035,25 +3666,34 @@ function checkNativeParser() {
3035
3666
  return false;
3036
3667
  }
3037
3668
  }
3038
- function tryNativeParse(file, content) {
3669
+ async function tryNativeParse(file, content) {
3039
3670
  try {
3040
3671
  const toolsDir = path3.join(process.cwd(), "tools");
3041
3672
  const crateDir = path3.join(toolsDir, "syn-parser");
3042
3673
  const tmpFile = path3.join(crateDir, "src", "input.rs");
3043
- writeFileSync(tmpFile, content, "utf8");
3044
- const result = spawnSync(
3045
- "cargo",
3046
- ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")],
3047
- {
3048
- cwd: process.cwd(),
3049
- encoding: "utf8",
3050
- timeout: 15e3,
3051
- stdio: ["pipe", "pipe", "pipe"],
3052
- windowsHide: true
3053
- }
3054
- );
3055
- if (result.status === 0 && result.stdout) {
3056
- 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());
3057
3697
  return {
3058
3698
  file,
3059
3699
  lang: "rs",
@@ -3522,7 +4162,7 @@ function compileGitignore(lines) {
3522
4162
  async function loadGitignoreMatcher(projectRoot) {
3523
4163
  let lines = [];
3524
4164
  try {
3525
- const raw = await fs14.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
4165
+ const raw = await fs2.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
3526
4166
  lines = raw.split("\n");
3527
4167
  } catch {
3528
4168
  }
@@ -3580,7 +4220,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
3580
4220
  }
3581
4221
  let entries;
3582
4222
  try {
3583
- entries = await fs14.readdir(dir, { withFileTypes: true });
4223
+ entries = await fs2.readdir(dir, { withFileTypes: true });
3584
4224
  } catch {
3585
4225
  return;
3586
4226
  }
@@ -3678,7 +4318,7 @@ async function runIndexerWithStore(store, opts) {
3678
4318
  batchFiles.map(async (file) => {
3679
4319
  let stat11;
3680
4320
  try {
3681
- stat11 = await fs14.stat(file, statOpts);
4321
+ stat11 = await fs2.stat(file, statOpts);
3682
4322
  } catch (e) {
3683
4323
  if (isAbortError(e)) throw e;
3684
4324
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
@@ -3692,7 +4332,7 @@ async function runIndexerWithStore(store, opts) {
3692
4332
  }
3693
4333
  let content;
3694
4334
  try {
3695
- content = await fs14.readFile(file, { encoding: "utf8", signal });
4335
+ content = await fs2.readFile(file, { encoding: "utf8", signal });
3696
4336
  } catch (e) {
3697
4337
  if (isAbortError(e)) throw e;
3698
4338
  return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
@@ -3742,9 +4382,7 @@ async function runIndexerWithStore(store, opts) {
3742
4382
  filesIndexed++;
3743
4383
  continue;
3744
4384
  }
3745
- const nextId = store.getMaxSymbolId() + 1;
3746
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
3747
- store.insertSymbols(symbolsWithIds, nextId);
4385
+ const symbolsWithIds = store.insertSymbols(parsed.symbols);
3748
4386
  const count = symbolsWithIds.length;
3749
4387
  symbolsIndexed += count;
3750
4388
  langStats[lang] = (langStats[lang] ?? 0) + count;
@@ -3773,7 +4411,7 @@ async function runIndexerWithStore(store, opts) {
3773
4411
  }
3774
4412
  for (const [file_] of existingMeta) {
3775
4413
  try {
3776
- await fs14.stat(file_);
4414
+ await fs2.stat(file_);
3777
4415
  } catch {
3778
4416
  store.deleteFile(file_);
3779
4417
  }
@@ -4023,10 +4661,10 @@ function circuitOpenError() {
4023
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."
4024
4662
  );
4025
4663
  }
4026
- function isUniqueConstraintError(err) {
4664
+ function isRecoverableConstraintError(err) {
4027
4665
  if (err instanceof Error) {
4028
4666
  const msg = err.message.toLowerCase();
4029
- return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
4667
+ return msg.includes("unique constraint") || msg.includes("constraint failed");
4030
4668
  }
4031
4669
  return false;
4032
4670
  }
@@ -4059,7 +4697,7 @@ async function runStartupIndex(opts) {
4059
4697
  return result;
4060
4698
  } catch (err) {
4061
4699
  _lastError = err instanceof Error ? err.message : String(err);
4062
- if (isUniqueConstraintError(err) && !opts.force) {
4700
+ if (isRecoverableConstraintError(err) && !opts.force) {
4063
4701
  _lastError = null;
4064
4702
  const rebuildResult = await runStartupIndex({
4065
4703
  ...opts,
@@ -4419,9 +5057,9 @@ async function fileDiff(input, ctx, _signal) {
4419
5057
  const results = [];
4420
5058
  for (const file of files) {
4421
5059
  const absPath = safeResolve(file, ctx);
4422
- const stat11 = await fs14.stat(absPath).catch(() => null);
5060
+ const stat11 = await fs2.stat(absPath).catch(() => null);
4423
5061
  if (!stat11?.isFile()) continue;
4424
- const content = await fs14.readFile(absPath, "utf8");
5062
+ const content = await fs2.readFile(absPath, "utf8");
4425
5063
  const lines = content.split(/\r?\n/);
4426
5064
  results.push(formatWithLineNumbers(file, lines));
4427
5065
  }
@@ -4481,7 +5119,7 @@ var documentTool = {
4481
5119
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
4482
5120
  for (const absPath of fileList) {
4483
5121
  try {
4484
- const content = await fs14.readFile(absPath, "utf8");
5122
+ const content = await fs2.readFile(absPath, "utf8");
4485
5123
  filesProcessed++;
4486
5124
  const processed = processFile(
4487
5125
  content,
@@ -4517,7 +5155,7 @@ async function resolveFiles(filesInput, cwd) {
4517
5155
  for (const f of files) {
4518
5156
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
4519
5157
  try {
4520
- const stat11 = await fs14.stat(absPath);
5158
+ const stat11 = await fs2.stat(absPath);
4521
5159
  if (stat11.isFile()) resolved.push(absPath);
4522
5160
  } catch {
4523
5161
  }
@@ -4610,7 +5248,7 @@ var editTool = {
4610
5248
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
4611
5249
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
4612
5250
  const absPath = await safeResolveReal(input.path, ctx);
4613
- const stat11 = await fs14.stat(absPath).catch((err) => {
5251
+ const stat11 = await fs2.stat(absPath).catch((err) => {
4614
5252
  if (err.code === "ENOENT") {
4615
5253
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
4616
5254
  }
@@ -4618,8 +5256,8 @@ var editTool = {
4618
5256
  });
4619
5257
  if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
4620
5258
  const autoRead = !ctx.hasRead(absPath);
4621
- const original = await fs14.readFile(absPath, "utf8");
4622
- const updated = await fs14.stat(absPath);
5259
+ const original = await fs2.readFile(absPath, "utf8");
5260
+ const updated = await fs2.stat(absPath);
4623
5261
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
4624
5262
  const lastReadMtime = ctx.lastReadMtime(absPath);
4625
5263
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -4665,7 +5303,7 @@ var editTool = {
4665
5303
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
4666
5304
  const newFile = toStyle(newFileLf, style);
4667
5305
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
4668
- const written = await fs14.stat(absPath);
5306
+ const written = await fs2.stat(absPath);
4669
5307
  ctx.recordRead(absPath, written.mtimeMs);
4670
5308
  ctx.session.recordFileChange({
4671
5309
  path: absPath,
@@ -5014,8 +5652,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
5014
5652
  );
5015
5653
  }
5016
5654
  var combineSignals = (signals) => AbortSignal.any(signals);
5017
- function guardedLookup(hostname, options, callback) {
5018
- dns.lookup(hostname, { all: true }).then((records) => {
5655
+ function guardedLookup(hostname2, options, callback) {
5656
+ dns.lookup(hostname2, { all: true }).then((records) => {
5019
5657
  const family = options?.family;
5020
5658
  const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
5021
5659
  const list = byFamily.length > 0 ? byFamily : records;
@@ -5042,7 +5680,7 @@ function guardedLookup(hostname, options, callback) {
5042
5680
  const first = list.at(0);
5043
5681
  if (!first) {
5044
5682
  callback(
5045
- Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
5683
+ Object.assign(new Error(`fetch: no address for ${hostname2}`), { code: "ENOTFOUND" })
5046
5684
  );
5047
5685
  return;
5048
5686
  }
@@ -5156,7 +5794,13 @@ var fetchTool = {
5156
5794
  const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
5157
5795
  const combined = combineSignals([opts.signal, ctrl.signal]);
5158
5796
  try {
5159
- 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
+ }
5160
5804
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
5161
5805
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
5162
5806
  throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
@@ -5212,9 +5856,9 @@ var fetchTool = {
5212
5856
  }
5213
5857
  }
5214
5858
  };
5215
- async function assertNotPrivate(hostname) {
5859
+ async function assertNotPrivate(hostname2) {
5216
5860
  if (ALLOW_PRIVATE) return;
5217
- const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
5861
+ const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
5218
5862
  if (host === "localhost" || host.endsWith(".localhost")) {
5219
5863
  throw new Error("fetch: blocked localhost target");
5220
5864
  }
@@ -5241,6 +5885,23 @@ async function assertNotPrivate(hostname) {
5241
5885
  }
5242
5886
  }
5243
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
+ }
5244
5905
  function prettyJson(s) {
5245
5906
  try {
5246
5907
  return JSON.stringify(JSON.parse(s), null, 2);
@@ -5672,7 +6333,7 @@ var globTool = {
5672
6333
  }
5673
6334
  let entries;
5674
6335
  try {
5675
- entries = await fs14.readdir(dir, { withFileTypes: true });
6336
+ entries = await fs2.readdir(dir, { withFileTypes: true });
5676
6337
  } catch {
5677
6338
  return;
5678
6339
  }
@@ -5688,7 +6349,7 @@ var globTool = {
5688
6349
  } else if (e.isFile()) {
5689
6350
  if (re.test(rel) || re.test(name)) {
5690
6351
  try {
5691
- const st = await fs14.stat(full);
6352
+ const st = await fs2.stat(full);
5692
6353
  results.push({ rel: full, mtime: st.mtimeMs });
5693
6354
  if (results.length >= limit) {
5694
6355
  truncated = true;
@@ -5707,7 +6368,7 @@ var globTool = {
5707
6368
  };
5708
6369
  async function readGitignore(dir) {
5709
6370
  try {
5710
- const raw = await fs14.readFile(path3.join(dir, ".gitignore"), "utf8");
6371
+ const raw = await fs2.readFile(path3.join(dir, ".gitignore"), "utf8");
5711
6372
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
5712
6373
  } catch {
5713
6374
  return [];
@@ -5761,6 +6422,7 @@ function capSubject(line) {
5761
6422
 
5762
6423
  // src/grep.ts
5763
6424
  var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
6425
+ var NATIVE_SCAN_CONCURRENCY = 32;
5764
6426
  var grepTool = {
5765
6427
  name: "grep",
5766
6428
  category: "Search",
@@ -5988,14 +6650,52 @@ async function runNative(input, base, mode, limit, signal) {
5988
6650
  const fileMatches = /* @__PURE__ */ new Map();
5989
6651
  let total = 0;
5990
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
+ };
5991
6690
  const walk = async (dir) => {
5992
6691
  if (stopped || signal.aborted) return;
5993
6692
  let entries;
5994
6693
  try {
5995
- entries = await fs14.readdir(dir, { withFileTypes: true });
6694
+ entries = await fs2.readdir(dir, { withFileTypes: true });
5996
6695
  } catch {
5997
6696
  return;
5998
6697
  }
6698
+ const files = [];
5999
6699
  for (const e of entries) {
6000
6700
  if (stopped) return;
6001
6701
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
@@ -6004,41 +6704,10 @@ async function runNative(input, base, mode, limit, signal) {
6004
6704
  if (e.isDirectory()) {
6005
6705
  await walk(full);
6006
6706
  } else if (e.isFile()) {
6007
- if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
6008
- if (globRe) globRe.lastIndex = 0;
6009
- try {
6010
- const stat11 = await fs14.stat(full);
6011
- if (stat11.size > 1e6) continue;
6012
- const head = await fs14.readFile(full);
6013
- if (isBinaryBuffer(head)) continue;
6014
- const text = head.toString("utf8");
6015
- const lines = text.split(/\r?\n/);
6016
- let fileHits = 0;
6017
- for (let i = 0; i < lines.length; i++) {
6018
- const ln = capSubject(lines[i] ?? "");
6019
- re.lastIndex = 0;
6020
- if (re.test(ln)) {
6021
- fileHits++;
6022
- total++;
6023
- if (mode === "content" && matches.length < limit) {
6024
- matches.push(`${full}:${i + 1}:${ln}`);
6025
- }
6026
- }
6027
- }
6028
- if (fileHits > 0) {
6029
- fileMatches.set(full, fileHits);
6030
- if (mode === "files_with_matches" && matches.length < limit) {
6031
- matches.push(full);
6032
- }
6033
- if (mode === "count" && matches.length < limit) {
6034
- matches.push(`${full}:${fileHits}`);
6035
- }
6036
- }
6037
- if (matches.length >= limit) stopped = true;
6038
- } catch {
6039
- }
6707
+ files.push({ full, name: e.name });
6040
6708
  }
6041
6709
  }
6710
+ await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
6042
6711
  };
6043
6712
  await walk(base);
6044
6713
  return {
@@ -6048,6 +6717,20 @@ async function runNative(input, base, mode, limit, signal) {
6048
6717
  used: "native"
6049
6718
  };
6050
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
+ }
6051
6734
  var installTool = {
6052
6735
  name: "install",
6053
6736
  category: "Package Management",
@@ -6222,7 +6905,7 @@ var jsonTool = {
6222
6905
  let raw;
6223
6906
  if (input.file) {
6224
6907
  try {
6225
- raw = await fs14.readFile(input.file, "utf8");
6908
+ raw = await fs2.readFile(input.file, "utf8");
6226
6909
  } catch {
6227
6910
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
6228
6911
  }
@@ -6261,8 +6944,8 @@ var jsonTool = {
6261
6944
  };
6262
6945
  }
6263
6946
  };
6264
- function query(data, path20) {
6265
- 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);
6266
6949
  let current = data;
6267
6950
  for (const part of parts) {
6268
6951
  if (current === null || current === void 0) return void 0;
@@ -6539,7 +7222,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6539
7222
  }
6540
7223
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
6541
7224
  var MAX_TAIL_LINES = 1e5;
6542
- async function fileLogs(path20, lines, filterRe, stream) {
7225
+ async function fileLogs(path21, lines, filterRe, stream) {
6543
7226
  const { createInterface } = await import('node:readline');
6544
7227
  const { createReadStream } = await import('node:fs');
6545
7228
  const entries = [];
@@ -6548,7 +7231,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6548
7231
  let writeIdx = 0;
6549
7232
  let totalLines = 0;
6550
7233
  const rl = createInterface({
6551
- input: createReadStream(path20),
7234
+ input: createReadStream(path21),
6552
7235
  crlfDelay: Number.POSITIVE_INFINITY
6553
7236
  });
6554
7237
  for await (const line of rl) {
@@ -6569,7 +7252,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6569
7252
  if (parsed) entries.push(parsed);
6570
7253
  }
6571
7254
  return {
6572
- source: path20,
7255
+ source: path21,
6573
7256
  entries,
6574
7257
  total: entries.length,
6575
7258
  truncated: totalLines > effLines,
@@ -6770,12 +7453,12 @@ var patchTool = {
6770
7453
  };
6771
7454
  }
6772
7455
  }
6773
- const tmpDir = await fs14.mkdtemp(path3.join(os.tmpdir(), ".wstack_patch_"));
7456
+ const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), ".wstack_patch_"));
6774
7457
  try {
6775
- await fs14.chmod(tmpDir, 448).catch(() => {
7458
+ await fs2.chmod(tmpDir, 448).catch(() => {
6776
7459
  });
6777
7460
  const patchFile = path3.join(tmpDir, "in.diff");
6778
- await fs14.writeFile(patchFile, input.patch, { mode: 384 });
7461
+ await fs2.writeFile(patchFile, input.patch, { mode: 384 });
6779
7462
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
6780
7463
  const result = await runPatch(args, dir, opts.signal);
6781
7464
  if (result.exitCode !== 0 && !dryRun) {
@@ -6796,7 +7479,7 @@ var patchTool = {
6796
7479
  message: result.stdout || "patch applied"
6797
7480
  };
6798
7481
  } finally {
6799
- await fs14.rm(tmpDir, { recursive: true, force: true }).catch(() => {
7482
+ await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
6800
7483
  });
6801
7484
  }
6802
7485
  }
@@ -7142,7 +7825,7 @@ var readTool = {
7142
7825
  const absPath = await safeResolveReal(input.path, ctx);
7143
7826
  let stat11;
7144
7827
  try {
7145
- stat11 = await fs14.stat(absPath);
7828
+ stat11 = await fs2.stat(absPath);
7146
7829
  } catch (err) {
7147
7830
  const code = err.code;
7148
7831
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -7167,7 +7850,7 @@ var readTool = {
7167
7850
  note: "Repeated read suppressed to save tokens."
7168
7851
  };
7169
7852
  }
7170
- const buf = await fs14.readFile(absPath);
7853
+ const buf = await fs2.readFile(absPath);
7171
7854
  if (isBinaryBuffer(buf)) {
7172
7855
  throw new Error(`read: "${input.path}" appears to be binary`);
7173
7856
  }
@@ -7313,11 +7996,11 @@ var replaceTool = {
7313
7996
  const dryRun = input.dry_run ?? false;
7314
7997
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
7315
7998
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
7316
- const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
7999
+ const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
7317
8000
  const results = [];
7318
8001
  let totalReplacements = 0;
7319
8002
  for (const absPath of fileList) {
7320
- const lstat2 = await fs14.lstat(absPath).catch((err) => {
8003
+ const lstat2 = await fs2.lstat(absPath).catch((err) => {
7321
8004
  if (err.code === "ENOENT") return null;
7322
8005
  throw err;
7323
8006
  });
@@ -7325,17 +8008,17 @@ var replaceTool = {
7325
8008
  if (lstat2.isSymbolicLink()) continue;
7326
8009
  let realPath;
7327
8010
  try {
7328
- realPath = await fs14.realpath(absPath);
8011
+ realPath = await fs2.realpath(absPath);
7329
8012
  } catch {
7330
8013
  continue;
7331
8014
  }
7332
8015
  const rel = path3.relative(realRoot, realPath);
7333
8016
  if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
7334
- const stat11 = await fs14.stat(realPath).catch(() => null);
8017
+ const stat11 = await fs2.stat(realPath).catch(() => null);
7335
8018
  if (!stat11 || !stat11.isFile()) continue;
7336
8019
  let content;
7337
8020
  try {
7338
- const buf = await fs14.readFile(realPath);
8021
+ const buf = await fs2.readFile(realPath);
7339
8022
  if (isBinaryBuffer(buf)) continue;
7340
8023
  content = buf.toString("utf8");
7341
8024
  } catch {
@@ -7387,7 +8070,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
7387
8070
  const resolved = [];
7388
8071
  for (const p of parts) {
7389
8072
  const absPath = safeResolve(p, ctx);
7390
- const stat11 = await fs14.stat(absPath).catch(() => null);
8073
+ const stat11 = await fs2.stat(absPath).catch(() => null);
7391
8074
  if (stat11?.isFile()) {
7392
8075
  resolved.push(absPath);
7393
8076
  }
@@ -7443,7 +8126,7 @@ async function globNative(pattern, base, extraGlob) {
7443
8126
  const walk = async (dir) => {
7444
8127
  let entries;
7445
8128
  try {
7446
- entries = await fs14.readdir(dir, { withFileTypes: true });
8129
+ entries = await fs2.readdir(dir, { withFileTypes: true });
7447
8130
  } catch {
7448
8131
  return;
7449
8132
  }
@@ -7451,7 +8134,7 @@ async function globNative(pattern, base, extraGlob) {
7451
8134
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
7452
8135
  const full = path3.join(dir, e.name);
7453
8136
  try {
7454
- const stat11 = await fs14.lstat(full);
8137
+ const stat11 = await fs2.lstat(full);
7455
8138
  if (stat11.isSymbolicLink()) continue;
7456
8139
  } catch {
7457
8140
  continue;
@@ -7629,7 +8312,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
7629
8312
  }
7630
8313
  const fullPath = target;
7631
8314
  if (!dryRun) {
7632
- await fs14.mkdir(path3.dirname(fullPath), { recursive: true });
8315
+ await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
7633
8316
  await atomicWrite(fullPath, substituteVars(content, name, vars));
7634
8317
  }
7635
8318
  files.push(resolvedPath);
@@ -7742,7 +8425,7 @@ async function duckduckgoSearch(query2, num, signal) {
7742
8425
  truncated: results.length >= num
7743
8426
  };
7744
8427
  } catch (err) {
7745
- 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) }));
7746
8429
  return {
7747
8430
  query: query2,
7748
8431
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -7906,11 +8589,11 @@ var setWorkingDirTool = {
7906
8589
  } catch (err) {
7907
8590
  return {
7908
8591
  current: ctx.workingDir,
7909
- error: toErrorMessage(err)
8592
+ error: toErrorMessage$1(err)
7910
8593
  };
7911
8594
  }
7912
8595
  try {
7913
- await fs14.access(resolved);
8596
+ await fs2.access(resolved);
7914
8597
  } catch {
7915
8598
  try {
7916
8599
  ctx.setWorkingDir(previous);
@@ -8966,7 +9649,7 @@ var treeTool = {
8966
9649
  }
8967
9650
  };
8968
9651
  async function walkDir(dir, depth, opts) {
8969
- const entries = await fs14.readdir(dir, { withFileTypes: true }).catch(() => []);
9652
+ const entries = await fs2.readdir(dir, { withFileTypes: true }).catch(() => []);
8970
9653
  const filtered = entries.filter((e) => {
8971
9654
  if (!opts.showHidden && e.name.startsWith(".")) return false;
8972
9655
  if (opts.exclude.has(e.name)) return false;
@@ -9124,14 +9807,14 @@ var writeTool = {
9124
9807
  let existed = false;
9125
9808
  let prev = "";
9126
9809
  try {
9127
- const stat12 = await fs14.stat(absPath);
9810
+ const stat12 = await fs2.stat(absPath);
9128
9811
  existed = stat12.isFile();
9129
9812
  if (existed) {
9130
9813
  if (!ctx.hasRead(absPath)) {
9131
- prev = await fs14.readFile(absPath, "utf8");
9814
+ prev = await fs2.readFile(absPath, "utf8");
9132
9815
  ctx.recordRead(absPath, stat12.mtimeMs);
9133
9816
  } else {
9134
- prev = await fs14.readFile(absPath, "utf8");
9817
+ prev = await fs2.readFile(absPath, "utf8");
9135
9818
  }
9136
9819
  }
9137
9820
  } catch (err) {
@@ -9142,7 +9825,7 @@ var writeTool = {
9142
9825
  await atomicWrite(absPath, input.content);
9143
9826
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
9144
9827
  + (new file, ${input.content.split("\n").length} lines)`;
9145
- const stat11 = await fs14.stat(absPath);
9828
+ const stat11 = await fs2.stat(absPath);
9146
9829
  ctx.recordRead(absPath, stat11.mtimeMs);
9147
9830
  ctx.session.recordFileChange({
9148
9831
  path: absPath,