@wrongstack/tools 0.236.0 → 0.250.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/audit.js +1 -0
  2. package/dist/audit.js.map +1 -1
  3. package/dist/background-indexer-DwJsyAB0.d.ts +373 -0
  4. package/dist/bash.js +5 -0
  5. package/dist/bash.js.map +1 -1
  6. package/dist/builtin.js +865 -327
  7. package/dist/builtin.js.map +1 -1
  8. package/dist/codebase-index/index.d.ts +53 -2
  9. package/dist/codebase-index/index.js +854 -364
  10. package/dist/codebase-index/index.js.map +1 -1
  11. package/dist/codebase-index/worker.d.ts +2 -0
  12. package/dist/codebase-index/worker.js +2321 -0
  13. package/dist/codebase-index/worker.js.map +1 -0
  14. package/dist/diff.js +2 -1
  15. package/dist/diff.js.map +1 -1
  16. package/dist/exec.js +1 -0
  17. package/dist/exec.js.map +1 -1
  18. package/dist/format.js +1 -0
  19. package/dist/format.js.map +1 -1
  20. package/dist/git.js +2 -1
  21. package/dist/git.js.map +1 -1
  22. package/dist/grep.js +2 -2
  23. package/dist/grep.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +886 -386
  26. package/dist/index.js.map +1 -1
  27. package/dist/install.js +1 -0
  28. package/dist/install.js.map +1 -1
  29. package/dist/lint.js +1 -0
  30. package/dist/lint.js.map +1 -1
  31. package/dist/logs.js +1 -1
  32. package/dist/logs.js.map +1 -1
  33. package/dist/outdated.js +1 -1
  34. package/dist/outdated.js.map +1 -1
  35. package/dist/pack.js +865 -327
  36. package/dist/pack.js.map +1 -1
  37. package/dist/patch.js +1 -1
  38. package/dist/patch.js.map +1 -1
  39. package/dist/replace.js +3 -2
  40. package/dist/replace.js.map +1 -1
  41. package/dist/test.js +1 -0
  42. package/dist/test.js.map +1 -1
  43. package/dist/typecheck.js +1 -0
  44. package/dist/typecheck.js.map +1 -1
  45. package/package.json +2 -2
  46. package/dist/background-indexer-CtbgPExj.d.ts +0 -228
package/dist/builtin.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, expectDefined, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths } from '@wrongstack/core';
3
+ import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
5
  import { statSync, writeFileSync, mkdirSync } from 'node:fs';
6
6
  import * as path2 from 'node:path';
7
7
  import { resolve, sep, dirname, join } from 'node:path';
8
- import * as fs13 from 'node:fs/promises';
8
+ import * as fs14 from 'node:fs/promises';
9
9
  import * as os from 'node:os';
10
10
  import { createRequire } from 'node:module';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { Worker } from 'node:worker_threads';
11
13
  import * as ts from 'typescript';
12
14
  import * as dns from 'node:dns/promises';
13
15
  import * as net from 'node:net';
@@ -43,7 +45,7 @@ async function* spawnStream(opts) {
43
45
  const maxQueue = opts.maxQueueSize ?? 500;
44
46
  let stdout = "";
45
47
  let stderr = "";
46
- let pending = "";
48
+ let pending2 = "";
47
49
  let error;
48
50
  const cmd = resolveWin32Command(opts.cmd);
49
51
  const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
@@ -52,6 +54,7 @@ async function* spawnStream(opts) {
52
54
  signal: opts.signal,
53
55
  env: buildChildEnv(),
54
56
  stdio: ["ignore", "pipe", "pipe"],
57
+ windowsHide: true,
55
58
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
56
59
  });
57
60
  const queue = [];
@@ -121,14 +124,14 @@ async function* spawnStream(opts) {
121
124
  exitCode = 1;
122
125
  continue;
123
126
  }
124
- pending += chunk.data;
125
- if (pending.length >= flushAt) {
126
- yield { type: "partial_output", text: pending };
127
- pending = "";
127
+ pending2 += chunk.data;
128
+ if (pending2.length >= flushAt) {
129
+ yield { type: "partial_output", text: pending2 };
130
+ pending2 = "";
128
131
  }
129
132
  }
130
- if (pending.length > 0) {
131
- yield { type: "partial_output", text: pending };
133
+ if (pending2.length > 0) {
134
+ yield { type: "partial_output", text: pending2 };
132
135
  }
133
136
  return {
134
137
  stdout,
@@ -168,12 +171,12 @@ function safeResolve(input, ctx) {
168
171
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
169
172
  }
170
173
  async function assertRealInsideRoot(absPath, ctx) {
171
- const realRoot = await fs13.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
174
+ const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
172
175
  let probe = absPath;
173
176
  for (; ; ) {
174
177
  let real;
175
178
  try {
176
- real = await fs13.realpath(probe);
179
+ real = await fs14.realpath(probe);
177
180
  } catch (err) {
178
181
  if (err.code === "ENOENT") {
179
182
  const parent = path2.dirname(probe);
@@ -865,6 +868,10 @@ var bashTool = {
865
868
  env,
866
869
  stdio: ["ignore", "pipe", "pipe"],
867
870
  detached: true,
871
+ // Detached console children on Windows allocate their own VISIBLE
872
+ // console window (one per background command — test suites flash
873
+ // dozens). CREATE_NO_WINDOW suppresses it; no-op elsewhere.
874
+ windowsHide: true,
868
875
  signal: opts.signal
869
876
  });
870
877
  const pid2 = child2.pid;
@@ -917,6 +924,7 @@ var bashTool = {
917
924
  env,
918
925
  stdio: ["ignore", "pipe", "pipe"],
919
926
  detached,
927
+ windowsHide: true,
920
928
  ...isWin ? {} : { signal: opts.signal }
921
929
  });
922
930
  const pid = child.pid;
@@ -931,7 +939,7 @@ var bashTool = {
931
939
  });
932
940
  }
933
941
  let buf = "";
934
- let pending = "";
942
+ let pending2 = "";
935
943
  let timedOut = false;
936
944
  const timers = [];
937
945
  function killWithTimeout(child2, timeoutMs2) {
@@ -1013,9 +1021,9 @@ var bashTool = {
1013
1021
  });
1014
1022
  let lastFlush = Date.now();
1015
1023
  const flush = () => {
1016
- if (pending.length === 0) return null;
1017
- const text = pending;
1018
- pending = "";
1024
+ if (pending2.length === 0) return null;
1025
+ const text = pending2;
1026
+ pending2 = "";
1019
1027
  lastFlush = Date.now();
1020
1028
  return text;
1021
1029
  };
@@ -1039,7 +1047,7 @@ var bashTool = {
1039
1047
  if (buf.length < MAX_OUTPUT) {
1040
1048
  buf += text.slice(0, MAX_OUTPUT - buf.length);
1041
1049
  }
1042
- pending += text;
1050
+ pending2 += text;
1043
1051
  push({ kind: "data", text });
1044
1052
  pauseIfFlooded();
1045
1053
  };
@@ -1077,7 +1085,7 @@ var bashTool = {
1077
1085
  return;
1078
1086
  }
1079
1087
  const now = Date.now();
1080
- if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
1088
+ if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
1081
1089
  const text = flush();
1082
1090
  if (text) yield { type: "partial_output", text };
1083
1091
  }
@@ -1201,8 +1209,88 @@ async function executeSingle(call, ctx, opts) {
1201
1209
  }
1202
1210
  }
1203
1211
 
1212
+ // src/codebase-index/circuit-breaker.ts
1213
+ var CircuitOpenError = class extends Error {
1214
+ name = "CircuitOpenError";
1215
+ };
1216
+ var IndexTimeoutError = class extends Error {
1217
+ name = "IndexTimeoutError";
1218
+ };
1219
+ var LockError = class extends Error {
1220
+ name = "LockError";
1221
+ };
1222
+ var IndexCircuitBreaker = class {
1223
+ failureThreshold;
1224
+ cooldownMs;
1225
+ now;
1226
+ state = "closed";
1227
+ consecutiveFailures = 0;
1228
+ openedAt = 0;
1229
+ lastFailure = null;
1230
+ probeInFlight = false;
1231
+ constructor(opts = {}) {
1232
+ this.failureThreshold = opts.failureThreshold ?? 3;
1233
+ this.cooldownMs = opts.cooldownMs ?? 6e4;
1234
+ this.now = opts.now ?? Date.now;
1235
+ }
1236
+ /**
1237
+ * True when a run may proceed. An open circuit transitions to half-open once
1238
+ * the cooldown has elapsed, admitting exactly one probe; further requests
1239
+ * are rejected until that probe settles via recordSuccess/recordFailure.
1240
+ */
1241
+ allowRequest() {
1242
+ if (this.state === "closed") return true;
1243
+ if (this.state === "open") {
1244
+ if (this.now() - this.openedAt < this.cooldownMs) return false;
1245
+ this.state = "half-open";
1246
+ this.probeInFlight = true;
1247
+ return true;
1248
+ }
1249
+ if (this.probeInFlight) return false;
1250
+ this.probeInFlight = true;
1251
+ return true;
1252
+ }
1253
+ recordSuccess() {
1254
+ this.state = "closed";
1255
+ this.consecutiveFailures = 0;
1256
+ this.lastFailure = null;
1257
+ this.probeInFlight = false;
1258
+ }
1259
+ recordFailure(err) {
1260
+ if (err instanceof LockError) {
1261
+ this.lastFailure = `[transient/lock] ${err.message}`;
1262
+ this.probeInFlight = false;
1263
+ return;
1264
+ }
1265
+ this.lastFailure = err instanceof Error ? err.message : String(err);
1266
+ this.probeInFlight = false;
1267
+ this.consecutiveFailures++;
1268
+ if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
1269
+ this.state = "open";
1270
+ this.openedAt = this.now();
1271
+ }
1272
+ }
1273
+ /** Force-close the circuit (manual recovery: `/codebase-reindex`). */
1274
+ reset() {
1275
+ this.state = "closed";
1276
+ this.consecutiveFailures = 0;
1277
+ this.lastFailure = null;
1278
+ this.probeInFlight = false;
1279
+ this.openedAt = 0;
1280
+ }
1281
+ snapshot() {
1282
+ return {
1283
+ state: this.state,
1284
+ consecutiveFailures: this.consecutiveFailures,
1285
+ lastFailure: this.lastFailure,
1286
+ cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
1287
+ };
1288
+ }
1289
+ };
1290
+ var indexCircuitBreaker = new IndexCircuitBreaker();
1291
+
1204
1292
  // src/codebase-index/schema.ts
1205
- var SCHEMA_VERSION = 1;
1293
+ var SCHEMA_VERSION = 2;
1206
1294
 
1207
1295
  // src/codebase-index/lsp-kind.ts
1208
1296
  function lspKindToInternalKind(k) {
@@ -1237,6 +1325,94 @@ function lspKindToInternalKind(k) {
1237
1325
  }
1238
1326
  }
1239
1327
 
1328
+ // src/codebase-index/bm25.ts
1329
+ var K1 = 1.5;
1330
+ var B = 0.75;
1331
+ function tokenise(text) {
1332
+ const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
1333
+ return sanitised.toLowerCase().split(" ").filter(Boolean);
1334
+ }
1335
+ function splitName(name) {
1336
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1337
+ }
1338
+ function buildIndexableText(name, signature, docComment) {
1339
+ return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
1340
+ }
1341
+ function buildBm25Index(docs) {
1342
+ const documents = docs.map((d) => {
1343
+ const tokens = tokenise(d.text);
1344
+ return { id: d.id, tokens, raw: d.text, len: tokens.length };
1345
+ });
1346
+ const df = {};
1347
+ for (const doc of documents) {
1348
+ const seen = /* @__PURE__ */ new Set();
1349
+ for (const t of doc.tokens) {
1350
+ if (!seen.has(t)) {
1351
+ df[t] = (df[t] ?? 0) + 1;
1352
+ seen.add(t);
1353
+ }
1354
+ }
1355
+ }
1356
+ const N = documents.length;
1357
+ const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
1358
+ const avgLen = N === 0 ? 0 : totalLen / N;
1359
+ return new Bm25Index(documents, df, N, avgLen);
1360
+ }
1361
+ var Bm25Index = class {
1362
+ constructor(documents, df, N, avgLen) {
1363
+ this.documents = documents;
1364
+ this.df = df;
1365
+ this.N = N;
1366
+ this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
1367
+ }
1368
+ documents;
1369
+ df;
1370
+ N;
1371
+ safeAvgLen;
1372
+ score(query2, filter) {
1373
+ const qTokens = tokenise(query2);
1374
+ if (qTokens.length === 0) return [];
1375
+ const results = [];
1376
+ for (const doc of this.documents) {
1377
+ if (filter && !filter(doc.id)) continue;
1378
+ let docScore = 0;
1379
+ for (const qTerm of qTokens) {
1380
+ let tf = 0;
1381
+ for (const t of doc.tokens) {
1382
+ if (t === qTerm) tf++;
1383
+ }
1384
+ if (tf === 0) continue;
1385
+ const dfVal = this.df[qTerm] ?? 0;
1386
+ if (dfVal === 0) continue;
1387
+ const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
1388
+ const lenRatio = B * (doc.len / this.safeAvgLen);
1389
+ const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
1390
+ docScore += idf * tfComponent;
1391
+ }
1392
+ if (docScore > 0) results.push({ id: doc.id, score: docScore });
1393
+ }
1394
+ return results;
1395
+ }
1396
+ getDoc(id) {
1397
+ return this.documents.find((d) => d.id === id);
1398
+ }
1399
+ extractSnippet(docId, queryTokens, radius = 40) {
1400
+ const doc = this.getDoc(docId);
1401
+ if (!doc) return "";
1402
+ for (const tok of queryTokens) {
1403
+ const idx = doc.raw.toLowerCase().indexOf(tok);
1404
+ if (idx !== -1) {
1405
+ const start = Math.max(0, idx - radius);
1406
+ const end = Math.min(doc.raw.length, idx + tok.length + radius);
1407
+ const excerpt = doc.raw.slice(start, end);
1408
+ const ellipsis = "\u2026";
1409
+ return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
1410
+ }
1411
+ }
1412
+ return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
1413
+ }
1414
+ };
1415
+
1240
1416
  // src/codebase-index/writer.ts
1241
1417
  var DB_FILE = "index.db";
1242
1418
  function resolveIndexDir(projectRoot, override) {
@@ -1272,15 +1448,79 @@ function loadDatabaseSync() {
1272
1448
  }
1273
1449
  return DatabaseSyncCtor;
1274
1450
  }
1451
+ var MAX_LOCK_RETRIES = 3;
1452
+ var LOCK_RETRY_BASE_DELAY_MS = 50;
1453
+ var LOCK_RETRY_MAX_DELAY_MS = 500;
1454
+ function isLockError(err) {
1455
+ if (!(err instanceof Error)) return false;
1456
+ const e = err;
1457
+ const code = e.code ?? e.sqliteCode;
1458
+ if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
1459
+ if (typeof code === "number" && (code === 5 || code === 6)) return true;
1460
+ if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
1461
+ return false;
1462
+ }
1463
+ function sleepSync(ms) {
1464
+ try {
1465
+ const sab = new SharedArrayBuffer(4);
1466
+ const view = new Int32Array(sab);
1467
+ Atomics.wait(view, 0, 0, ms);
1468
+ } catch {
1469
+ }
1470
+ }
1275
1471
  var IndexStore = class {
1276
1472
  db;
1277
1473
  /** Absolute path to this project's index directory. */
1278
1474
  indexDir;
1475
+ /**
1476
+ * True when the SQLite build provides FTS5 (Node's bundled SQLite does).
1477
+ * When false, ranked search falls back to the LIKE + in-process BM25 path.
1478
+ */
1479
+ ftsAvailable = false;
1480
+ /**
1481
+ * Execute a SQLite write operation with automatic retry on lock conflicts.
1482
+ *
1483
+ * When another wstack process is holding the write lock the statement first
1484
+ * waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
1485
+ * that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
1486
+ * giving the competing writer time to finish and release the lock.
1487
+ *
1488
+ * @param fn The write operation to execute. Can return a value which is
1489
+ * returned to the caller on success.
1490
+ * @throws {@link LockError} when all retries are exhausted on a lock conflict
1491
+ * (non-lock errors always propagate on the first attempt).
1492
+ */
1493
+ runWithRetry(fn) {
1494
+ let lastError;
1495
+ for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
1496
+ try {
1497
+ return fn();
1498
+ } catch (err) {
1499
+ lastError = err;
1500
+ if (!isLockError(err)) throw err;
1501
+ if (attempt === MAX_LOCK_RETRIES) {
1502
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
1503
+ throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
1504
+ }
1505
+ const delay = Math.min(
1506
+ LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
1507
+ LOCK_RETRY_MAX_DELAY_MS
1508
+ );
1509
+ sleepSync(delay);
1510
+ }
1511
+ }
1512
+ throw lastError;
1513
+ }
1279
1514
  constructor(projectRoot, opts = {}) {
1280
1515
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
1281
1516
  fs.mkdirSync(this.indexDir, { recursive: true });
1282
1517
  const Database = loadDatabaseSync();
1283
1518
  this.db = new Database(path2.join(this.indexDir, DB_FILE));
1519
+ try {
1520
+ this.db.exec("PRAGMA journal_mode = WAL");
1521
+ this.db.exec("PRAGMA busy_timeout = 5000");
1522
+ } catch {
1523
+ }
1284
1524
  this.initSchema();
1285
1525
  }
1286
1526
  initSchema() {
@@ -1289,6 +1529,21 @@ var IndexStore = class {
1289
1529
  key TEXT PRIMARY KEY,
1290
1530
  value TEXT NOT NULL
1291
1531
  );
1532
+ `);
1533
+ const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
1534
+ const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
1535
+ if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
1536
+ this.db.exec(`
1537
+ DROP TABLE IF EXISTS symbols;
1538
+ DROP TABLE IF EXISTS files;
1539
+ DROP TABLE IF EXISTS refs;
1540
+ `);
1541
+ this.db.exec("DROP TABLE IF EXISTS symbols_fts");
1542
+ this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
1543
+ } else if (storedVersion === null) {
1544
+ this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
1545
+ }
1546
+ this.db.exec(`
1292
1547
  CREATE TABLE IF NOT EXISTS files (
1293
1548
  file TEXT PRIMARY KEY,
1294
1549
  lang TEXT NOT NULL,
@@ -1329,53 +1584,76 @@ var IndexStore = class {
1329
1584
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
1330
1585
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
1331
1586
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
1332
- const versionRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
1333
- if (!versionRows.length) {
1334
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
1587
+ try {
1588
+ this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
1589
+ this.ftsAvailable = true;
1590
+ } catch {
1591
+ this.ftsAvailable = false;
1335
1592
  }
1336
1593
  }
1337
1594
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
1338
1595
  insertSymbols(symbols, nextId) {
1339
- const stmt = this.db.prepare(
1340
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
1341
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1342
- );
1343
- let id = nextId;
1344
- for (const s of symbols) {
1345
- stmt.run(
1346
- id++,
1347
- s.lang,
1348
- s.kind,
1349
- s.name,
1350
- s.file,
1351
- s.line,
1352
- s.col,
1353
- s.signature,
1354
- s.docComment,
1355
- s.scope,
1356
- s.text,
1357
- s.file
1596
+ return this.runWithRetry(() => {
1597
+ const stmt = this.db.prepare(
1598
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
1599
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1358
1600
  );
1359
- }
1360
- return id;
1601
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
1602
+ let id = nextId;
1603
+ for (const s of symbols) {
1604
+ stmt.run(
1605
+ id,
1606
+ s.lang,
1607
+ s.kind,
1608
+ s.name,
1609
+ s.file,
1610
+ s.line,
1611
+ s.col,
1612
+ s.signature,
1613
+ s.docComment,
1614
+ s.scope,
1615
+ s.text,
1616
+ s.file
1617
+ );
1618
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
1619
+ id++;
1620
+ }
1621
+ return id;
1622
+ });
1361
1623
  }
1362
1624
  deleteSymbolsForFile(file) {
1363
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
1625
+ this.runWithRetry(() => {
1626
+ if (this.ftsAvailable) {
1627
+ this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
1628
+ }
1629
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
1630
+ });
1364
1631
  }
1632
+ /**
1633
+ * Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
1634
+ * when a source file disappears between index runs — previously this only
1635
+ * dropped the `files` row, leaving its symbols orphaned but still searchable.
1636
+ */
1365
1637
  deleteFile(file) {
1366
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
1638
+ this.runWithRetry(() => {
1639
+ this.deleteRefsForFile(file);
1640
+ this.deleteSymbolsForFile(file);
1641
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
1642
+ });
1367
1643
  }
1368
1644
  // ─── File metadata ──────────────────────────────────────────────────────────
1369
1645
  upsertFile(meta) {
1370
- this.db.prepare(
1371
- `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
1372
- VALUES (?, ?, ?, ?, ?)
1373
- ON CONFLICT(file) DO UPDATE SET
1374
- lang = excluded.lang,
1375
- mtime_ms = excluded.mtime_ms,
1376
- symbol_count = excluded.symbol_count,
1377
- last_indexed = excluded.last_indexed`
1378
- ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
1646
+ this.runWithRetry(() => {
1647
+ this.db.prepare(
1648
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
1649
+ VALUES (?, ?, ?, ?, ?)
1650
+ ON CONFLICT(file) DO UPDATE SET
1651
+ lang = excluded.lang,
1652
+ mtime_ms = excluded.mtime_ms,
1653
+ symbol_count = excluded.symbol_count,
1654
+ last_indexed = excluded.last_indexed`
1655
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
1656
+ });
1379
1657
  }
1380
1658
  getFileMeta(file) {
1381
1659
  const rows = this.db.prepare(
@@ -1442,6 +1720,94 @@ var IndexStore = class {
1442
1720
  lspKind: filter?.lspKind
1443
1721
  }));
1444
1722
  }
1723
+ /**
1724
+ * Ranked search — the one-stop query the codebase-search tool and plug-lsp
1725
+ * use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
1726
+ * `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
1727
+ * legacy LIKE scan + in-process BM25 (identical semantics, slower).
1728
+ *
1729
+ * Tokens are matched as prefixes (`"tok"*`), mirroring the old
1730
+ * `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
1731
+ * "users", camelCase-split text makes "complex" find "complexOperation").
1732
+ */
1733
+ searchRanked(query2, filter, limit) {
1734
+ const tokens = tokenise(query2);
1735
+ if (tokens.length === 0 || !this.ftsAvailable) {
1736
+ return this.searchRankedFallback(query2, filter, limit);
1737
+ }
1738
+ let effectiveKind = filter?.kind;
1739
+ if (filter?.lspKind !== void 0) {
1740
+ const mapped = lspKindToInternalKind(filter.lspKind);
1741
+ if (mapped === null) return { results: [], total: 0 };
1742
+ effectiveKind = mapped;
1743
+ }
1744
+ const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
1745
+ const conditions = ["symbols_fts MATCH ?"];
1746
+ const values = [match];
1747
+ if (effectiveKind) {
1748
+ conditions.push("s.kind = ?");
1749
+ values.push(effectiveKind);
1750
+ }
1751
+ if (filter?.lang) {
1752
+ conditions.push("s.lang = ?");
1753
+ values.push(filter.lang);
1754
+ }
1755
+ if (filter?.file) {
1756
+ conditions.push("s.file LIKE ?");
1757
+ values.push(`%${filter.file}%`);
1758
+ }
1759
+ const where = conditions.join(" AND ");
1760
+ const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
1761
+ const total = countRows[0] ? Number(countRows[0].n) : 0;
1762
+ if (total === 0) return { results: [], total: 0 };
1763
+ const rows = this.db.prepare(
1764
+ `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
1765
+ -bm25(symbols_fts) AS score,
1766
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
1767
+ FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
1768
+ WHERE ${where}
1769
+ ORDER BY bm25(symbols_fts)
1770
+ LIMIT ?`
1771
+ ).all(...values, limit);
1772
+ return {
1773
+ results: rows.map((r) => ({
1774
+ id: r.id,
1775
+ lang: r.lang,
1776
+ kind: r.kind,
1777
+ name: r.name,
1778
+ file: r.file,
1779
+ line: r.line,
1780
+ col: r.col,
1781
+ signature: r.signature,
1782
+ docComment: r.doc_comment,
1783
+ // bm25() is negative-is-better; negate so callers keep "higher is
1784
+ // better" and clamp so a match never reports a zero score.
1785
+ score: Math.max(1e-4, r.score),
1786
+ snippet: r.snippet,
1787
+ lspKind: filter?.lspKind
1788
+ })),
1789
+ total
1790
+ };
1791
+ }
1792
+ /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
1793
+ searchRankedFallback(query2, filter, limit) {
1794
+ const candidates = this.search(query2, filter);
1795
+ if (candidates.length === 0) return { results: [], total: 0 };
1796
+ if (!query2.trim()) {
1797
+ return { results: candidates.slice(0, limit), total: candidates.length };
1798
+ }
1799
+ const bm25 = buildBm25Index(
1800
+ candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
1801
+ );
1802
+ const scored = bm25.score(query2, (id) => candidates.some((c) => c.id === id));
1803
+ scored.sort((a, b) => b.score - a.score);
1804
+ const qTokens = tokenise(query2);
1805
+ const results = scored.slice(0, limit).map(({ id, score }) => {
1806
+ const c = expectDefined(candidates.find((cand) => cand.id === id));
1807
+ return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
1808
+ });
1809
+ return { results, total: candidates.length };
1810
+ }
1445
1811
  getAllIndexable() {
1446
1812
  return this.db.prepare("SELECT id, text FROM symbols").all().map(
1447
1813
  ({ id, text }) => ({ id, text })
@@ -1491,14 +1857,19 @@ var IndexStore = class {
1491
1857
  };
1492
1858
  }
1493
1859
  setLastIndexed(ts2) {
1494
- this.db.prepare(
1495
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
1496
- ).run(String(ts2));
1860
+ this.runWithRetry(() => {
1861
+ this.db.prepare(
1862
+ "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
1863
+ ).run(String(ts2));
1864
+ });
1497
1865
  }
1498
1866
  clearAll() {
1499
- this.db.exec("DELETE FROM symbols");
1500
- this.db.exec("DELETE FROM files");
1501
- this.db.exec("DELETE FROM refs");
1867
+ this.runWithRetry(() => {
1868
+ this.db.exec("DELETE FROM symbols");
1869
+ this.db.exec("DELETE FROM files");
1870
+ this.db.exec("DELETE FROM refs");
1871
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
1872
+ });
1502
1873
  }
1503
1874
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
1504
1875
  /**
@@ -1506,46 +1877,52 @@ var IndexStore = class {
1506
1877
  * Replaces any existing refs from the same source (idempotent on re-index).
1507
1878
  */
1508
1879
  insertRefs(fromId, refs) {
1509
- this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
1510
- if (refs.length === 0) return;
1511
- const stmt = this.db.prepare(
1512
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
1513
- VALUES (?, ?, ?, ?, ?)`
1514
- );
1515
- for (const ref of refs) {
1516
- stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
1517
- }
1880
+ this.runWithRetry(() => {
1881
+ this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
1882
+ if (refs.length === 0) return;
1883
+ const stmt = this.db.prepare(
1884
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
1885
+ VALUES (?, ?, ?, ?, ?)`
1886
+ );
1887
+ for (const ref of refs) {
1888
+ stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
1889
+ }
1890
+ });
1518
1891
  }
1519
1892
  /**
1520
1893
  * Delete all refs whose source symbols are in a given file.
1521
1894
  * Used when re-indexing a file to clear stale refs.
1522
1895
  */
1523
1896
  deleteRefsForFile(file) {
1524
- const ids = this.db.prepare(
1525
- "SELECT id FROM symbols WHERE file = ?"
1526
- ).all(file);
1527
- if (!ids.length) return;
1528
- const placeholders = ids.map(() => "?").join(",");
1529
- this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
1897
+ this.runWithRetry(() => {
1898
+ const ids = this.db.prepare(
1899
+ "SELECT id FROM symbols WHERE file = ?"
1900
+ ).all(file);
1901
+ if (!ids.length) return;
1902
+ const placeholders = ids.map(() => "?").join(",");
1903
+ this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
1904
+ });
1530
1905
  }
1531
1906
  /**
1532
1907
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
1533
1908
  * Call this after all symbols have been inserted to fill in cross-references.
1534
1909
  */
1535
1910
  resolveRefs() {
1536
- const unresolved = this.db.prepare(
1537
- "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
1538
- ).all();
1539
- let resolved = 0;
1540
- for (const row of unresolved) {
1541
- const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
1542
- const first = target[0];
1543
- if (first) {
1544
- this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
1545
- resolved++;
1911
+ return this.runWithRetry(() => {
1912
+ const unresolved = this.db.prepare(
1913
+ "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
1914
+ ).all();
1915
+ let resolved = 0;
1916
+ for (const row of unresolved) {
1917
+ const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
1918
+ const first = target[0];
1919
+ if (first) {
1920
+ this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
1921
+ resolved++;
1922
+ }
1546
1923
  }
1547
- }
1548
- return resolved;
1924
+ return resolved;
1925
+ });
1549
1926
  }
1550
1927
  /**
1551
1928
  * Find all references TO a given symbol (who calls / uses this symbol?).
@@ -2306,7 +2683,7 @@ function parseSymbols4(opts) {
2306
2683
  }
2307
2684
  function checkNativeParser() {
2308
2685
  try {
2309
- execFileSync("rustc", ["--version"], { stdio: "pipe" });
2686
+ execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
2310
2687
  const toolsDir = path2.join(process.cwd(), "tools");
2311
2688
  try {
2312
2689
  execFileSync(
@@ -2319,7 +2696,7 @@ function checkNativeParser() {
2319
2696
  "--manifest-path",
2320
2697
  path2.join(toolsDir, "Cargo.toml")
2321
2698
  ],
2322
- { stdio: "pipe" }
2699
+ { stdio: "pipe", windowsHide: true }
2323
2700
  );
2324
2701
  return true;
2325
2702
  } catch {
@@ -2342,7 +2719,8 @@ function tryNativeParse(file, content) {
2342
2719
  cwd: process.cwd(),
2343
2720
  encoding: "utf8",
2344
2721
  timeout: 15e3,
2345
- stdio: ["pipe", "pipe", "pipe"]
2722
+ stdio: ["pipe", "pipe", "pipe"],
2723
+ windowsHide: true
2346
2724
  }
2347
2725
  );
2348
2726
  if (result.status === 0 && result.stdout) {
@@ -2756,10 +3134,6 @@ function isScalar(value) {
2756
3134
  if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
2757
3135
  return false;
2758
3136
  }
2759
- function truncate(s, max) {
2760
- if (s.length <= max) return s;
2761
- return s.slice(0, max) + "...";
2762
- }
2763
3137
  function makeSymbol2(opts) {
2764
3138
  return {
2765
3139
  id: 0,
@@ -2819,43 +3193,13 @@ function compileGitignore(lines) {
2819
3193
  async function loadGitignoreMatcher(projectRoot) {
2820
3194
  let lines = [];
2821
3195
  try {
2822
- const raw = await fs13.readFile(path2.join(projectRoot, ".gitignore"), "utf8");
3196
+ const raw = await fs14.readFile(path2.join(projectRoot, ".gitignore"), "utf8");
2823
3197
  lines = raw.split("\n");
2824
3198
  } catch {
2825
3199
  }
2826
3200
  return compileGitignore(lines);
2827
3201
  }
2828
3202
 
2829
- // src/codebase-index/background-indexer.ts
2830
- var _ready = false;
2831
- var _indexing = false;
2832
- var _currentFile = 0;
2833
- var _totalFiles = 0;
2834
- var _lastError = null;
2835
- function setIndexReady() {
2836
- _ready = true;
2837
- }
2838
- function getIndexState() {
2839
- return {
2840
- ready: _ready,
2841
- indexing: _indexing,
2842
- currentFile: _currentFile,
2843
- totalFiles: _totalFiles,
2844
- lastError: _lastError
2845
- };
2846
- }
2847
- var _listeners = [];
2848
- function emitState() {
2849
- const state = getIndexState();
2850
- for (const l of _listeners) l(state);
2851
- }
2852
- function _setIndexProgress(current, total) {
2853
- _currentFile = current;
2854
- _totalFiles = total;
2855
- emitState();
2856
- }
2857
- Promise.resolve();
2858
-
2859
3203
  // src/codebase-index/indexer.ts
2860
3204
  var YIELD_EVERY_N = 50;
2861
3205
  function yieldEventLoop() {
@@ -2906,7 +3250,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2906
3250
  }
2907
3251
  let entries;
2908
3252
  try {
2909
- entries = await fs13.readdir(dir, { withFileTypes: true });
3253
+ entries = await fs14.readdir(dir, { withFileTypes: true });
2910
3254
  } catch {
2911
3255
  return;
2912
3256
  }
@@ -2993,7 +3337,7 @@ async function runIndexerWithStore(store, opts) {
2993
3337
  }
2994
3338
  for (let fi = 0; fi < files.length; fi++) {
2995
3339
  const file = expectDefined(files[fi]);
2996
- _setIndexProgress(fi + 1, files.length);
3340
+ opts.onProgress?.(fi + 1, files.length);
2997
3341
  if (fi > 0 && fi % YIELD_EVERY_N === 0) {
2998
3342
  await yieldEventLoop();
2999
3343
  throwIfAborted(signal);
@@ -3001,7 +3345,7 @@ async function runIndexerWithStore(store, opts) {
3001
3345
  let stat10;
3002
3346
  try {
3003
3347
  const statOpts = signal ? { signal } : {};
3004
- stat10 = await fs13.stat(file, statOpts);
3348
+ stat10 = await fs14.stat(file, statOpts);
3005
3349
  } catch (e) {
3006
3350
  if (isAbortError(e)) throw e;
3007
3351
  store.deleteFile(file);
@@ -3021,7 +3365,7 @@ async function runIndexerWithStore(store, opts) {
3021
3365
  store.deleteSymbolsForFile(file);
3022
3366
  let content;
3023
3367
  try {
3024
- content = await fs13.readFile(file, { encoding: "utf8", signal });
3368
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
3025
3369
  } catch (e) {
3026
3370
  if (isAbortError(e)) throw e;
3027
3371
  errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
@@ -3072,7 +3416,7 @@ async function runIndexerWithStore(store, opts) {
3072
3416
  }
3073
3417
  for (const [file_] of existingMeta) {
3074
3418
  try {
3075
- await fs13.stat(file_);
3419
+ await fs14.stat(file_);
3076
3420
  } catch {
3077
3421
  store.deleteFile(file_);
3078
3422
  }
@@ -3088,6 +3432,290 @@ async function runIndexerWithStore(store, opts) {
3088
3432
  };
3089
3433
  }
3090
3434
 
3435
+ // src/codebase-index/index-service.ts
3436
+ function stubCtx(projectRoot) {
3437
+ return {
3438
+ projectRoot,
3439
+ cwd: projectRoot,
3440
+ messages: [],
3441
+ todos: [],
3442
+ readFiles: /* @__PURE__ */ new Set(),
3443
+ fileMtimes: /* @__PURE__ */ new Map()
3444
+ };
3445
+ }
3446
+ async function indexService(args, hooks = {}) {
3447
+ return runIndexer(stubCtx(args.projectRoot), {
3448
+ projectRoot: args.projectRoot,
3449
+ indexDir: args.indexDir,
3450
+ files: args.files,
3451
+ force: args.force,
3452
+ langs: args.langs,
3453
+ ignore: args.ignore,
3454
+ signal: hooks.signal,
3455
+ onProgress: hooks.onProgress
3456
+ });
3457
+ }
3458
+ function searchService(args) {
3459
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3460
+ try {
3461
+ return store.searchRanked(
3462
+ args.query,
3463
+ {
3464
+ kind: args.kind,
3465
+ lang: args.lang,
3466
+ file: args.file,
3467
+ lspKind: args.lspKind
3468
+ },
3469
+ args.limit
3470
+ );
3471
+ } finally {
3472
+ store.close();
3473
+ }
3474
+ }
3475
+ function statsService(args) {
3476
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3477
+ try {
3478
+ return store.getStats();
3479
+ } finally {
3480
+ store.close();
3481
+ }
3482
+ }
3483
+
3484
+ // src/codebase-index/background-indexer.ts
3485
+ var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
3486
+ var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
3487
+ var _ready = false;
3488
+ var _indexing = false;
3489
+ var _currentFile = 0;
3490
+ var _totalFiles = 0;
3491
+ var _lastError = null;
3492
+ function isIndexing() {
3493
+ return _indexing;
3494
+ }
3495
+ function getIndexState() {
3496
+ return {
3497
+ ready: _ready,
3498
+ indexing: _indexing,
3499
+ currentFile: _currentFile,
3500
+ totalFiles: _totalFiles,
3501
+ lastError: _lastError,
3502
+ circuit: indexCircuitBreaker.snapshot()
3503
+ };
3504
+ }
3505
+ var _listeners = [];
3506
+ function emitState() {
3507
+ const state = getIndexState();
3508
+ for (const l of _listeners) l(state);
3509
+ }
3510
+ function setIndexProgress(current, total) {
3511
+ _currentFile = current;
3512
+ _totalFiles = total;
3513
+ emitState();
3514
+ }
3515
+ var worker = null;
3516
+ var workerUnavailable = false;
3517
+ var nextRpcId = 1;
3518
+ var pending = /* @__PURE__ */ new Map();
3519
+ function resolveWorkerUrl() {
3520
+ if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
3521
+ for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
3522
+ try {
3523
+ const url = new URL(rel, import.meta.url);
3524
+ if (url.protocol === "file:" && fs.existsSync(fileURLToPath(url))) return url;
3525
+ } catch {
3526
+ }
3527
+ }
3528
+ return null;
3529
+ }
3530
+ function failAllPending(err) {
3531
+ const entries = [...pending.values()];
3532
+ pending.clear();
3533
+ for (const p of entries) p.reject(err);
3534
+ }
3535
+ function ensureWorker() {
3536
+ if (worker) return worker;
3537
+ if (workerUnavailable) return null;
3538
+ const url = resolveWorkerUrl();
3539
+ if (!url) {
3540
+ workerUnavailable = true;
3541
+ return null;
3542
+ }
3543
+ try {
3544
+ const w = new Worker(url, { name: "wstack-codebase-index" });
3545
+ w.unref();
3546
+ w.on("message", (msg) => {
3547
+ if (msg.type === "progress") {
3548
+ pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
3549
+ return;
3550
+ }
3551
+ const entry = pending.get(msg.id);
3552
+ if (!entry) return;
3553
+ pending.delete(msg.id);
3554
+ if (msg.ok) entry.resolve(msg.result);
3555
+ else entry.reject(new Error(msg.error));
3556
+ });
3557
+ w.on("error", (err) => {
3558
+ worker = null;
3559
+ failAllPending(err);
3560
+ });
3561
+ w.on("exit", () => {
3562
+ if (worker === w) worker = null;
3563
+ failAllPending(new Error("codebase-index worker exited"));
3564
+ });
3565
+ worker = w;
3566
+ return w;
3567
+ } catch {
3568
+ workerUnavailable = true;
3569
+ return null;
3570
+ }
3571
+ }
3572
+ function terminateWorker(reason) {
3573
+ const w = worker;
3574
+ worker = null;
3575
+ failAllPending(reason);
3576
+ if (w) void w.terminate().catch(() => {
3577
+ });
3578
+ }
3579
+ function callIndexOp(op, args, opts) {
3580
+ const w = ensureWorker();
3581
+ if (!w) return callInline(op, args, opts);
3582
+ return new Promise((resolve7, reject) => {
3583
+ const id = nextRpcId++;
3584
+ const timer = setTimeout(() => {
3585
+ pending.delete(id);
3586
+ const err = new IndexTimeoutError(
3587
+ `Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
3588
+ );
3589
+ terminateWorker(err);
3590
+ reject(err);
3591
+ }, opts.timeoutMs);
3592
+ timer.unref?.();
3593
+ const onAbort = () => {
3594
+ w.postMessage({ type: "cancel", id });
3595
+ };
3596
+ if (opts.signal?.aborted) onAbort();
3597
+ else opts.signal?.addEventListener("abort", onAbort, { once: true });
3598
+ const cleanup = () => {
3599
+ clearTimeout(timer);
3600
+ opts.signal?.removeEventListener("abort", onAbort);
3601
+ };
3602
+ pending.set(id, {
3603
+ resolve: (v) => {
3604
+ cleanup();
3605
+ resolve7(v);
3606
+ },
3607
+ reject: (e) => {
3608
+ cleanup();
3609
+ reject(e);
3610
+ },
3611
+ onProgress: opts.onProgress
3612
+ });
3613
+ w.postMessage({ type: "request", id, op, args });
3614
+ });
3615
+ }
3616
+ async function callInline(op, args, opts) {
3617
+ const ac = new AbortController();
3618
+ const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
3619
+ if (opts.signal?.aborted) onOuterAbort();
3620
+ else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
3621
+ let timer;
3622
+ const watchdog = new Promise((_, reject) => {
3623
+ timer = setTimeout(() => {
3624
+ const err = new IndexTimeoutError(
3625
+ `Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
3626
+ );
3627
+ ac.abort(err);
3628
+ reject(err);
3629
+ }, opts.timeoutMs);
3630
+ timer.unref?.();
3631
+ });
3632
+ const job = async () => {
3633
+ switch (op) {
3634
+ case "index":
3635
+ return await indexService(args, {
3636
+ signal: ac.signal,
3637
+ onProgress: opts.onProgress
3638
+ });
3639
+ case "search":
3640
+ return searchService(args);
3641
+ case "stats":
3642
+ return statsService(args);
3643
+ default:
3644
+ throw new Error(`unknown index op: ${String(op)}`);
3645
+ }
3646
+ };
3647
+ try {
3648
+ return await Promise.race([job(), watchdog]);
3649
+ } finally {
3650
+ if (timer) clearTimeout(timer);
3651
+ opts.signal?.removeEventListener("abort", onOuterAbort);
3652
+ }
3653
+ }
3654
+ var chain = Promise.resolve();
3655
+ function withMutex(job) {
3656
+ const run = chain.then(job, job);
3657
+ chain = run.then(
3658
+ () => void 0,
3659
+ () => void 0
3660
+ );
3661
+ return run;
3662
+ }
3663
+ function circuitOpenError() {
3664
+ const c = indexCircuitBreaker.snapshot();
3665
+ return new CircuitOpenError(
3666
+ "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."
3667
+ );
3668
+ }
3669
+ async function runStartupIndex(opts) {
3670
+ if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
3671
+ _indexing = true;
3672
+ emitState();
3673
+ try {
3674
+ const result = await withMutex(() => {
3675
+ _currentFile = 0;
3676
+ _totalFiles = 0;
3677
+ _lastError = null;
3678
+ return callIndexOp(
3679
+ "index",
3680
+ {
3681
+ projectRoot: opts.projectRoot,
3682
+ indexDir: opts.indexDir,
3683
+ force: opts.force,
3684
+ langs: opts.langs
3685
+ },
3686
+ {
3687
+ timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
3688
+ signal: opts.signal,
3689
+ onProgress: setIndexProgress
3690
+ }
3691
+ );
3692
+ });
3693
+ _ready = true;
3694
+ indexCircuitBreaker.recordSuccess();
3695
+ return result;
3696
+ } catch (err) {
3697
+ _lastError = err instanceof Error ? err.message : String(err);
3698
+ _ready = true;
3699
+ if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
3700
+ throw err;
3701
+ } finally {
3702
+ _indexing = false;
3703
+ emitState();
3704
+ }
3705
+ }
3706
+ async function searchCodebaseIndex(args, opts = {}) {
3707
+ return callIndexOp("search", args, {
3708
+ timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
3709
+ signal: opts.signal
3710
+ });
3711
+ }
3712
+ async function codebaseIndexStats(args, opts = {}) {
3713
+ return callIndexOp("stats", args, {
3714
+ timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
3715
+ signal: opts.signal
3716
+ });
3717
+ }
3718
+
3091
3719
  // src/codebase-index/codebase-index-tool.ts
3092
3720
  var codebaseIndexTool = {
3093
3721
  name: "codebase-index",
@@ -3113,103 +3741,34 @@ var codebaseIndexTool = {
3113
3741
  }
3114
3742
  },
3115
3743
  async execute(input, ctx, execOpts) {
3116
- const result = await runIndexer(ctx, {
3744
+ if (isIndexing()) {
3745
+ return {
3746
+ filesIndexed: 0,
3747
+ symbolsIndexed: 0,
3748
+ langStats: {},
3749
+ durationMs: 0,
3750
+ errors: [],
3751
+ note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
3752
+ };
3753
+ }
3754
+ const circuit = indexCircuitBreaker.snapshot();
3755
+ if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
3756
+ return {
3757
+ filesIndexed: 0,
3758
+ symbolsIndexed: 0,
3759
+ langStats: {},
3760
+ durationMs: 0,
3761
+ errors: [],
3762
+ note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
3763
+ };
3764
+ }
3765
+ return await runStartupIndex({
3117
3766
  projectRoot: ctx.projectRoot,
3118
3767
  force: input.force ?? false,
3119
3768
  langs: input.langs,
3120
3769
  indexDir: codebaseIndexDirOverride(ctx),
3121
3770
  signal: execOpts?.signal
3122
3771
  });
3123
- setIndexReady();
3124
- return result;
3125
- }
3126
- };
3127
-
3128
- // src/codebase-index/bm25.ts
3129
- var K1 = 1.5;
3130
- var B = 0.75;
3131
- function tokenise(text) {
3132
- const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
3133
- return sanitised.toLowerCase().split(" ").filter(Boolean);
3134
- }
3135
- function splitName(name) {
3136
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
3137
- }
3138
- function buildIndexableText(name, signature, docComment) {
3139
- return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
3140
- }
3141
- function buildBm25Index(docs) {
3142
- const documents = docs.map((d) => {
3143
- const tokens = tokenise(d.text);
3144
- return { id: d.id, tokens, raw: d.text, len: tokens.length };
3145
- });
3146
- const df = {};
3147
- for (const doc of documents) {
3148
- const seen = /* @__PURE__ */ new Set();
3149
- for (const t of doc.tokens) {
3150
- if (!seen.has(t)) {
3151
- df[t] = (df[t] ?? 0) + 1;
3152
- seen.add(t);
3153
- }
3154
- }
3155
- }
3156
- const N = documents.length;
3157
- const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
3158
- const avgLen = N === 0 ? 0 : totalLen / N;
3159
- return new Bm25Index(documents, df, N, avgLen);
3160
- }
3161
- var Bm25Index = class {
3162
- constructor(documents, df, N, avgLen) {
3163
- this.documents = documents;
3164
- this.df = df;
3165
- this.N = N;
3166
- this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
3167
- }
3168
- documents;
3169
- df;
3170
- N;
3171
- safeAvgLen;
3172
- score(query2, filter) {
3173
- const qTokens = tokenise(query2);
3174
- if (qTokens.length === 0) return [];
3175
- const results = [];
3176
- for (const doc of this.documents) {
3177
- if (filter && !filter(doc.id)) continue;
3178
- let docScore = 0;
3179
- for (const qTerm of qTokens) {
3180
- let tf = 0;
3181
- for (const t of doc.tokens) {
3182
- if (t === qTerm) tf++;
3183
- }
3184
- if (tf === 0) continue;
3185
- const dfVal = this.df[qTerm] ?? 0;
3186
- if (dfVal === 0) continue;
3187
- const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
3188
- const lenRatio = B * (doc.len / this.safeAvgLen);
3189
- const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
3190
- docScore += idf * tfComponent;
3191
- }
3192
- if (docScore > 0) results.push({ id: doc.id, score: docScore });
3193
- }
3194
- return results;
3195
- }
3196
- getDoc(id) {
3197
- return this.documents.find((d) => d.id === id);
3198
- }
3199
- extractSnippet(docId, queryTokens, radius = 40) {
3200
- const doc = this.getDoc(docId);
3201
- if (!doc) return "";
3202
- for (const tok of queryTokens) {
3203
- const idx = doc.raw.toLowerCase().indexOf(tok);
3204
- if (idx !== -1) {
3205
- const start = Math.max(0, idx - radius);
3206
- const end = Math.min(doc.raw.length, idx + tok.length + radius);
3207
- const excerpt = doc.raw.slice(start, end);
3208
- const ellipsis = "\u2026";
3209
- return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
3210
- }
3211
- }
3212
- return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
3213
3772
  }
3214
3773
  };
3215
3774
 
@@ -3255,7 +3814,7 @@ var codebaseSearchTool = {
3255
3814
  },
3256
3815
  required: ["query"]
3257
3816
  },
3258
- async execute(input, ctx) {
3817
+ async execute(input, ctx, execOpts) {
3259
3818
  const state = getIndexState();
3260
3819
  if (!state.ready) {
3261
3820
  return {
@@ -3274,51 +3833,30 @@ var codebaseSearchTool = {
3274
3833
  };
3275
3834
  }
3276
3835
  if (state.lastError) {
3836
+ const circuit = state.circuit;
3837
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
3277
3838
  return {
3278
3839
  results: [],
3279
3840
  total: 0,
3280
3841
  query: input.query,
3281
- indexStatus: `Index build failed: ${state.lastError}. Try /codebase-reindex.`
3842
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
3282
3843
  };
3283
3844
  }
3284
- const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3285
- try {
3286
- const limit = Math.min(input.limit ?? 20, 100);
3287
- const candidates = store.search(input.query, {
3845
+ const limit = Math.min(input.limit ?? 20, 100);
3846
+ const { results, total } = await searchCodebaseIndex(
3847
+ {
3848
+ projectRoot: ctx.projectRoot,
3849
+ indexDir: codebaseIndexDirOverride(ctx),
3850
+ query: input.query,
3288
3851
  kind: input.kind,
3289
3852
  lang: input.lang,
3290
3853
  file: input.file,
3291
- lspKind: input.lspKind
3292
- });
3293
- if (candidates.length === 0) {
3294
- return { results: [], total: 0, query: input.query };
3295
- }
3296
- const indexable = candidates.map((c) => ({
3297
- id: c.id,
3298
- text: buildIndexableText(c.name, c.signature, c.docComment)
3299
- }));
3300
- const bm25 = buildBm25Index(indexable);
3301
- const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
3302
- scored.sort((a, b) => b.score - a.score);
3303
- const top = scored.slice(0, limit);
3304
- const qTokens = tokenise(input.query);
3305
- const results = top.map(({ id, score }) => {
3306
- const c = expectDefined(candidates.find((c2) => c2.id === id));
3307
- const snippet = bm25.extractSnippet(id, qTokens);
3308
- return {
3309
- ...c,
3310
- score,
3311
- snippet
3312
- };
3313
- });
3314
- return {
3315
- results,
3316
- total: candidates.length,
3317
- query: input.query
3318
- };
3319
- } finally {
3320
- store.close();
3321
- }
3854
+ lspKind: input.lspKind,
3855
+ limit
3856
+ },
3857
+ { signal: execOpts?.signal }
3858
+ );
3859
+ return { results, total, query: input.query };
3322
3860
  }
3323
3861
  };
3324
3862
 
@@ -3337,7 +3875,7 @@ var codebaseStatsTool = {
3337
3875
  properties: {},
3338
3876
  additionalProperties: false
3339
3877
  },
3340
- async execute(_input, ctx) {
3878
+ async execute(_input, ctx, execOpts) {
3341
3879
  const idxState = getIndexState();
3342
3880
  if (!idxState.ready) {
3343
3881
  return {
@@ -3352,34 +3890,30 @@ var codebaseStatsTool = {
3352
3890
  indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
3353
3891
  };
3354
3892
  }
3893
+ const stats = await codebaseIndexStats(
3894
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3895
+ { signal: execOpts?.signal }
3896
+ );
3355
3897
  if (idxState.indexing) {
3356
- const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3357
- try {
3358
- const stats = store2.getStats();
3359
- return {
3360
- ...stats,
3361
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
3362
- };
3363
- } finally {
3364
- store2.close();
3365
- }
3366
- }
3367
- const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3368
- try {
3369
- const stats = store.getStats();
3370
3898
  return {
3371
- totalSymbols: stats.totalSymbols,
3372
- totalFiles: stats.totalFiles,
3373
- byLang: stats.byLang,
3374
- byKind: stats.byKind,
3375
- lastIndexed: stats.lastIndexed,
3376
- sizeBytes: stats.sizeBytes,
3377
- indexPath: stats.indexPath,
3378
- version: stats.version
3899
+ ...stats,
3900
+ indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
3379
3901
  };
3380
- } finally {
3381
- store.close();
3382
3902
  }
3903
+ const circuit = idxState.circuit;
3904
+ return {
3905
+ totalSymbols: stats.totalSymbols,
3906
+ totalFiles: stats.totalFiles,
3907
+ byLang: stats.byLang,
3908
+ byKind: stats.byKind,
3909
+ lastIndexed: stats.lastIndexed,
3910
+ sizeBytes: stats.sizeBytes,
3911
+ indexPath: stats.indexPath,
3912
+ version: stats.version,
3913
+ ...circuit.state === "open" ? {
3914
+ indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
3915
+ } : {}
3916
+ };
3383
3917
  }
3384
3918
  };
3385
3919
  var diffTool = {
@@ -3481,7 +4015,8 @@ function runGit(args, cwd, signal) {
3481
4015
  cwd,
3482
4016
  signal,
3483
4017
  env: buildChildEnv(),
3484
- stdio: ["ignore", "pipe", "pipe"]
4018
+ stdio: ["ignore", "pipe", "pipe"],
4019
+ windowsHide: true
3485
4020
  });
3486
4021
  child.stdout?.on("data", (c) => {
3487
4022
  stdout += c.toString();
@@ -3507,9 +4042,9 @@ async function fileDiff(input, ctx, _signal) {
3507
4042
  const results = [];
3508
4043
  for (const file of files) {
3509
4044
  const absPath = safeResolve(file, ctx);
3510
- const stat10 = await fs13.stat(absPath).catch(() => null);
4045
+ const stat10 = await fs14.stat(absPath).catch(() => null);
3511
4046
  if (!stat10?.isFile()) continue;
3512
- const content = await fs13.readFile(absPath, "utf8");
4047
+ const content = await fs14.readFile(absPath, "utf8");
3513
4048
  const lines = content.split(/\r?\n/);
3514
4049
  results.push(formatWithLineNumbers(file, lines));
3515
4050
  }
@@ -3571,7 +4106,7 @@ var documentTool = {
3571
4106
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
3572
4107
  for (const absPath of fileList) {
3573
4108
  try {
3574
- const content = await fs13.readFile(absPath, "utf8");
4109
+ const content = await fs14.readFile(absPath, "utf8");
3575
4110
  filesProcessed++;
3576
4111
  const processed = processFile(
3577
4112
  content,
@@ -3607,7 +4142,7 @@ async function resolveFiles(filesInput, cwd) {
3607
4142
  for (const f of files) {
3608
4143
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
3609
4144
  try {
3610
- const stat10 = await fs13.stat(absPath);
4145
+ const stat10 = await fs14.stat(absPath);
3611
4146
  if (stat10.isFile()) resolved.push(absPath);
3612
4147
  } catch {
3613
4148
  }
@@ -3699,7 +4234,7 @@ var editTool = {
3699
4234
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3700
4235
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3701
4236
  const absPath = await safeResolveReal(input.path, ctx);
3702
- const stat10 = await fs13.stat(absPath).catch((err) => {
4237
+ const stat10 = await fs14.stat(absPath).catch((err) => {
3703
4238
  if (err.code === "ENOENT") {
3704
4239
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
3705
4240
  }
@@ -3709,8 +4244,8 @@ var editTool = {
3709
4244
  if (!ctx.hasRead(absPath)) {
3710
4245
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
3711
4246
  }
3712
- const original = await fs13.readFile(absPath, "utf8");
3713
- const updated = await fs13.stat(absPath);
4247
+ const original = await fs14.readFile(absPath, "utf8");
4248
+ const updated = await fs14.stat(absPath);
3714
4249
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
3715
4250
  const lastReadMtime = ctx.lastReadMtime(absPath);
3716
4251
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -3750,7 +4285,7 @@ var editTool = {
3750
4285
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
3751
4286
  const newFile = toStyle(newFileLf, style);
3752
4287
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
3753
- const written = await fs13.stat(absPath);
4288
+ const written = await fs14.stat(absPath);
3754
4289
  ctx.recordRead(absPath, written.mtimeMs);
3755
4290
  ctx.session.recordFileChange({
3756
4291
  path: absPath,
@@ -4006,6 +4541,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4006
4541
  cwd,
4007
4542
  env: buildChildEnv(sessionId),
4008
4543
  stdio: ["ignore", "pipe", "pipe"],
4544
+ windowsHide: true,
4009
4545
  ...isWin ? {} : { signal },
4010
4546
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
4011
4547
  });
@@ -4725,7 +5261,8 @@ function runGit2(args, cwd, signal) {
4725
5261
  cwd,
4726
5262
  signal,
4727
5263
  env: buildChildEnv(),
4728
- stdio: ["ignore", "pipe", "pipe"]
5264
+ stdio: ["ignore", "pipe", "pipe"],
5265
+ windowsHide: true
4729
5266
  });
4730
5267
  child.stdout?.on("data", (chunk) => {
4731
5268
  if (stdout.length < MAX_OUTPUT3) {
@@ -4801,7 +5338,7 @@ var globTool = {
4801
5338
  }
4802
5339
  let entries;
4803
5340
  try {
4804
- entries = await fs13.readdir(dir, { withFileTypes: true });
5341
+ entries = await fs14.readdir(dir, { withFileTypes: true });
4805
5342
  } catch {
4806
5343
  return;
4807
5344
  }
@@ -4817,7 +5354,7 @@ var globTool = {
4817
5354
  } else if (e.isFile()) {
4818
5355
  if (re.test(rel) || re.test(name)) {
4819
5356
  try {
4820
- const st = await fs13.stat(full);
5357
+ const st = await fs14.stat(full);
4821
5358
  results.push({ rel: full, mtime: st.mtimeMs });
4822
5359
  if (results.length >= limit) {
4823
5360
  truncated = true;
@@ -4836,7 +5373,7 @@ var globTool = {
4836
5373
  };
4837
5374
  async function readGitignore(dir) {
4838
5375
  try {
4839
- const raw = await fs13.readFile(path2.join(dir, ".gitignore"), "utf8");
5376
+ const raw = await fs14.readFile(path2.join(dir, ".gitignore"), "utf8");
4840
5377
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
4841
5378
  } catch {
4842
5379
  return [];
@@ -4970,7 +5507,7 @@ var grepTool = {
4970
5507
  async function detectRg(signal) {
4971
5508
  return new Promise((resolve7) => {
4972
5509
  try {
4973
- const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
5510
+ const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
4974
5511
  p.on("error", () => resolve7(false));
4975
5512
  p.on("close", (code) => resolve7(code === 0));
4976
5513
  } catch {
@@ -5000,7 +5537,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
5000
5537
  const FLUSH_AT = 16;
5001
5538
  const MAX_BUF_BYTES = 1e6;
5002
5539
  let bufOverflow = false;
5003
- const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
5540
+ const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
5004
5541
  const queue = [];
5005
5542
  let waiter;
5006
5543
  const wake = () => {
@@ -5120,7 +5657,7 @@ async function runNative(input, base, mode, limit, signal) {
5120
5657
  if (stopped || signal.aborted) return;
5121
5658
  let entries;
5122
5659
  try {
5123
- entries = await fs13.readdir(dir, { withFileTypes: true });
5660
+ entries = await fs14.readdir(dir, { withFileTypes: true });
5124
5661
  } catch {
5125
5662
  return;
5126
5663
  }
@@ -5135,9 +5672,9 @@ async function runNative(input, base, mode, limit, signal) {
5135
5672
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
5136
5673
  if (globRe) globRe.lastIndex = 0;
5137
5674
  try {
5138
- const stat10 = await fs13.stat(full);
5675
+ const stat10 = await fs14.stat(full);
5139
5676
  if (stat10.size > 1e6) continue;
5140
- const head = await fs13.readFile(full);
5677
+ const head = await fs14.readFile(full);
5141
5678
  if (isBinaryBuffer(head)) continue;
5142
5679
  const text = head.toString("utf8");
5143
5680
  const lines = text.split(/\r?\n/);
@@ -5346,7 +5883,7 @@ var jsonTool = {
5346
5883
  let raw;
5347
5884
  if (input.file) {
5348
5885
  try {
5349
- raw = await fs13.readFile(input.file, "utf8");
5886
+ raw = await fs14.readFile(input.file, "utf8");
5350
5887
  } catch {
5351
5888
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
5352
5889
  }
@@ -5625,7 +6162,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5625
6162
  clearTimeout(timer);
5626
6163
  resolve7(result);
5627
6164
  };
5628
- const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
6165
+ const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
5629
6166
  const timer = setTimeout(() => {
5630
6167
  child.kill("SIGTERM");
5631
6168
  finish(empty());
@@ -5771,7 +6308,7 @@ function runOutdated(manager, args, cwd, signal) {
5771
6308
  const MAX = 1e5;
5772
6309
  const resolved = resolveWin32Command(manager);
5773
6310
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5774
- const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
6311
+ const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
5775
6312
  child.stdout?.on("data", (c) => {
5776
6313
  if (stdout.length < MAX) stdout += c.toString();
5777
6314
  });
@@ -5867,12 +6404,12 @@ var patchTool = {
5867
6404
  };
5868
6405
  }
5869
6406
  }
5870
- const tmpDir = await fs13.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
6407
+ const tmpDir = await fs14.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
5871
6408
  try {
5872
- await fs13.chmod(tmpDir, 448).catch(() => {
6409
+ await fs14.chmod(tmpDir, 448).catch(() => {
5873
6410
  });
5874
6411
  const patchFile = path2.join(tmpDir, "in.diff");
5875
- await fs13.writeFile(patchFile, input.patch, { mode: 384 });
6412
+ await fs14.writeFile(patchFile, input.patch, { mode: 384 });
5876
6413
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
5877
6414
  const result = await runPatch(args, dir, opts.signal);
5878
6415
  if (result.exitCode !== 0 && !dryRun) {
@@ -5893,7 +6430,7 @@ var patchTool = {
5893
6430
  message: result.stdout || "patch applied"
5894
6431
  };
5895
6432
  } finally {
5896
- await fs13.rm(tmpDir, { recursive: true, force: true }).catch(() => {
6433
+ await fs14.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5897
6434
  });
5898
6435
  }
5899
6436
  }
@@ -5918,7 +6455,7 @@ function runPatch(args, cwd, signal) {
5918
6455
  let stdout = "";
5919
6456
  let stderr = "";
5920
6457
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
5921
- const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
6458
+ const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
5922
6459
  child.stdout?.on("data", (c) => {
5923
6460
  stdout += c.toString();
5924
6461
  });
@@ -6195,7 +6732,7 @@ var readTool = {
6195
6732
  const absPath = await safeResolveReal(input.path, ctx);
6196
6733
  let stat10;
6197
6734
  try {
6198
- stat10 = await fs13.stat(absPath);
6735
+ stat10 = await fs14.stat(absPath);
6199
6736
  } catch (err) {
6200
6737
  const code = err.code;
6201
6738
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -6207,7 +6744,7 @@ var readTool = {
6207
6744
  if (stat10.size > MAX_BYTES2) {
6208
6745
  throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
6209
6746
  }
6210
- const buf = await fs13.readFile(absPath);
6747
+ const buf = await fs14.readFile(absPath);
6211
6748
  if (isBinaryBuffer(buf)) {
6212
6749
  throw new Error(`read: "${input.path}" appears to be binary`);
6213
6750
  }
@@ -6275,11 +6812,11 @@ var replaceTool = {
6275
6812
  const dryRun = input.dry_run ?? false;
6276
6813
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
6277
6814
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
6278
- const realRoot = await fs13.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
6815
+ const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
6279
6816
  const results = [];
6280
6817
  let totalReplacements = 0;
6281
6818
  for (const absPath of fileList) {
6282
- const lstat2 = await fs13.lstat(absPath).catch((err) => {
6819
+ const lstat2 = await fs14.lstat(absPath).catch((err) => {
6283
6820
  if (err.code === "ENOENT") return null;
6284
6821
  throw err;
6285
6822
  });
@@ -6287,17 +6824,17 @@ var replaceTool = {
6287
6824
  if (lstat2.isSymbolicLink()) continue;
6288
6825
  let realPath;
6289
6826
  try {
6290
- realPath = await fs13.realpath(absPath);
6827
+ realPath = await fs14.realpath(absPath);
6291
6828
  } catch {
6292
6829
  continue;
6293
6830
  }
6294
6831
  const rel = path2.relative(realRoot, realPath);
6295
6832
  if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
6296
- const stat10 = await fs13.stat(realPath).catch(() => null);
6833
+ const stat10 = await fs14.stat(realPath).catch(() => null);
6297
6834
  if (!stat10 || !stat10.isFile()) continue;
6298
6835
  let content;
6299
6836
  try {
6300
- const buf = await fs13.readFile(realPath);
6837
+ const buf = await fs14.readFile(realPath);
6301
6838
  if (isBinaryBuffer(buf)) continue;
6302
6839
  content = buf.toString("utf8");
6303
6840
  } catch {
@@ -6349,7 +6886,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
6349
6886
  const resolved = [];
6350
6887
  for (const p of parts) {
6351
6888
  const absPath = safeResolve(p, ctx);
6352
- const stat10 = await fs13.stat(absPath).catch(() => null);
6889
+ const stat10 = await fs14.stat(absPath).catch(() => null);
6353
6890
  if (stat10?.isFile()) {
6354
6891
  resolved.push(absPath);
6355
6892
  }
@@ -6370,7 +6907,7 @@ async function globFiles(pattern, base, extraGlob) {
6370
6907
  function checkRg() {
6371
6908
  return new Promise((resolve7) => {
6372
6909
  try {
6373
- const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
6910
+ const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
6374
6911
  p.on("error", () => resolve7(false));
6375
6912
  p.on("close", (code) => resolve7(code === 0));
6376
6913
  } catch {
@@ -6383,7 +6920,8 @@ function spawnRgFind(pattern, base) {
6383
6920
  const child = spawn("rg", args, {
6384
6921
  signal: AbortSignal.timeout(3e4),
6385
6922
  env: buildChildEnv(),
6386
- stdio: ["ignore", "pipe", "pipe"]
6923
+ stdio: ["ignore", "pipe", "pipe"],
6924
+ windowsHide: true
6387
6925
  });
6388
6926
  let buf = "";
6389
6927
  child.stdout?.on("data", (chunk) => {
@@ -6404,7 +6942,7 @@ async function globNative(pattern, base, extraGlob) {
6404
6942
  const walk = async (dir) => {
6405
6943
  let entries;
6406
6944
  try {
6407
- entries = await fs13.readdir(dir, { withFileTypes: true });
6945
+ entries = await fs14.readdir(dir, { withFileTypes: true });
6408
6946
  } catch {
6409
6947
  return;
6410
6948
  }
@@ -6412,7 +6950,7 @@ async function globNative(pattern, base, extraGlob) {
6412
6950
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
6413
6951
  const full = path2.join(dir, e.name);
6414
6952
  try {
6415
- const stat10 = await fs13.lstat(full);
6953
+ const stat10 = await fs14.lstat(full);
6416
6954
  if (stat10.isSymbolicLink()) continue;
6417
6955
  } catch {
6418
6956
  continue;
@@ -6589,7 +7127,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
6589
7127
  }
6590
7128
  const fullPath = target;
6591
7129
  if (!dryRun) {
6592
- await fs13.mkdir(path2.dirname(fullPath), { recursive: true });
7130
+ await fs14.mkdir(path2.dirname(fullPath), { recursive: true });
6593
7131
  await atomicWrite(fullPath, substituteVars(content, name, vars));
6594
7132
  }
6595
7133
  files.push(resolvedPath);
@@ -6867,7 +7405,7 @@ var setWorkingDirTool = {
6867
7405
  };
6868
7406
  }
6869
7407
  try {
6870
- await fs13.access(resolved);
7408
+ await fs14.access(resolved);
6871
7409
  } catch {
6872
7410
  try {
6873
7411
  ctx.setWorkingDir(previous);
@@ -7840,7 +8378,7 @@ var treeTool = {
7840
8378
  }
7841
8379
  };
7842
8380
  async function walkDir(dir, depth, opts) {
7843
- const entries = await fs13.readdir(dir, { withFileTypes: true }).catch(() => []);
8381
+ const entries = await fs14.readdir(dir, { withFileTypes: true }).catch(() => []);
7844
8382
  const filtered = entries.filter((e) => {
7845
8383
  if (!opts.showHidden && e.name.startsWith(".")) return false;
7846
8384
  if (opts.exclude.has(e.name)) return false;
@@ -7990,14 +8528,14 @@ var writeTool = {
7990
8528
  let existed = false;
7991
8529
  let prev = "";
7992
8530
  try {
7993
- const stat11 = await fs13.stat(absPath);
8531
+ const stat11 = await fs14.stat(absPath);
7994
8532
  existed = stat11.isFile();
7995
8533
  if (existed) {
7996
8534
  if (!ctx.hasRead(absPath)) {
7997
- prev = await fs13.readFile(absPath, "utf8");
8535
+ prev = await fs14.readFile(absPath, "utf8");
7998
8536
  ctx.recordRead(absPath, stat11.mtimeMs);
7999
8537
  } else {
8000
- prev = await fs13.readFile(absPath, "utf8");
8538
+ prev = await fs14.readFile(absPath, "utf8");
8001
8539
  }
8002
8540
  }
8003
8541
  } catch (err) {
@@ -8008,7 +8546,7 @@ var writeTool = {
8008
8546
  await atomicWrite(absPath, input.content);
8009
8547
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
8010
8548
  + (new file, ${input.content.split("\n").length} lines)`;
8011
- const stat10 = await fs13.stat(absPath);
8549
+ const stat10 = await fs14.stat(absPath);
8012
8550
  ctx.recordRead(absPath, stat10.mtimeMs);
8013
8551
  ctx.session.recordFileChange({
8014
8552
  path: absPath,