@wrongstack/tools 0.270.0 → 0.272.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/builtin.js CHANGED
@@ -1,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)) {
@@ -657,6 +660,16 @@ function resolveWin32Command(cmd) {
657
660
  }
658
661
  return cmd;
659
662
  }
663
+ var WIN32_SHELL_META = /[&|<>\r\n\0]/;
664
+ function assertSafeWin32ShellArgs(args) {
665
+ for (const a of args) {
666
+ if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
667
+ throw new Error(
668
+ "win32 shell spawn: argument contains a shell metacharacter (one of & | < > or a newline) that could enable command injection through the .cmd/.bat wrapper \u2014 refusing to run. Offending argument: " + JSON.stringify(a)
669
+ );
670
+ }
671
+ }
672
+ }
660
673
 
661
674
  // src/_spawn-stream.ts
662
675
  var isWin = process.platform === "win32";
@@ -672,6 +685,7 @@ async function* spawnStream(opts) {
672
685
  const resolved = resolveWin32Command(opts.cmd);
673
686
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
674
687
  const cmd = needsShell ? opts.cmd : resolved;
688
+ if (needsShell) assertSafeWin32ShellArgs(opts.args);
675
689
  const child = spawn(cmd, opts.args, {
676
690
  cwd: opts.cwd,
677
691
  env: buildChildEnv(),
@@ -756,8 +770,10 @@ async function* spawnStream(opts) {
756
770
  queue.push({ kind: "close", data: "", code: 124 });
757
771
  wake();
758
772
  };
759
- if (opts.signal.aborted) onAbort();
760
- else opts.signal.addEventListener("abort", onAbort, { once: true });
773
+ if (isWin) {
774
+ if (opts.signal.aborted) onAbort();
775
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
776
+ }
761
777
  let exitCode = 0;
762
778
  let spawnFailed = false;
763
779
  try {
@@ -801,7 +817,7 @@ async function* spawnStream(opts) {
801
817
  };
802
818
  } finally {
803
819
  spool.finalize();
804
- opts.signal.removeEventListener("abort", onAbort);
820
+ if (isWin) opts.signal.removeEventListener("abort", onAbort);
805
821
  child.stdout?.off("data", onOut);
806
822
  child.stderr?.off("data", onErr);
807
823
  child.stdout?.destroy();
@@ -856,13 +872,13 @@ function safeResolve(input, ctx) {
856
872
  async function assertRealInsideRoot(absPath, ctx) {
857
873
  if (ctx.allowOutsideProjectRoot) return;
858
874
  const realRoots = await Promise.all(
859
- allowedRoots(ctx).map((r) => fs14.realpath(r).catch(() => path3.resolve(r)))
875
+ allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path3.resolve(r)))
860
876
  );
861
877
  let probe = absPath;
862
878
  for (; ; ) {
863
879
  let real;
864
880
  try {
865
- real = await fs14.realpath(probe);
881
+ real = await fs2.realpath(probe);
866
882
  } catch (err) {
867
883
  if (err.code === "ENOENT") {
868
884
  const parent = path3.dirname(probe);
@@ -1067,6 +1083,560 @@ function parseAuditOutput(json, exitCode) {
1067
1083
  };
1068
1084
  }
1069
1085
  }
1086
+ var REGISTRY_FILE = ".wrongstack/process-registry.json";
1087
+ var HEARTBEAT_INTERVAL_MS = 5e3;
1088
+ var STALE_THRESHOLD_MS = 3e4;
1089
+ var LOCKFILE = ".wrongstack/.process-registry.lock";
1090
+ function generateInstanceId() {
1091
+ const hostname2 = os2.hostname();
1092
+ const pid = process.pid;
1093
+ const random = Math.random().toString(36).slice(2, 8);
1094
+ return `${hostname2}:${pid}:${random}`;
1095
+ }
1096
+ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
1097
+ const start = Date.now();
1098
+ const pidStr = String(process.pid);
1099
+ const hostStr = os2.hostname();
1100
+ while (Date.now() - start < timeoutMs) {
1101
+ try {
1102
+ await fs2.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
1103
+ return async () => {
1104
+ try {
1105
+ await fs2.unlink(lockfilePath);
1106
+ } catch {
1107
+ }
1108
+ };
1109
+ } catch (err) {
1110
+ if (err.code === "EEXIST") {
1111
+ try {
1112
+ const content = await fs2.readFile(lockfilePath, "utf-8");
1113
+ const parts = content.split(":");
1114
+ const lockPidStr = parts[0] ?? "0";
1115
+ const lockPid = parseInt(lockPidStr, 10);
1116
+ if (process.platform !== "win32") {
1117
+ try {
1118
+ process.kill(lockPid, 0);
1119
+ } catch {
1120
+ await fs2.unlink(lockfilePath);
1121
+ continue;
1122
+ }
1123
+ }
1124
+ } catch {
1125
+ try {
1126
+ await fs2.unlink(lockfilePath);
1127
+ } catch {
1128
+ }
1129
+ }
1130
+ await new Promise((r) => setTimeout(r, 100));
1131
+ continue;
1132
+ }
1133
+ throw err;
1134
+ }
1135
+ }
1136
+ throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
1137
+ }
1138
+ async function readRegistryFile(filePath) {
1139
+ try {
1140
+ const content = await fs2.readFile(filePath, "utf-8");
1141
+ const parsed = JSON.parse(content);
1142
+ if (parsed.instances && Array.isArray(parsed.instances)) {
1143
+ parsed.instances = new Map(parsed.instances);
1144
+ }
1145
+ return parsed;
1146
+ } catch (err) {
1147
+ if (err.code === "ENOENT") {
1148
+ return {
1149
+ version: 1,
1150
+ instances: /* @__PURE__ */ new Map(),
1151
+ protectedPatterns: ["wrongstack", "node"],
1152
+ lastCleanup: Date.now()
1153
+ };
1154
+ }
1155
+ throw err;
1156
+ }
1157
+ }
1158
+ async function writeRegistryFile(filePath, data) {
1159
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
1160
+ const content = JSON.stringify(data, (_k, v) => {
1161
+ if (v instanceof Map) {
1162
+ return Array.from(v.entries());
1163
+ }
1164
+ return v;
1165
+ }, 2);
1166
+ await fs2.writeFile(tmpPath, content, "utf-8");
1167
+ await fs2.rename(tmpPath, filePath);
1168
+ }
1169
+ var PersistentProcessRegistry = class {
1170
+ instanceId;
1171
+ registryPath;
1172
+ lockPath;
1173
+ baseRegistry;
1174
+ heartbeatInterval = null;
1175
+ isShuttingDown = false;
1176
+ constructor(baseRegistry) {
1177
+ this.instanceId = generateInstanceId();
1178
+ const homeDir = os2.homedir();
1179
+ this.registryPath = path3.join(homeDir, REGISTRY_FILE);
1180
+ this.lockPath = path3.join(homeDir, LOCKFILE);
1181
+ this.baseRegistry = baseRegistry ?? getProcessRegistry();
1182
+ this.ensureDirectory().catch((err) => {
1183
+ console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
1184
+ });
1185
+ }
1186
+ async ensureDirectory() {
1187
+ const dir = path3.dirname(this.registryPath);
1188
+ try {
1189
+ await fs2.mkdir(dir, { recursive: true });
1190
+ } catch (err) {
1191
+ if (err.code !== "EEXIST") throw err;
1192
+ }
1193
+ }
1194
+ /**
1195
+ * Start the heartbeat and periodic cleanup tasks.
1196
+ */
1197
+ start() {
1198
+ if (this.heartbeatInterval) return;
1199
+ this.syncToPersistent();
1200
+ this.heartbeatInterval = setInterval(() => {
1201
+ this.heartbeat();
1202
+ }, HEARTBEAT_INTERVAL_MS);
1203
+ this.heartbeatInterval.unref?.();
1204
+ setInterval(() => {
1205
+ this.cleanupStaleEntries();
1206
+ }, STALE_THRESHOLD_MS).unref?.();
1207
+ this.registerMainProcess();
1208
+ process.on("exit", () => this.syncToPersistent());
1209
+ }
1210
+ /**
1211
+ * Stop the heartbeat and clean up.
1212
+ */
1213
+ stop() {
1214
+ this.isShuttingDown = true;
1215
+ if (this.heartbeatInterval) {
1216
+ clearInterval(this.heartbeatInterval);
1217
+ this.heartbeatInterval = null;
1218
+ }
1219
+ this.syncToPersistent();
1220
+ }
1221
+ /**
1222
+ * Register the main WrongStack process as protected.
1223
+ */
1224
+ registerMainProcess() {
1225
+ const mainPid = process.pid;
1226
+ this.updatePersistentEntry({
1227
+ pid: mainPid,
1228
+ name: "wrongstack-main",
1229
+ command: process.argv.slice(0, 3).join(" "),
1230
+ startedAt: Date.now(),
1231
+ lastHeartbeat: Date.now(),
1232
+ instanceId: this.instanceId,
1233
+ hostname: os2.hostname(),
1234
+ protected: true,
1235
+ spawnMode: "main",
1236
+ parentPid: process.ppid,
1237
+ platform: process.platform
1238
+ });
1239
+ }
1240
+ /**
1241
+ * Register a spawned child process with the persistent registry.
1242
+ */
1243
+ registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
1244
+ const entry = {
1245
+ pid,
1246
+ name,
1247
+ command,
1248
+ startedAt: Date.now(),
1249
+ lastHeartbeat: Date.now(),
1250
+ instanceId: this.instanceId,
1251
+ hostname: os2.hostname(),
1252
+ protected: true,
1253
+ // All WrongStack child processes are protected by default
1254
+ spawnMode,
1255
+ parentPid: process.pid,
1256
+ platform: process.platform
1257
+ };
1258
+ if (sessionId) {
1259
+ entry.sessionId = sessionId;
1260
+ }
1261
+ this.updatePersistentEntry(entry);
1262
+ }
1263
+ /**
1264
+ * Update or add an entry in the persistent registry.
1265
+ */
1266
+ async updatePersistentEntry(entry) {
1267
+ const release = await acquireLock(this.lockPath);
1268
+ try {
1269
+ const data = await readRegistryFile(this.registryPath);
1270
+ data.instances.set(String(entry.pid), entry);
1271
+ this.baseRegistry.register({
1272
+ pid: entry.pid,
1273
+ name: entry.name,
1274
+ command: entry.command,
1275
+ startedAt: entry.startedAt,
1276
+ sessionId: entry.sessionId,
1277
+ protected: entry.protected,
1278
+ child: null
1279
+ // Main process has no child handle
1280
+ });
1281
+ await writeRegistryFile(this.registryPath, data);
1282
+ } finally {
1283
+ await release();
1284
+ }
1285
+ }
1286
+ /**
1287
+ * Unregister a process from the persistent registry.
1288
+ */
1289
+ async unregister(pid) {
1290
+ const release = await acquireLock(this.lockPath);
1291
+ try {
1292
+ const data = await readRegistryFile(this.registryPath);
1293
+ data.instances.delete(String(pid));
1294
+ await writeRegistryFile(this.registryPath, data);
1295
+ } finally {
1296
+ await release();
1297
+ }
1298
+ }
1299
+ /**
1300
+ * Send heartbeat to mark all this instance's processes as alive.
1301
+ */
1302
+ heartbeat() {
1303
+ if (this.isShuttingDown) return;
1304
+ this.syncToPersistent();
1305
+ }
1306
+ /**
1307
+ * Sync this instance's processes to the persistent registry.
1308
+ */
1309
+ async syncToPersistent() {
1310
+ const release = await acquireLock(this.lockPath);
1311
+ try {
1312
+ const data = await readRegistryFile(this.registryPath);
1313
+ const now = Date.now();
1314
+ const updatedInstances = /* @__PURE__ */ new Map();
1315
+ for (const [_pidStr, entry] of data.instances) {
1316
+ if (entry.instanceId === this.instanceId) {
1317
+ entry.lastHeartbeat = now;
1318
+ }
1319
+ if (entry.instanceId === this.instanceId || now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
1320
+ updatedInstances.set(_pidStr, entry);
1321
+ }
1322
+ }
1323
+ data.instances = updatedInstances;
1324
+ data.lastCleanup = now;
1325
+ await writeRegistryFile(this.registryPath, data);
1326
+ } catch (err) {
1327
+ console.error("PersistentProcessRegistry: sync failed", err);
1328
+ } finally {
1329
+ await release();
1330
+ }
1331
+ }
1332
+ /**
1333
+ * Remove entries for processes that are no longer running.
1334
+ */
1335
+ async cleanupStaleEntries() {
1336
+ const release = await acquireLock(this.lockPath);
1337
+ try {
1338
+ const data = await readRegistryFile(this.registryPath);
1339
+ const now = Date.now();
1340
+ const stalePids = [];
1341
+ for (const [_pidStr, entry] of data.instances) {
1342
+ const age = now - entry.lastHeartbeat;
1343
+ if (age > STALE_THRESHOLD_MS) {
1344
+ try {
1345
+ if (process.platform !== "win32") {
1346
+ process.kill(entry.pid, 0);
1347
+ } else {
1348
+ console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
1349
+ }
1350
+ } catch {
1351
+ stalePids.push(_pidStr);
1352
+ }
1353
+ }
1354
+ }
1355
+ if (stalePids.length > 0) {
1356
+ for (const pidStr of stalePids) {
1357
+ data.instances.delete(pidStr);
1358
+ }
1359
+ await writeRegistryFile(this.registryPath, data);
1360
+ }
1361
+ } catch (err) {
1362
+ console.error("PersistentProcessRegistry: cleanup failed", err);
1363
+ } finally {
1364
+ await release();
1365
+ }
1366
+ }
1367
+ /**
1368
+ * Check if a PID belongs to a WrongStack process and should be protected.
1369
+ */
1370
+ async isProtectedPid(pid) {
1371
+ const release = await acquireLock(this.lockPath);
1372
+ try {
1373
+ const data = await readRegistryFile(this.registryPath);
1374
+ const entry = data.instances.get(String(pid));
1375
+ if (!entry) return false;
1376
+ if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
1377
+ return false;
1378
+ }
1379
+ return entry.protected;
1380
+ } finally {
1381
+ await release();
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Get all protected PIDs from all WrongStack instances.
1386
+ */
1387
+ async getAllProtectedPids() {
1388
+ const release = await acquireLock(this.lockPath);
1389
+ try {
1390
+ const data = await readRegistryFile(this.registryPath);
1391
+ const now = Date.now();
1392
+ const protectedPids = [];
1393
+ for (const [_pidStr, entry] of data.instances) {
1394
+ if (entry.protected && now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
1395
+ protectedPids.push(entry.pid);
1396
+ }
1397
+ }
1398
+ return protectedPids;
1399
+ } finally {
1400
+ await release();
1401
+ }
1402
+ }
1403
+ /**
1404
+ * Get complete status of all tracked processes across all instances.
1405
+ */
1406
+ async getGlobalStatus() {
1407
+ const release = await acquireLock(this.lockPath);
1408
+ try {
1409
+ const data = await readRegistryFile(this.registryPath);
1410
+ const now = Date.now();
1411
+ const instances = /* @__PURE__ */ new Map();
1412
+ let protectedCount = 0;
1413
+ let staleCount = 0;
1414
+ for (const [_pidStr, entry] of data.instances) {
1415
+ const instanceEntries = instances.get(entry.instanceId) ?? [];
1416
+ instanceEntries.push(entry);
1417
+ instances.set(entry.instanceId, instanceEntries);
1418
+ if (entry.protected) protectedCount++;
1419
+ if (now - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
1420
+ }
1421
+ return {
1422
+ instances,
1423
+ totalProcesses: data.instances.size,
1424
+ protectedCount,
1425
+ staleCount
1426
+ };
1427
+ } finally {
1428
+ await release();
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Get the instance ID for this process.
1433
+ */
1434
+ getInstanceId() {
1435
+ return this.instanceId;
1436
+ }
1437
+ /**
1438
+ * Check if a kill command should be blocked.
1439
+ * Returns true if the kill should be blocked (target is a WrongStack process).
1440
+ */
1441
+ async shouldBlockKill(pid) {
1442
+ const protectedPids = await this.getAllProtectedPids();
1443
+ return protectedPids.includes(pid);
1444
+ }
1445
+ /**
1446
+ * Add a pattern-based protection rule.
1447
+ * Processes whose command matches any protected pattern are protected.
1448
+ */
1449
+ async addProtectedPattern(pattern) {
1450
+ const release = await acquireLock(this.lockPath);
1451
+ try {
1452
+ const data = await readRegistryFile(this.registryPath);
1453
+ if (!data.protectedPatterns.includes(pattern)) {
1454
+ data.protectedPatterns.push(pattern);
1455
+ await writeRegistryFile(this.registryPath, data);
1456
+ }
1457
+ } finally {
1458
+ await release();
1459
+ }
1460
+ }
1461
+ };
1462
+ var _persistentRegistry;
1463
+ function getPersistentProcessRegistry() {
1464
+ if (!_persistentRegistry) {
1465
+ _persistentRegistry = new PersistentProcessRegistry();
1466
+ }
1467
+ return _persistentRegistry;
1468
+ }
1469
+
1470
+ // src/bash-kill-guard.ts
1471
+ function extractKillCommand(command) {
1472
+ const normalized = command.replace(/\s+/g, " ").trim();
1473
+ const shellCMatch = normalized.match(
1474
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
1475
+ );
1476
+ if (shellCMatch?.[1]) {
1477
+ const inner = shellCMatch[1].trim();
1478
+ return isKillRelatedCommand(inner) ? inner : null;
1479
+ }
1480
+ const shellCUnquoted = normalized.match(
1481
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
1482
+ );
1483
+ if (shellCUnquoted?.[1]) {
1484
+ return shellCUnquoted[1];
1485
+ }
1486
+ return null;
1487
+ }
1488
+ function isKillRelatedCommand(cmd) {
1489
+ const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
1490
+ if (/^kill(\s|$)/.test(normalized)) return true;
1491
+ if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
1492
+ if (/^taskkill\s/i.test(normalized)) return true;
1493
+ if (/^tskill\s/i.test(normalized)) return true;
1494
+ if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
1495
+ return false;
1496
+ }
1497
+ function parseKillCommand(command) {
1498
+ const normalized = command.replace(/\s+/g, " ").trim();
1499
+ const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
1500
+ if (simpleMatch) {
1501
+ const signal = simpleMatch[1] ?? "-TERM";
1502
+ const pidOrGroup = simpleMatch[2];
1503
+ if (!pidOrGroup) return null;
1504
+ const isGroupKill = pidOrGroup.startsWith("-");
1505
+ const pid = isGroupKill ? parseInt(pidOrGroup.slice(1), 10) : parseInt(pidOrGroup, 10);
1506
+ return {
1507
+ pid,
1508
+ signal: signal.slice(1),
1509
+ isGroupKill,
1510
+ isAllKill: false,
1511
+ originalCommand: command
1512
+ };
1513
+ }
1514
+ const pkillMatch = normalized.match(/^pkill\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
1515
+ if (pkillMatch?.[2]) {
1516
+ const name = pkillMatch[2];
1517
+ const signalMatch = pkillMatch[1];
1518
+ return {
1519
+ name,
1520
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
1521
+ isGroupKill: false,
1522
+ isAllKill: false,
1523
+ originalCommand: command
1524
+ };
1525
+ }
1526
+ const killallMatch = normalized.match(/^killall\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
1527
+ if (killallMatch?.[2]) {
1528
+ const name = killallMatch[2];
1529
+ const signalMatch = killallMatch[1];
1530
+ return {
1531
+ name,
1532
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
1533
+ isGroupKill: false,
1534
+ isAllKill: false,
1535
+ originalCommand: command
1536
+ };
1537
+ }
1538
+ const pgrepMatch = normalized.match(/^pgrep\s+(.+)$/);
1539
+ if (pgrepMatch) {
1540
+ return null;
1541
+ }
1542
+ const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
1543
+ if (taskkillMatch?.[1]) {
1544
+ const pidStr = taskkillMatch[1];
1545
+ return {
1546
+ pid: parseInt(pidStr, 10),
1547
+ signal: normalized.includes("/F") ? "FORCE" : "TERM",
1548
+ isGroupKill: false,
1549
+ isAllKill: false,
1550
+ originalCommand: command
1551
+ };
1552
+ }
1553
+ const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
1554
+ if (tskillMatch?.[1]) {
1555
+ const pidStr = tskillMatch[1];
1556
+ return {
1557
+ pid: parseInt(pidStr, 10),
1558
+ signal: "TERM",
1559
+ isGroupKill: false,
1560
+ isAllKill: false,
1561
+ originalCommand: command
1562
+ };
1563
+ }
1564
+ return null;
1565
+ }
1566
+ async function getProtectedEntries() {
1567
+ const registry = getPersistentProcessRegistry();
1568
+ const status = await registry.getGlobalStatus();
1569
+ const entries = [];
1570
+ for (const instanceEntries of status.instances.values()) {
1571
+ for (const entry of instanceEntries) {
1572
+ if (entry.protected && Date.now() - entry.lastHeartbeat < 3e4) {
1573
+ entries.push(entry);
1574
+ }
1575
+ }
1576
+ }
1577
+ return entries;
1578
+ }
1579
+ async function isKillProtected(kill) {
1580
+ const registry = getPersistentProcessRegistry();
1581
+ if (kill.name) {
1582
+ const entries = await getProtectedEntries();
1583
+ const killNameLower = kill.name.toLowerCase();
1584
+ for (const entry of entries) {
1585
+ if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
1586
+ return true;
1587
+ }
1588
+ }
1589
+ if (killNameLower.includes("wrongstack")) {
1590
+ return true;
1591
+ }
1592
+ if (killNameLower.includes("node") && entries.length > 0) {
1593
+ return true;
1594
+ }
1595
+ return false;
1596
+ }
1597
+ if (kill.isGroupKill) {
1598
+ const protectedPids = await registry.getAllProtectedPids();
1599
+ return protectedPids.length > 0;
1600
+ }
1601
+ if (kill.pid !== void 0) {
1602
+ return registry.shouldBlockKill(kill.pid);
1603
+ }
1604
+ return false;
1605
+ }
1606
+ async function checkAndBlockKillCommand(command) {
1607
+ const normalized = command.replace(/\s+/g, " ").trim();
1608
+ const killCmd = extractKillCommand(normalized) || (isKillRelatedCommand(normalized) ? normalized : null);
1609
+ if (!killCmd) {
1610
+ return { blocked: false };
1611
+ }
1612
+ const parsed = parseKillCommand(killCmd);
1613
+ if (!parsed) {
1614
+ if (killCmd.includes("kill") && /kill\s+.*\|/.test(killCmd)) {
1615
+ return {
1616
+ blocked: true,
1617
+ reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
1618
+ };
1619
+ }
1620
+ return { blocked: false };
1621
+ }
1622
+ if (await isKillProtected(parsed)) {
1623
+ let target;
1624
+ if (parsed.name) {
1625
+ target = `process name "${parsed.name}"`;
1626
+ } else if (parsed.pid !== void 0) {
1627
+ target = `PID ${parsed.pid}`;
1628
+ } else {
1629
+ target = "(unknown target)";
1630
+ }
1631
+ const signal = parsed.signal ? ` (${parsed.signal})` : "";
1632
+ const groupNote = parsed.isGroupKill ? " (process group)" : "";
1633
+ return {
1634
+ blocked: true,
1635
+ reason: `Blocked: kill${signal} ${target}${groupNote} targets a protected WrongStack process.`
1636
+ };
1637
+ }
1638
+ return { blocked: false };
1639
+ }
1070
1640
 
1071
1641
  // src/bash.ts
1072
1642
  var MAX_OUTPUT = 32768;
@@ -1136,6 +1706,20 @@ var bashTool = {
1136
1706
  };
1137
1707
  return;
1138
1708
  }
1709
+ const killCheck = await checkAndBlockKillCommand(input.command);
1710
+ if (killCheck.blocked) {
1711
+ yield {
1712
+ type: "final",
1713
+ output: {
1714
+ output: "",
1715
+ exit_code: 1,
1716
+ timed_out: false,
1717
+ pid: null,
1718
+ error: killCheck.reason || "Kill command blocked: targets a protected WrongStack process."
1719
+ }
1720
+ };
1721
+ return;
1722
+ }
1139
1723
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
1140
1724
  if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
1141
1725
  console.warn(JSON.stringify({
@@ -1148,7 +1732,7 @@ var bashTool = {
1148
1732
  }));
1149
1733
  }
1150
1734
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
1151
- const isWin3 = os.platform() === "win32";
1735
+ const isWin3 = os2.platform() === "win32";
1152
1736
  const shell = (() => {
1153
1737
  const explicit = process.env[isWin3 ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
1154
1738
  if (explicit) return explicit;
@@ -1762,7 +2346,7 @@ function loadDatabaseSync() {
1762
2346
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
1763
2347
  } catch (err) {
1764
2348
  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)}`
2349
+ `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage$1(err)}`
1766
2350
  );
1767
2351
  }
1768
2352
  return DatabaseSyncCtor;
@@ -1911,33 +2495,53 @@ var IndexStore = class {
1911
2495
  }
1912
2496
  }
1913
2497
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
1914
- insertSymbols(symbols, nextId) {
2498
+ /**
2499
+ * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
2500
+ * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
2501
+ * the same transaction, preventing UNIQUE constraint violations when two
2502
+ * processes index concurrently (each would see a different `MAX(id)` and
2503
+ * neither can insert with the other's IDs).
2504
+ *
2505
+ * @returns The symbols array with `id` fields populated so the caller can
2506
+ * use them for refs without re-reading from the DB.
2507
+ */
2508
+ insertSymbols(symbols) {
1915
2509
  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
2510
+ this.db.exec("BEGIN IMMEDIATE");
2511
+ try {
2512
+ const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
2513
+ let nextId = (maxRows[0]?.m ?? 0) + 1;
2514
+ const stmt = this.db.prepare(
2515
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
2516
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1936
2517
  );
1937
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
1938
- id++;
2518
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
2519
+ const result = [];
2520
+ for (const s of symbols) {
2521
+ const id = nextId++;
2522
+ stmt.run(
2523
+ id,
2524
+ s.lang,
2525
+ s.kind,
2526
+ s.name,
2527
+ s.file,
2528
+ s.line,
2529
+ s.col,
2530
+ s.signature,
2531
+ s.docComment,
2532
+ s.scope,
2533
+ s.text,
2534
+ s.file
2535
+ );
2536
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
2537
+ result.push({ ...s, id });
2538
+ }
2539
+ this.db.exec("COMMIT");
2540
+ return result;
2541
+ } catch (err) {
2542
+ this.db.exec("ROLLBACK");
2543
+ throw err;
1939
2544
  }
1940
- return id;
1941
2545
  });
1942
2546
  }
1943
2547
  deleteSymbolsForFile(file) {
@@ -2491,10 +3095,10 @@ function detectLang(file) {
2491
3095
  if (idx < 0) return null;
2492
3096
  return extToLang(file.slice(idx));
2493
3097
  }
2494
- function parseSymbols2(opts) {
3098
+ async function parseSymbols2(opts) {
2495
3099
  const { file, content, lang } = opts;
2496
3100
  try {
2497
- return syncGoParse(file, content, lang);
3101
+ return await syncGoParse(file, content, lang);
2498
3102
  } catch {
2499
3103
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2500
3104
  }
@@ -2731,19 +3335,34 @@ func formatType(t ast.Expr) string {
2731
3335
  }
2732
3336
  }
2733
3337
  `;
2734
- function syncGoParse(filePath, content, lang) {
2735
- const tmpDir = path3.join(os.tmpdir(), "ws-go-parse");
3338
+ async function syncGoParse(filePath, content, lang) {
3339
+ const tmpDir = path3.join(os2.tmpdir(), "ws-go-parse");
2736
3340
  try {
2737
- mkdirSync(tmpDir, { recursive: true });
3341
+ await fs2.mkdir(tmpDir, { recursive: true });
2738
3342
  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",
3343
+ await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
3344
+ const proc = spawn("go", ["run", scriptPath], {
3345
+ stdio: ["pipe", "pipe", "pipe"],
2744
3346
  windowsHide: true
2745
3347
  });
2746
- if (!stdout.trim()) {
3348
+ let stdout = "";
3349
+ proc.stdout?.on("data", (chunk) => {
3350
+ stdout += chunk.toString();
3351
+ });
3352
+ proc.stdin?.write(content);
3353
+ proc.stdin?.end();
3354
+ const { code } = await Promise.race([
3355
+ new Promise((resolve6) => {
3356
+ proc.on("close", (c) => resolve6({ code: c }));
3357
+ }),
3358
+ new Promise(
3359
+ (_, reject) => setTimeout(() => {
3360
+ proc.kill("SIGKILL");
3361
+ reject(new Error("timeout"));
3362
+ }, 15e3)
3363
+ )
3364
+ ]).catch(() => ({ code: -1 }));
3365
+ if (code !== 0 || !stdout.trim()) {
2747
3366
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2748
3367
  }
2749
3368
  const raw = JSON.parse(stdout.trim());
@@ -2765,10 +3384,10 @@ function syncGoParse(filePath, content, lang) {
2765
3384
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2766
3385
  }
2767
3386
  }
2768
- function parseSymbols3(opts) {
3387
+ async function parseSymbols3(opts) {
2769
3388
  const { file, lang } = opts;
2770
3389
  try {
2771
- return syncPyParse(file, lang);
3390
+ return await syncPyParse(file, lang);
2772
3391
  } catch {
2773
3392
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2774
3393
  }
@@ -2977,18 +3596,32 @@ visitor.visit(tree)
2977
3596
 
2978
3597
  print(json.dumps([s.to_dict() for s in syms]))
2979
3598
  `;
2980
- function syncPyParse(filePath, lang) {
3599
+ async function syncPyParse(filePath, lang) {
2981
3600
  try {
2982
- const tmpDir = path3.join(os.tmpdir(), "ws-py-parse");
2983
- mkdirSync(tmpDir, { recursive: true });
3601
+ const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
3602
+ await fs2.mkdir(tmpDir, { recursive: true });
2984
3603
  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",
3604
+ await fs2.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
3605
+ const proc = spawn("python", [scriptPath, filePath], {
3606
+ stdio: ["pipe", "pipe", "pipe"],
2989
3607
  windowsHide: true
2990
3608
  });
2991
- if (!stdout.trim()) {
3609
+ let stdout = "";
3610
+ proc.stdout?.on("data", (chunk) => {
3611
+ stdout += chunk.toString();
3612
+ });
3613
+ const { code } = await Promise.race([
3614
+ new Promise((resolve6) => {
3615
+ proc.on("close", (c) => resolve6({ code: c }));
3616
+ }),
3617
+ new Promise(
3618
+ (_, reject) => setTimeout(() => {
3619
+ proc.kill("SIGKILL");
3620
+ reject(new Error("timeout"));
3621
+ }, 15e3)
3622
+ )
3623
+ ]).catch(() => ({ code: -1 }));
3624
+ if (code !== 0 || !stdout.trim()) {
2992
3625
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2993
3626
  }
2994
3627
  const raw = JSON.parse(stdout.trim());
@@ -3010,11 +3643,11 @@ function syncPyParse(filePath, lang) {
3010
3643
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
3011
3644
  }
3012
3645
  }
3013
- function parseSymbols4(opts) {
3646
+ async function parseSymbols4(opts) {
3014
3647
  const { file, content, lang } = opts;
3015
3648
  const nativeAvailable = checkNativeParser();
3016
3649
  if (nativeAvailable) {
3017
- const result = tryNativeParse(file, content);
3650
+ const result = await tryNativeParse(file, content);
3018
3651
  if (result) return result;
3019
3652
  }
3020
3653
  return regexParse({ file, content, lang });
@@ -3044,25 +3677,34 @@ function checkNativeParser() {
3044
3677
  return false;
3045
3678
  }
3046
3679
  }
3047
- function tryNativeParse(file, content) {
3680
+ async function tryNativeParse(file, content) {
3048
3681
  try {
3049
3682
  const toolsDir = path3.join(process.cwd(), "tools");
3050
3683
  const crateDir = path3.join(toolsDir, "syn-parser");
3051
3684
  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);
3685
+ await fs2.writeFile(tmpFile, content, "utf8");
3686
+ const proc = spawn("cargo", ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")], {
3687
+ cwd: process.cwd(),
3688
+ stdio: ["pipe", "pipe", "pipe"],
3689
+ windowsHide: true
3690
+ });
3691
+ let stdout = "";
3692
+ proc.stdout?.on("data", (chunk) => {
3693
+ stdout += chunk.toString();
3694
+ });
3695
+ const { code } = await Promise.race([
3696
+ new Promise((resolve6) => {
3697
+ proc.on("close", (c) => resolve6({ code: c }));
3698
+ }),
3699
+ new Promise(
3700
+ (_, reject) => setTimeout(() => {
3701
+ proc.kill("SIGKILL");
3702
+ reject(new Error("timeout"));
3703
+ }, 15e3)
3704
+ )
3705
+ ]).catch(() => ({ code: -1 }));
3706
+ if (code === 0 && stdout.trim()) {
3707
+ const symbols = JSON.parse(stdout.trim());
3066
3708
  return {
3067
3709
  file,
3068
3710
  lang: "rs",
@@ -3531,7 +4173,7 @@ function compileGitignore(lines) {
3531
4173
  async function loadGitignoreMatcher(projectRoot) {
3532
4174
  let lines = [];
3533
4175
  try {
3534
- const raw = await fs14.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
4176
+ const raw = await fs2.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
3535
4177
  lines = raw.split("\n");
3536
4178
  } catch {
3537
4179
  }
@@ -3589,7 +4231,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
3589
4231
  }
3590
4232
  let entries;
3591
4233
  try {
3592
- entries = await fs14.readdir(dir, { withFileTypes: true });
4234
+ entries = await fs2.readdir(dir, { withFileTypes: true });
3593
4235
  } catch {
3594
4236
  return;
3595
4237
  }
@@ -3687,7 +4329,7 @@ async function runIndexerWithStore(store, opts) {
3687
4329
  batchFiles.map(async (file) => {
3688
4330
  let stat11;
3689
4331
  try {
3690
- stat11 = await fs14.stat(file, statOpts);
4332
+ stat11 = await fs2.stat(file, statOpts);
3691
4333
  } catch (e) {
3692
4334
  if (isAbortError(e)) throw e;
3693
4335
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
@@ -3701,7 +4343,7 @@ async function runIndexerWithStore(store, opts) {
3701
4343
  }
3702
4344
  let content;
3703
4345
  try {
3704
- content = await fs14.readFile(file, { encoding: "utf8", signal });
4346
+ content = await fs2.readFile(file, { encoding: "utf8", signal });
3705
4347
  } catch (e) {
3706
4348
  if (isAbortError(e)) throw e;
3707
4349
  return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
@@ -3751,9 +4393,7 @@ async function runIndexerWithStore(store, opts) {
3751
4393
  filesIndexed++;
3752
4394
  continue;
3753
4395
  }
3754
- const nextId = store.getMaxSymbolId() + 1;
3755
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
3756
- store.insertSymbols(symbolsWithIds, nextId);
4396
+ const symbolsWithIds = store.insertSymbols(parsed.symbols);
3757
4397
  const count = symbolsWithIds.length;
3758
4398
  symbolsIndexed += count;
3759
4399
  langStats[lang] = (langStats[lang] ?? 0) + count;
@@ -3782,7 +4422,7 @@ async function runIndexerWithStore(store, opts) {
3782
4422
  }
3783
4423
  for (const [file_] of existingMeta) {
3784
4424
  try {
3785
- await fs14.stat(file_);
4425
+ await fs2.stat(file_);
3786
4426
  } catch {
3787
4427
  store.deleteFile(file_);
3788
4428
  }
@@ -4032,10 +4672,10 @@ function circuitOpenError() {
4032
4672
  "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
4673
  );
4034
4674
  }
4035
- function isUniqueConstraintError(err) {
4675
+ function isRecoverableConstraintError(err) {
4036
4676
  if (err instanceof Error) {
4037
4677
  const msg = err.message.toLowerCase();
4038
- return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
4678
+ return msg.includes("unique constraint") || msg.includes("constraint failed");
4039
4679
  }
4040
4680
  return false;
4041
4681
  }
@@ -4068,7 +4708,7 @@ async function runStartupIndex(opts) {
4068
4708
  return result;
4069
4709
  } catch (err) {
4070
4710
  _lastError = err instanceof Error ? err.message : String(err);
4071
- if (isUniqueConstraintError(err) && !opts.force) {
4711
+ if (isRecoverableConstraintError(err) && !opts.force) {
4072
4712
  _lastError = null;
4073
4713
  const rebuildResult = await runStartupIndex({
4074
4714
  ...opts,
@@ -4428,9 +5068,9 @@ async function fileDiff(input, ctx, _signal) {
4428
5068
  const results = [];
4429
5069
  for (const file of files) {
4430
5070
  const absPath = safeResolve(file, ctx);
4431
- const stat11 = await fs14.stat(absPath).catch(() => null);
5071
+ const stat11 = await fs2.stat(absPath).catch(() => null);
4432
5072
  if (!stat11?.isFile()) continue;
4433
- const content = await fs14.readFile(absPath, "utf8");
5073
+ const content = await fs2.readFile(absPath, "utf8");
4434
5074
  const lines = content.split(/\r?\n/);
4435
5075
  results.push(formatWithLineNumbers(file, lines));
4436
5076
  }
@@ -4490,7 +5130,7 @@ var documentTool = {
4490
5130
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
4491
5131
  for (const absPath of fileList) {
4492
5132
  try {
4493
- const content = await fs14.readFile(absPath, "utf8");
5133
+ const content = await fs2.readFile(absPath, "utf8");
4494
5134
  filesProcessed++;
4495
5135
  const processed = processFile(
4496
5136
  content,
@@ -4526,7 +5166,7 @@ async function resolveFiles(filesInput, cwd) {
4526
5166
  for (const f of files) {
4527
5167
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
4528
5168
  try {
4529
- const stat11 = await fs14.stat(absPath);
5169
+ const stat11 = await fs2.stat(absPath);
4530
5170
  if (stat11.isFile()) resolved.push(absPath);
4531
5171
  } catch {
4532
5172
  }
@@ -4619,7 +5259,7 @@ var editTool = {
4619
5259
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
4620
5260
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
4621
5261
  const absPath = await safeResolveReal(input.path, ctx);
4622
- const stat11 = await fs14.stat(absPath).catch((err) => {
5262
+ const stat11 = await fs2.stat(absPath).catch((err) => {
4623
5263
  if (err.code === "ENOENT") {
4624
5264
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
4625
5265
  }
@@ -4627,8 +5267,8 @@ var editTool = {
4627
5267
  });
4628
5268
  if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
4629
5269
  const autoRead = !ctx.hasRead(absPath);
4630
- const original = await fs14.readFile(absPath, "utf8");
4631
- const updated = await fs14.stat(absPath);
5270
+ const original = await fs2.readFile(absPath, "utf8");
5271
+ const updated = await fs2.stat(absPath);
4632
5272
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
4633
5273
  const lastReadMtime = ctx.lastReadMtime(absPath);
4634
5274
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -4674,7 +5314,7 @@ var editTool = {
4674
5314
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
4675
5315
  const newFile = toStyle(newFileLf, style);
4676
5316
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
4677
- const written = await fs14.stat(absPath);
5317
+ const written = await fs2.stat(absPath);
4678
5318
  ctx.recordRead(absPath, written.mtimeMs);
4679
5319
  ctx.session.recordFileChange({
4680
5320
  path: absPath,
@@ -4930,6 +5570,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4930
5570
  const resolved = resolveWin32Command(cmd);
4931
5571
  const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
4932
5572
  const spawnCmd = needsShell ? cmd : resolved;
5573
+ if (needsShell) assertSafeWin32ShellArgs(args);
4933
5574
  const child = spawn(spawnCmd, args, {
4934
5575
  cwd,
4935
5576
  env: buildChildEnv(sessionId),
@@ -5023,8 +5664,8 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
5023
5664
  );
5024
5665
  }
5025
5666
  var combineSignals = (signals) => AbortSignal.any(signals);
5026
- function guardedLookup(hostname, options, callback) {
5027
- dns.lookup(hostname, { all: true }).then((records) => {
5667
+ function guardedLookup(hostname2, options, callback) {
5668
+ dns.lookup(hostname2, { all: true }).then((records) => {
5028
5669
  const family = options?.family;
5029
5670
  const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
5030
5671
  const list = byFamily.length > 0 ? byFamily : records;
@@ -5051,7 +5692,7 @@ function guardedLookup(hostname, options, callback) {
5051
5692
  const first = list.at(0);
5052
5693
  if (!first) {
5053
5694
  callback(
5054
- Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
5695
+ Object.assign(new Error(`fetch: no address for ${hostname2}`), { code: "ENOTFOUND" })
5055
5696
  );
5056
5697
  return;
5057
5698
  }
@@ -5165,7 +5806,13 @@ var fetchTool = {
5165
5806
  const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
5166
5807
  const combined = combineSignals([opts.signal, ctrl.signal]);
5167
5808
  try {
5168
- const res = await guardedFetch(input.url, 5, combined);
5809
+ let res;
5810
+ try {
5811
+ res = await guardedFetch(input.url, 5, combined);
5812
+ } catch (err) {
5813
+ if (opts.signal.aborted) throw err;
5814
+ throw describeFetchError(err, input.url, ctrl.signal.aborted);
5815
+ }
5169
5816
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
5170
5817
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
5171
5818
  throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
@@ -5221,9 +5868,9 @@ var fetchTool = {
5221
5868
  }
5222
5869
  }
5223
5870
  };
5224
- async function assertNotPrivate(hostname) {
5871
+ async function assertNotPrivate(hostname2) {
5225
5872
  if (ALLOW_PRIVATE) return;
5226
- const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
5873
+ const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
5227
5874
  if (host === "localhost" || host.endsWith(".localhost")) {
5228
5875
  throw new Error("fetch: blocked localhost target");
5229
5876
  }
@@ -5250,6 +5897,23 @@ async function assertNotPrivate(hostname) {
5250
5897
  }
5251
5898
  }
5252
5899
  }
5900
+ function describeFetchError(err, url, timedOut) {
5901
+ if (timedOut) {
5902
+ return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
5903
+ }
5904
+ const parts = [];
5905
+ const seen = /* @__PURE__ */ new Set();
5906
+ let cur = err;
5907
+ while (cur instanceof Error && !seen.has(cur)) {
5908
+ seen.add(cur);
5909
+ const code = cur.code;
5910
+ const label = code ? `${code}: ${cur.message}` : cur.message;
5911
+ if (label && label !== "fetch failed" && !parts.includes(label)) parts.push(label);
5912
+ cur = cur.cause;
5913
+ }
5914
+ const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
5915
+ return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
5916
+ }
5253
5917
  function prettyJson(s) {
5254
5918
  try {
5255
5919
  return JSON.stringify(JSON.parse(s), null, 2);
@@ -5681,7 +6345,7 @@ var globTool = {
5681
6345
  }
5682
6346
  let entries;
5683
6347
  try {
5684
- entries = await fs14.readdir(dir, { withFileTypes: true });
6348
+ entries = await fs2.readdir(dir, { withFileTypes: true });
5685
6349
  } catch {
5686
6350
  return;
5687
6351
  }
@@ -5697,7 +6361,7 @@ var globTool = {
5697
6361
  } else if (e.isFile()) {
5698
6362
  if (re.test(rel) || re.test(name)) {
5699
6363
  try {
5700
- const st = await fs14.stat(full);
6364
+ const st = await fs2.stat(full);
5701
6365
  results.push({ rel: full, mtime: st.mtimeMs });
5702
6366
  if (results.length >= limit) {
5703
6367
  truncated = true;
@@ -5716,7 +6380,7 @@ var globTool = {
5716
6380
  };
5717
6381
  async function readGitignore(dir) {
5718
6382
  try {
5719
- const raw = await fs14.readFile(path3.join(dir, ".gitignore"), "utf8");
6383
+ const raw = await fs2.readFile(path3.join(dir, ".gitignore"), "utf8");
5720
6384
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
5721
6385
  } catch {
5722
6386
  return [];
@@ -5770,6 +6434,7 @@ function capSubject(line) {
5770
6434
 
5771
6435
  // src/grep.ts
5772
6436
  var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
6437
+ var NATIVE_SCAN_CONCURRENCY = 32;
5773
6438
  var grepTool = {
5774
6439
  name: "grep",
5775
6440
  category: "Search",
@@ -5997,14 +6662,52 @@ async function runNative(input, base, mode, limit, signal) {
5997
6662
  const fileMatches = /* @__PURE__ */ new Map();
5998
6663
  let total = 0;
5999
6664
  let stopped = false;
6665
+ const scanFile = async (full, name) => {
6666
+ if (stopped || signal.aborted) return;
6667
+ if (globRe && !globRe.test(name) && !globRe.test(full)) return;
6668
+ if (globRe) globRe.lastIndex = 0;
6669
+ try {
6670
+ const stat11 = await fs2.stat(full);
6671
+ if (stat11.size > 1e6 || stopped || signal.aborted) return;
6672
+ const head = await fs2.readFile(full);
6673
+ if (isBinaryBuffer(head) || stopped || signal.aborted) return;
6674
+ const text = head.toString("utf8");
6675
+ const lines = text.split(/\r?\n/);
6676
+ let fileHits = 0;
6677
+ for (let i = 0; i < lines.length; i++) {
6678
+ if (stopped || signal.aborted) break;
6679
+ const ln = capSubject(lines[i] ?? "");
6680
+ re.lastIndex = 0;
6681
+ if (re.test(ln)) {
6682
+ fileHits++;
6683
+ total++;
6684
+ if (mode === "content" && matches.length < limit) {
6685
+ matches.push(`${full}:${i + 1}:${ln}`);
6686
+ }
6687
+ }
6688
+ }
6689
+ if (fileHits > 0) {
6690
+ fileMatches.set(full, fileHits);
6691
+ if (mode === "files_with_matches" && matches.length < limit) {
6692
+ matches.push(full);
6693
+ }
6694
+ if (mode === "count" && matches.length < limit) {
6695
+ matches.push(`${full}:${fileHits}`);
6696
+ }
6697
+ }
6698
+ if (matches.length >= limit) stopped = true;
6699
+ } catch {
6700
+ }
6701
+ };
6000
6702
  const walk = async (dir) => {
6001
6703
  if (stopped || signal.aborted) return;
6002
6704
  let entries;
6003
6705
  try {
6004
- entries = await fs14.readdir(dir, { withFileTypes: true });
6706
+ entries = await fs2.readdir(dir, { withFileTypes: true });
6005
6707
  } catch {
6006
6708
  return;
6007
6709
  }
6710
+ const files = [];
6008
6711
  for (const e of entries) {
6009
6712
  if (stopped) return;
6010
6713
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
@@ -6013,41 +6716,10 @@ async function runNative(input, base, mode, limit, signal) {
6013
6716
  if (e.isDirectory()) {
6014
6717
  await walk(full);
6015
6718
  } 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
- }
6719
+ files.push({ full, name: e.name });
6049
6720
  }
6050
6721
  }
6722
+ await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
6051
6723
  };
6052
6724
  await walk(base);
6053
6725
  return {
@@ -6057,6 +6729,20 @@ async function runNative(input, base, mode, limit, signal) {
6057
6729
  used: "native"
6058
6730
  };
6059
6731
  }
6732
+ async function mapWithConcurrency(items, concurrency, fn) {
6733
+ if (items.length === 0) return;
6734
+ let next = 0;
6735
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
6736
+ const workers = Array.from({ length: workerCount }, async () => {
6737
+ for (; ; ) {
6738
+ const idx = next++;
6739
+ if (idx >= items.length) return;
6740
+ const item = items[idx];
6741
+ if (item !== void 0) await fn(item);
6742
+ }
6743
+ });
6744
+ await Promise.all(workers);
6745
+ }
6060
6746
  var installTool = {
6061
6747
  name: "install",
6062
6748
  category: "Package Management",
@@ -6231,7 +6917,7 @@ var jsonTool = {
6231
6917
  let raw;
6232
6918
  if (input.file) {
6233
6919
  try {
6234
- raw = await fs14.readFile(input.file, "utf8");
6920
+ raw = await fs2.readFile(input.file, "utf8");
6235
6921
  } catch {
6236
6922
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
6237
6923
  }
@@ -6270,8 +6956,8 @@ var jsonTool = {
6270
6956
  };
6271
6957
  }
6272
6958
  };
6273
- function query(data, path20) {
6274
- const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
6959
+ function query(data, path21) {
6960
+ const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
6275
6961
  let current = data;
6276
6962
  for (const part of parts) {
6277
6963
  if (current === null || current === void 0) return void 0;
@@ -6548,7 +7234,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6548
7234
  }
6549
7235
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
6550
7236
  var MAX_TAIL_LINES = 1e5;
6551
- async function fileLogs(path20, lines, filterRe, stream) {
7237
+ async function fileLogs(path21, lines, filterRe, stream) {
6552
7238
  const { createInterface } = await import('node:readline');
6553
7239
  const { createReadStream } = await import('node:fs');
6554
7240
  const entries = [];
@@ -6557,7 +7243,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6557
7243
  let writeIdx = 0;
6558
7244
  let totalLines = 0;
6559
7245
  const rl = createInterface({
6560
- input: createReadStream(path20),
7246
+ input: createReadStream(path21),
6561
7247
  crlfDelay: Number.POSITIVE_INFINITY
6562
7248
  });
6563
7249
  for await (const line of rl) {
@@ -6578,7 +7264,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6578
7264
  if (parsed) entries.push(parsed);
6579
7265
  }
6580
7266
  return {
6581
- source: path20,
7267
+ source: path21,
6582
7268
  entries,
6583
7269
  total: entries.length,
6584
7270
  truncated: totalLines > effLines,
@@ -6682,6 +7368,7 @@ function runOutdated(manager, args, cwd, signal) {
6682
7368
  const resolved = resolveWin32Command(manager);
6683
7369
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
6684
7370
  const spawnCmd = needsShell ? manager : resolved;
7371
+ if (needsShell) assertSafeWin32ShellArgs(args);
6685
7372
  const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
6686
7373
  child.stdout?.on("data", (c) => {
6687
7374
  if (stdout.length < MAX) stdout += c.toString();
@@ -6779,12 +7466,12 @@ var patchTool = {
6779
7466
  };
6780
7467
  }
6781
7468
  }
6782
- const tmpDir = await fs14.mkdtemp(path3.join(os.tmpdir(), ".wstack_patch_"));
7469
+ const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), ".wstack_patch_"));
6783
7470
  try {
6784
- await fs14.chmod(tmpDir, 448).catch(() => {
7471
+ await fs2.chmod(tmpDir, 448).catch(() => {
6785
7472
  });
6786
7473
  const patchFile = path3.join(tmpDir, "in.diff");
6787
- await fs14.writeFile(patchFile, input.patch, { mode: 384 });
7474
+ await fs2.writeFile(patchFile, input.patch, { mode: 384 });
6788
7475
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
6789
7476
  const result = await runPatch(args, dir, opts.signal);
6790
7477
  if (result.exitCode !== 0 && !dryRun) {
@@ -6805,7 +7492,7 @@ var patchTool = {
6805
7492
  message: result.stdout || "patch applied"
6806
7493
  };
6807
7494
  } finally {
6808
- await fs14.rm(tmpDir, { recursive: true, force: true }).catch(() => {
7495
+ await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
6809
7496
  });
6810
7497
  }
6811
7498
  }
@@ -7151,7 +7838,7 @@ var readTool = {
7151
7838
  const absPath = await safeResolveReal(input.path, ctx);
7152
7839
  let stat11;
7153
7840
  try {
7154
- stat11 = await fs14.stat(absPath);
7841
+ stat11 = await fs2.stat(absPath);
7155
7842
  } catch (err) {
7156
7843
  const code = err.code;
7157
7844
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -7176,7 +7863,7 @@ var readTool = {
7176
7863
  note: "Repeated read suppressed to save tokens."
7177
7864
  };
7178
7865
  }
7179
- const buf = await fs14.readFile(absPath);
7866
+ const buf = await fs2.readFile(absPath);
7180
7867
  if (isBinaryBuffer(buf)) {
7181
7868
  throw new Error(`read: "${input.path}" appears to be binary`);
7182
7869
  }
@@ -7322,11 +8009,11 @@ var replaceTool = {
7322
8009
  const dryRun = input.dry_run ?? false;
7323
8010
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
7324
8011
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
7325
- const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
8012
+ const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
7326
8013
  const results = [];
7327
8014
  let totalReplacements = 0;
7328
8015
  for (const absPath of fileList) {
7329
- const lstat2 = await fs14.lstat(absPath).catch((err) => {
8016
+ const lstat2 = await fs2.lstat(absPath).catch((err) => {
7330
8017
  if (err.code === "ENOENT") return null;
7331
8018
  throw err;
7332
8019
  });
@@ -7334,17 +8021,17 @@ var replaceTool = {
7334
8021
  if (lstat2.isSymbolicLink()) continue;
7335
8022
  let realPath;
7336
8023
  try {
7337
- realPath = await fs14.realpath(absPath);
8024
+ realPath = await fs2.realpath(absPath);
7338
8025
  } catch {
7339
8026
  continue;
7340
8027
  }
7341
8028
  const rel = path3.relative(realRoot, realPath);
7342
8029
  if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
7343
- const stat11 = await fs14.stat(realPath).catch(() => null);
8030
+ const stat11 = await fs2.stat(realPath).catch(() => null);
7344
8031
  if (!stat11 || !stat11.isFile()) continue;
7345
8032
  let content;
7346
8033
  try {
7347
- const buf = await fs14.readFile(realPath);
8034
+ const buf = await fs2.readFile(realPath);
7348
8035
  if (isBinaryBuffer(buf)) continue;
7349
8036
  content = buf.toString("utf8");
7350
8037
  } catch {
@@ -7396,7 +8083,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
7396
8083
  const resolved = [];
7397
8084
  for (const p of parts) {
7398
8085
  const absPath = safeResolve(p, ctx);
7399
- const stat11 = await fs14.stat(absPath).catch(() => null);
8086
+ const stat11 = await fs2.stat(absPath).catch(() => null);
7400
8087
  if (stat11?.isFile()) {
7401
8088
  resolved.push(absPath);
7402
8089
  }
@@ -7452,7 +8139,7 @@ async function globNative(pattern, base, extraGlob) {
7452
8139
  const walk = async (dir) => {
7453
8140
  let entries;
7454
8141
  try {
7455
- entries = await fs14.readdir(dir, { withFileTypes: true });
8142
+ entries = await fs2.readdir(dir, { withFileTypes: true });
7456
8143
  } catch {
7457
8144
  return;
7458
8145
  }
@@ -7460,7 +8147,7 @@ async function globNative(pattern, base, extraGlob) {
7460
8147
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
7461
8148
  const full = path3.join(dir, e.name);
7462
8149
  try {
7463
- const stat11 = await fs14.lstat(full);
8150
+ const stat11 = await fs2.lstat(full);
7464
8151
  if (stat11.isSymbolicLink()) continue;
7465
8152
  } catch {
7466
8153
  continue;
@@ -7638,7 +8325,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
7638
8325
  }
7639
8326
  const fullPath = target;
7640
8327
  if (!dryRun) {
7641
- await fs14.mkdir(path3.dirname(fullPath), { recursive: true });
8328
+ await fs2.mkdir(path3.dirname(fullPath), { recursive: true });
7642
8329
  await atomicWrite(fullPath, substituteVars(content, name, vars));
7643
8330
  }
7644
8331
  files.push(resolvedPath);
@@ -7751,7 +8438,7 @@ async function duckduckgoSearch(query2, num, signal) {
7751
8438
  truncated: results.length >= num
7752
8439
  };
7753
8440
  } catch (err) {
7754
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
8441
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$1(err) }));
7755
8442
  return {
7756
8443
  query: query2,
7757
8444
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -7915,11 +8602,11 @@ var setWorkingDirTool = {
7915
8602
  } catch (err) {
7916
8603
  return {
7917
8604
  current: ctx.workingDir,
7918
- error: toErrorMessage(err)
8605
+ error: toErrorMessage$1(err)
7919
8606
  };
7920
8607
  }
7921
8608
  try {
7922
- await fs14.access(resolved);
8609
+ await fs2.access(resolved);
7923
8610
  } catch {
7924
8611
  try {
7925
8612
  ctx.setWorkingDir(previous);
@@ -8975,7 +9662,7 @@ var treeTool = {
8975
9662
  }
8976
9663
  };
8977
9664
  async function walkDir(dir, depth, opts) {
8978
- const entries = await fs14.readdir(dir, { withFileTypes: true }).catch(() => []);
9665
+ const entries = await fs2.readdir(dir, { withFileTypes: true }).catch(() => []);
8979
9666
  const filtered = entries.filter((e) => {
8980
9667
  if (!opts.showHidden && e.name.startsWith(".")) return false;
8981
9668
  if (opts.exclude.has(e.name)) return false;
@@ -9133,14 +9820,14 @@ var writeTool = {
9133
9820
  let existed = false;
9134
9821
  let prev = "";
9135
9822
  try {
9136
- const stat12 = await fs14.stat(absPath);
9823
+ const stat12 = await fs2.stat(absPath);
9137
9824
  existed = stat12.isFile();
9138
9825
  if (existed) {
9139
9826
  if (!ctx.hasRead(absPath)) {
9140
- prev = await fs14.readFile(absPath, "utf8");
9827
+ prev = await fs2.readFile(absPath, "utf8");
9141
9828
  ctx.recordRead(absPath, stat12.mtimeMs);
9142
9829
  } else {
9143
- prev = await fs14.readFile(absPath, "utf8");
9830
+ prev = await fs2.readFile(absPath, "utf8");
9144
9831
  }
9145
9832
  }
9146
9833
  } catch (err) {
@@ -9151,7 +9838,7 @@ var writeTool = {
9151
9838
  await atomicWrite(absPath, input.content);
9152
9839
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
9153
9840
  + (new file, ${input.content.split("\n").length} lines)`;
9154
- const stat11 = await fs14.stat(absPath);
9841
+ const stat11 = await fs2.stat(absPath);
9155
9842
  ctx.recordRead(absPath, stat11.mtimeMs);
9156
9843
  ctx.session.recordFileChange({
9157
9844
  path: absPath,