@wrongstack/tools 0.155.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.
- package/dist/audit.js +22 -1
- package/dist/audit.js.map +1 -1
- package/dist/background-indexer-DwJsyAB0.d.ts +373 -0
- package/dist/bash.js +121 -24
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +1553 -544
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +9 -2
- package/dist/circuit-breaker.js +11 -2
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/codebase-index/index.d.ts +53 -2
- package/dist/codebase-index/index.js +866 -367
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.d.ts +2 -0
- package/dist/codebase-index/worker.js +2321 -0
- package/dist/codebase-index/worker.js.map +1 -0
- package/dist/diff.js +3 -2
- package/dist/diff.js.map +1 -1
- package/dist/document.js +1 -1
- package/dist/document.js.map +1 -1
- package/dist/edit.js +1 -1
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +61 -11
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +22 -1
- package/dist/format.js.map +1 -1
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/glob.js +1 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +3 -3
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1593 -622
- package/dist/index.js.map +1 -1
- package/dist/install.js +66 -14
- package/dist/install.js.map +1 -1
- package/dist/lint.js +22 -1
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +2 -2
- package/dist/logs.js.map +1 -1
- package/dist/outdated.js +2 -2
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +1553 -544
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +2 -2
- package/dist/patch.js.map +1 -1
- package/dist/process-registry.d.ts +21 -16
- package/dist/process-registry.js +48 -10
- package/dist/process-registry.js.map +1 -1
- package/dist/read.js +1 -1
- package/dist/read.js.map +1 -1
- package/dist/replace.js +4 -3
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +1 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/search.js +19 -16
- package/dist/search.js.map +1 -1
- package/dist/test.js +22 -1
- package/dist/test.js.map +1 -1
- package/dist/todo.js +44 -0
- package/dist/todo.js.map +1 -1
- package/dist/tree.js +1 -1
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +22 -1
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +1 -1
- package/dist/write.js.map +1 -1
- package/package.json +5 -5
- package/dist/background-indexer-CtbgPExj.d.ts +0 -228
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs4 from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { resolve, sep, dirname } from 'node:path';
|
|
3
|
+
import { resolve, sep, dirname, join } from 'node:path';
|
|
4
4
|
import * as Core from '@wrongstack/core';
|
|
5
|
-
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan,
|
|
5
|
+
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
6
6
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import * as os from 'node:os';
|
|
8
8
|
import * as fs7 from 'node:fs';
|
|
@@ -11,7 +11,10 @@ import * as dns from 'node:dns/promises';
|
|
|
11
11
|
import * as net from 'node:net';
|
|
12
12
|
import { Agent } from 'undici';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { Worker } from 'node:worker_threads';
|
|
14
16
|
import * as ts from 'typescript';
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
15
18
|
|
|
16
19
|
// src/read.ts
|
|
17
20
|
async function detectPackageManager(cwd) {
|
|
@@ -29,7 +32,7 @@ async function detectPackageManager(cwd) {
|
|
|
29
32
|
return "npm";
|
|
30
33
|
}
|
|
31
34
|
function resolvePath(input, ctx) {
|
|
32
|
-
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
|
|
35
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
33
36
|
}
|
|
34
37
|
function ensureInsideRoot(absPath, ctx) {
|
|
35
38
|
const root = path.resolve(ctx.projectRoot);
|
|
@@ -598,7 +601,7 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
598
601
|
function checkRg() {
|
|
599
602
|
return new Promise((resolve7) => {
|
|
600
603
|
try {
|
|
601
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
|
|
604
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
|
|
602
605
|
p.on("error", () => resolve7(false));
|
|
603
606
|
p.on("close", (code) => resolve7(code === 0));
|
|
604
607
|
} catch {
|
|
@@ -611,7 +614,8 @@ function spawnRgFind(pattern, base) {
|
|
|
611
614
|
const child = spawn("rg", args, {
|
|
612
615
|
signal: AbortSignal.timeout(3e4),
|
|
613
616
|
env: buildChildEnv(),
|
|
614
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
617
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
618
|
+
windowsHide: true
|
|
615
619
|
});
|
|
616
620
|
let buf = "";
|
|
617
621
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -827,7 +831,7 @@ var grepTool = {
|
|
|
827
831
|
async function detectRg(signal) {
|
|
828
832
|
return new Promise((resolve7) => {
|
|
829
833
|
try {
|
|
830
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
|
|
834
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
|
|
831
835
|
p.on("error", () => resolve7(false));
|
|
832
836
|
p.on("close", (code) => resolve7(code === 0));
|
|
833
837
|
} catch {
|
|
@@ -857,7 +861,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
857
861
|
const FLUSH_AT = 16;
|
|
858
862
|
const MAX_BUF_BYTES = 1e6;
|
|
859
863
|
let bufOverflow = false;
|
|
860
|
-
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
864
|
+
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
861
865
|
const queue = [];
|
|
862
866
|
let waiter;
|
|
863
867
|
const wake = () => {
|
|
@@ -1098,8 +1102,13 @@ var CircuitBreaker = class {
|
|
|
1098
1102
|
* Call this BEFORE spawning a bash/exec process.
|
|
1099
1103
|
* Returns true if the call is allowed; false if the breaker is open.
|
|
1100
1104
|
* When false, callers MUST NOT spawn a process.
|
|
1105
|
+
*
|
|
1106
|
+
* @param bypass - If true, skip the circuit breaker check entirely.
|
|
1107
|
+
* Use for background/fire-and-forget processes that should
|
|
1108
|
+
* not affect breaker state.
|
|
1101
1109
|
*/
|
|
1102
|
-
beforeCall() {
|
|
1110
|
+
beforeCall(bypass = false) {
|
|
1111
|
+
if (bypass) return true;
|
|
1103
1112
|
this._checkStateTransition();
|
|
1104
1113
|
if (this.state === "open") return false;
|
|
1105
1114
|
return true;
|
|
@@ -1109,8 +1118,12 @@ var CircuitBreaker = class {
|
|
|
1109
1118
|
* `durationMs` is the wall-clock time the process ran.
|
|
1110
1119
|
* `failed` is true when the process returned a non-zero exit code or
|
|
1111
1120
|
* threw an exception before spawning.
|
|
1121
|
+
*
|
|
1122
|
+
* @param bypass - If true, do not update breaker state.
|
|
1123
|
+
* Use for background/fire-and-forget processes.
|
|
1112
1124
|
*/
|
|
1113
|
-
afterCall(durationMs, failed) {
|
|
1125
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
1126
|
+
if (bypass) return;
|
|
1114
1127
|
const now = Date.now();
|
|
1115
1128
|
if (this.state === "half-open") {
|
|
1116
1129
|
if (failed) {
|
|
@@ -1209,6 +1222,17 @@ function redactCommand(cmd) {
|
|
|
1209
1222
|
return result;
|
|
1210
1223
|
}
|
|
1211
1224
|
var DEFAULT_GRACE_MS = 2e3;
|
|
1225
|
+
function killWin32Tree(pid) {
|
|
1226
|
+
try {
|
|
1227
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
1228
|
+
stdio: "ignore",
|
|
1229
|
+
windowsHide: true
|
|
1230
|
+
}).unref();
|
|
1231
|
+
return true;
|
|
1232
|
+
} catch {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1212
1236
|
var ProcessRegistryImpl = class {
|
|
1213
1237
|
processes = /* @__PURE__ */ new Map();
|
|
1214
1238
|
breaker;
|
|
@@ -1266,16 +1290,20 @@ var ProcessRegistryImpl = class {
|
|
|
1266
1290
|
/**
|
|
1267
1291
|
* Called before spawning a process. Returns true if allowed; false if
|
|
1268
1292
|
* the circuit breaker is open.
|
|
1293
|
+
*
|
|
1294
|
+
* @param bypass - If true, skip circuit breaker check (for background processes).
|
|
1269
1295
|
*/
|
|
1270
|
-
beforeCall() {
|
|
1271
|
-
return this.breaker.beforeCall();
|
|
1296
|
+
beforeCall(bypass = false) {
|
|
1297
|
+
return this.breaker.beforeCall(bypass);
|
|
1272
1298
|
}
|
|
1273
1299
|
/**
|
|
1274
1300
|
* Called after a process finishes. `durationMs` is wall-clock time;
|
|
1275
1301
|
* `failed` is true for non-zero exit codes.
|
|
1302
|
+
*
|
|
1303
|
+
* @param bypass - If true, do not update circuit breaker state (for background processes).
|
|
1276
1304
|
*/
|
|
1277
|
-
afterCall(durationMs, failed) {
|
|
1278
|
-
this.breaker.afterCall(durationMs, failed);
|
|
1305
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
1306
|
+
this.breaker.afterCall(durationMs, failed, bypass);
|
|
1279
1307
|
}
|
|
1280
1308
|
/** Force-open the circuit breaker (Ctrl+C, /kill force). */
|
|
1281
1309
|
forceBreakerOpen() {
|
|
@@ -1306,9 +1334,22 @@ var ProcessRegistryImpl = class {
|
|
|
1306
1334
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
1307
1335
|
const isWin = os.platform() === "win32";
|
|
1308
1336
|
if (isWin) {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1337
|
+
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
1338
|
+
if (liveRealChild && killWin32Tree(pid)) {
|
|
1339
|
+
const fallback = setTimeout(() => {
|
|
1340
|
+
if (p.child.exitCode === null) {
|
|
1341
|
+
try {
|
|
1342
|
+
p.child.kill("SIGKILL");
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}, graceMs);
|
|
1347
|
+
fallback.unref?.();
|
|
1348
|
+
} else {
|
|
1349
|
+
try {
|
|
1350
|
+
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1351
|
+
} catch {
|
|
1352
|
+
}
|
|
1312
1353
|
}
|
|
1313
1354
|
p.killed = true;
|
|
1314
1355
|
return true;
|
|
@@ -1387,6 +1428,7 @@ var MAX_OUTPUT = 32768;
|
|
|
1387
1428
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
1388
1429
|
var STREAM_FLUSH_INTERVAL_MS = 200;
|
|
1389
1430
|
var STREAM_FLUSH_BYTES = 4 * 1024;
|
|
1431
|
+
var MAX_QUEUE_CHUNKS = 500;
|
|
1390
1432
|
var bashTool = {
|
|
1391
1433
|
name: "bash",
|
|
1392
1434
|
category: "Shell",
|
|
@@ -1434,7 +1476,8 @@ var bashTool = {
|
|
|
1434
1476
|
async *executeStream(input, ctx, opts) {
|
|
1435
1477
|
if (!input?.command) throw new Error("bash: command is required");
|
|
1436
1478
|
const registry = getProcessRegistry();
|
|
1437
|
-
|
|
1479
|
+
const bypassBreaker = !!input.background;
|
|
1480
|
+
if (!registry.beforeCall(bypassBreaker)) {
|
|
1438
1481
|
yield {
|
|
1439
1482
|
type: "final",
|
|
1440
1483
|
output: {
|
|
@@ -1447,6 +1490,17 @@ var bashTool = {
|
|
|
1447
1490
|
};
|
|
1448
1491
|
return;
|
|
1449
1492
|
}
|
|
1493
|
+
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
1494
|
+
if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
|
|
1495
|
+
console.warn(JSON.stringify({
|
|
1496
|
+
level: "warn",
|
|
1497
|
+
event: "bash.pipe_to_shell_detected",
|
|
1498
|
+
message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
|
|
1499
|
+
command_prefix: input.command.slice(0, 100),
|
|
1500
|
+
// Log first 100 chars for review
|
|
1501
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1502
|
+
}));
|
|
1503
|
+
}
|
|
1450
1504
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1451
1505
|
const isWin = os.platform() === "win32";
|
|
1452
1506
|
const shell = (() => {
|
|
@@ -1472,6 +1526,10 @@ var bashTool = {
|
|
|
1472
1526
|
env,
|
|
1473
1527
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1474
1528
|
detached: true,
|
|
1529
|
+
// Detached console children on Windows allocate their own VISIBLE
|
|
1530
|
+
// console window (one per background command — test suites flash
|
|
1531
|
+
// dozens). CREATE_NO_WINDOW suppresses it; no-op elsewhere.
|
|
1532
|
+
windowsHide: true,
|
|
1475
1533
|
signal: opts.signal
|
|
1476
1534
|
});
|
|
1477
1535
|
const pid2 = child2.pid;
|
|
@@ -1505,7 +1563,7 @@ var bashTool = {
|
|
|
1505
1563
|
}
|
|
1506
1564
|
});
|
|
1507
1565
|
child2.on("close", () => {
|
|
1508
|
-
registry.afterCall(Date.now() - startedAt, false);
|
|
1566
|
+
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
1509
1567
|
});
|
|
1510
1568
|
if (typeof pid2 === "number") child2.unref();
|
|
1511
1569
|
yield {
|
|
@@ -1524,7 +1582,8 @@ var bashTool = {
|
|
|
1524
1582
|
env,
|
|
1525
1583
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1526
1584
|
detached,
|
|
1527
|
-
|
|
1585
|
+
windowsHide: true,
|
|
1586
|
+
...isWin ? {} : { signal: opts.signal }
|
|
1528
1587
|
});
|
|
1529
1588
|
const pid = child.pid;
|
|
1530
1589
|
if (typeof pid === "number") {
|
|
@@ -1538,14 +1597,27 @@ var bashTool = {
|
|
|
1538
1597
|
});
|
|
1539
1598
|
}
|
|
1540
1599
|
let buf = "";
|
|
1541
|
-
let
|
|
1600
|
+
let pending2 = "";
|
|
1542
1601
|
let timedOut = false;
|
|
1543
1602
|
const timers = [];
|
|
1544
1603
|
function killWithTimeout(child2, timeoutMs2) {
|
|
1545
1604
|
if (isWin) {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1605
|
+
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
1606
|
+
const fallback = setTimeout(() => {
|
|
1607
|
+
if (child2.exitCode === null) {
|
|
1608
|
+
try {
|
|
1609
|
+
child2.kill();
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}, 2e3);
|
|
1614
|
+
timers.push(fallback);
|
|
1615
|
+
fallback.unref?.();
|
|
1616
|
+
} else {
|
|
1617
|
+
try {
|
|
1618
|
+
child2.kill();
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1549
1621
|
}
|
|
1550
1622
|
return;
|
|
1551
1623
|
}
|
|
@@ -1584,6 +1656,11 @@ var bashTool = {
|
|
|
1584
1656
|
}, timeoutMs);
|
|
1585
1657
|
timers.push(timer);
|
|
1586
1658
|
timer.unref?.();
|
|
1659
|
+
const onAbort = () => killWithTimeout(child, 2e3);
|
|
1660
|
+
if (isWin) {
|
|
1661
|
+
if (opts.signal.aborted) onAbort();
|
|
1662
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
1663
|
+
}
|
|
1587
1664
|
const queue = [];
|
|
1588
1665
|
let resolveNext = null;
|
|
1589
1666
|
const push = (c) => {
|
|
@@ -1602,24 +1679,38 @@ var bashTool = {
|
|
|
1602
1679
|
});
|
|
1603
1680
|
let lastFlush = Date.now();
|
|
1604
1681
|
const flush = () => {
|
|
1605
|
-
if (
|
|
1606
|
-
const text =
|
|
1607
|
-
|
|
1682
|
+
if (pending2.length === 0) return null;
|
|
1683
|
+
const text = pending2;
|
|
1684
|
+
pending2 = "";
|
|
1608
1685
|
lastFlush = Date.now();
|
|
1609
1686
|
return text;
|
|
1610
1687
|
};
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1688
|
+
let paused = false;
|
|
1689
|
+
const pauseIfFlooded = () => {
|
|
1690
|
+
if (!paused && queue.length >= MAX_QUEUE_CHUNKS) {
|
|
1691
|
+
paused = true;
|
|
1692
|
+
child.stdout?.pause();
|
|
1693
|
+
child.stderr?.pause();
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
const resumeIfDrained = () => {
|
|
1697
|
+
if (paused && queue.length < MAX_QUEUE_CHUNKS) {
|
|
1698
|
+
paused = false;
|
|
1699
|
+
child.stdout?.resume();
|
|
1700
|
+
child.stderr?.resume();
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
const onData = (chunk) => {
|
|
1618
1704
|
const text = chunk.toString();
|
|
1619
|
-
buf
|
|
1620
|
-
|
|
1705
|
+
if (buf.length < MAX_OUTPUT) {
|
|
1706
|
+
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1707
|
+
}
|
|
1708
|
+
pending2 += text;
|
|
1621
1709
|
push({ kind: "data", text });
|
|
1622
|
-
|
|
1710
|
+
pauseIfFlooded();
|
|
1711
|
+
};
|
|
1712
|
+
child.stdout?.on("data", onData);
|
|
1713
|
+
child.stderr?.on("data", onData);
|
|
1623
1714
|
child.on("error", (err) => {
|
|
1624
1715
|
for (const t of timers) clearTimeout(t);
|
|
1625
1716
|
registry.afterCall(Date.now() - startedAt, true);
|
|
@@ -1634,6 +1725,7 @@ var bashTool = {
|
|
|
1634
1725
|
try {
|
|
1635
1726
|
while (true) {
|
|
1636
1727
|
const c = await next();
|
|
1728
|
+
resumeIfDrained();
|
|
1637
1729
|
if (c.kind === "error") throw c.err;
|
|
1638
1730
|
if (c.kind === "end") {
|
|
1639
1731
|
const remainder = flush();
|
|
@@ -1651,13 +1743,22 @@ var bashTool = {
|
|
|
1651
1743
|
return;
|
|
1652
1744
|
}
|
|
1653
1745
|
const now = Date.now();
|
|
1654
|
-
if (
|
|
1746
|
+
if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
|
|
1655
1747
|
const text = flush();
|
|
1656
1748
|
if (text) yield { type: "partial_output", text };
|
|
1657
1749
|
}
|
|
1658
1750
|
}
|
|
1659
1751
|
} finally {
|
|
1660
1752
|
for (const t of timers) clearTimeout(t);
|
|
1753
|
+
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1754
|
+
child.stdout?.off("data", onData);
|
|
1755
|
+
child.stderr?.off("data", onData);
|
|
1756
|
+
child.stdout?.destroy();
|
|
1757
|
+
child.stderr?.destroy();
|
|
1758
|
+
if (child.exitCode === null && !child.killed) {
|
|
1759
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
1760
|
+
else killWithTimeout(child, 2e3);
|
|
1761
|
+
}
|
|
1661
1762
|
}
|
|
1662
1763
|
}
|
|
1663
1764
|
};
|
|
@@ -1890,12 +1991,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1890
1991
|
let killed = false;
|
|
1891
1992
|
const startedAt = Date.now();
|
|
1892
1993
|
const resolved = resolveWin32Command(cmd);
|
|
1893
|
-
const
|
|
1994
|
+
const isWin = process.platform === "win32";
|
|
1995
|
+
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
1894
1996
|
const child = spawn(resolved, args, {
|
|
1895
1997
|
cwd,
|
|
1896
|
-
signal,
|
|
1897
1998
|
env: buildChildEnv(sessionId),
|
|
1898
1999
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2000
|
+
windowsHide: true,
|
|
2001
|
+
...isWin ? {} : { signal },
|
|
1899
2002
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
1900
2003
|
});
|
|
1901
2004
|
const registry = getProcessRegistry();
|
|
@@ -1909,6 +2012,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1909
2012
|
if (typeof pid === "number") registry.kill(pid);
|
|
1910
2013
|
else child.kill("SIGTERM");
|
|
1911
2014
|
}, timeout);
|
|
2015
|
+
const onAbort = () => {
|
|
2016
|
+
killed = true;
|
|
2017
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
2018
|
+
else child.kill("SIGTERM");
|
|
2019
|
+
};
|
|
2020
|
+
if (isWin) {
|
|
2021
|
+
if (signal.aborted) onAbort();
|
|
2022
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2023
|
+
}
|
|
1912
2024
|
child.stdout?.on("data", (chunk) => {
|
|
1913
2025
|
if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
|
|
1914
2026
|
});
|
|
@@ -1917,6 +2029,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1917
2029
|
});
|
|
1918
2030
|
child.on("close", (code) => {
|
|
1919
2031
|
clearTimeout(timer);
|
|
2032
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
1920
2033
|
if (typeof pid === "number") registry.unregister(pid);
|
|
1921
2034
|
const durationMs = Date.now() - startedAt;
|
|
1922
2035
|
const exitCode = killed ? 124 : code ?? 1;
|
|
@@ -1933,6 +2046,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1933
2046
|
});
|
|
1934
2047
|
child.on("error", (err) => {
|
|
1935
2048
|
clearTimeout(timer);
|
|
2049
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
1936
2050
|
if (typeof pid === "number") registry.unregister(pid);
|
|
1937
2051
|
registry.afterCall(Date.now() - startedAt, true);
|
|
1938
2052
|
resolve7({
|
|
@@ -2350,13 +2464,24 @@ var searchTool = {
|
|
|
2350
2464
|
async function duckduckgoSearch(query2, num, signal) {
|
|
2351
2465
|
const encoded = encodeURIComponent(query2);
|
|
2352
2466
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
results,
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2467
|
+
try {
|
|
2468
|
+
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS2);
|
|
2469
|
+
const html = await response.text();
|
|
2470
|
+
const results = parseDuckDuckGo(html, num);
|
|
2471
|
+
return {
|
|
2472
|
+
query: query2,
|
|
2473
|
+
results,
|
|
2474
|
+
source: "duckduckgo",
|
|
2475
|
+
truncated: results.length >= num
|
|
2476
|
+
};
|
|
2477
|
+
} catch {
|
|
2478
|
+
return {
|
|
2479
|
+
query: query2,
|
|
2480
|
+
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
2481
|
+
source: "duckduckgo",
|
|
2482
|
+
truncated: false
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2360
2485
|
}
|
|
2361
2486
|
function takeFrom(iter, max) {
|
|
2362
2487
|
const out = [];
|
|
@@ -2475,21 +2600,11 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
|
2475
2600
|
}
|
|
2476
2601
|
}
|
|
2477
2602
|
function anySignal(...signals) {
|
|
2478
|
-
|
|
2479
|
-
for (const s of signals) {
|
|
2480
|
-
if (s.aborted) {
|
|
2481
|
-
controller.abort();
|
|
2482
|
-
break;
|
|
2483
|
-
}
|
|
2484
|
-
s.addEventListener("abort", () => controller.abort());
|
|
2485
|
-
}
|
|
2486
|
-
return controller.signal;
|
|
2603
|
+
return AbortSignal.any(signals);
|
|
2487
2604
|
}
|
|
2488
2605
|
function stripTags2(html) {
|
|
2489
2606
|
return html.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
2490
2607
|
}
|
|
2491
|
-
|
|
2492
|
-
// src/todo.ts
|
|
2493
2608
|
var todoTool = {
|
|
2494
2609
|
name: "todo",
|
|
2495
2610
|
category: "Session",
|
|
@@ -2548,6 +2663,48 @@ var todoTool = {
|
|
|
2548
2663
|
}
|
|
2549
2664
|
}
|
|
2550
2665
|
ctx.state.replaceTodos(items);
|
|
2666
|
+
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
2667
|
+
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
2668
|
+
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
2669
|
+
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
2670
|
+
for (const item of items) {
|
|
2671
|
+
if (item.promotedFromPlan) {
|
|
2672
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
|
|
2673
|
+
}
|
|
2674
|
+
if (item.promotedFromTask) {
|
|
2675
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
for (const planId of completedPlanIds) {
|
|
2679
|
+
if (pendingPlanIds.has(planId)) continue;
|
|
2680
|
+
const planPath = ctx.meta["plan.path"];
|
|
2681
|
+
if (typeof planPath !== "string" || !planPath) continue;
|
|
2682
|
+
try {
|
|
2683
|
+
const plan = await loadPlan(planPath);
|
|
2684
|
+
if (plan) {
|
|
2685
|
+
const updated = setPlanItemStatus(plan, planId, "done");
|
|
2686
|
+
await savePlan(planPath, updated);
|
|
2687
|
+
}
|
|
2688
|
+
} catch {
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
for (const taskId of completedTaskIds) {
|
|
2692
|
+
if (pendingTaskIds.has(taskId)) continue;
|
|
2693
|
+
const taskPath = ctx.meta["task.path"];
|
|
2694
|
+
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
2695
|
+
try {
|
|
2696
|
+
const file = await loadTasks(taskPath);
|
|
2697
|
+
if (file) {
|
|
2698
|
+
const task = file.tasks.find((t) => t.id === taskId);
|
|
2699
|
+
if (task && task.status !== "completed") {
|
|
2700
|
+
task.status = "completed";
|
|
2701
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2702
|
+
await saveTasks(taskPath, file);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
} catch {
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2551
2708
|
return {
|
|
2552
2709
|
count: items.length,
|
|
2553
2710
|
in_progress: items.filter((t) => t.status === "in_progress").length
|
|
@@ -2558,7 +2715,7 @@ var planTool = {
|
|
|
2558
2715
|
name: "plan",
|
|
2559
2716
|
category: "Session",
|
|
2560
2717
|
description: "Manage a persistent strategic plan for the current session. Unlike todos, plans are meant for higher-level, multi-phase approaches and survive across conversation resumptions. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute.",
|
|
2561
|
-
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns or even multiple sessions.',
|
|
2718
|
+
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns or even multiple sessions.',
|
|
2562
2719
|
permission: "confirm",
|
|
2563
2720
|
mutating: true,
|
|
2564
2721
|
capabilities: ["fs.write"],
|
|
@@ -2575,9 +2732,9 @@ var planTool = {
|
|
|
2575
2732
|
"done",
|
|
2576
2733
|
"remove",
|
|
2577
2734
|
"promote",
|
|
2578
|
-
"derive",
|
|
2579
2735
|
"template_use",
|
|
2580
|
-
"clear"
|
|
2736
|
+
"clear",
|
|
2737
|
+
"taskify"
|
|
2581
2738
|
],
|
|
2582
2739
|
description: "The operation to perform on the plan board."
|
|
2583
2740
|
},
|
|
@@ -2596,7 +2753,7 @@ var planTool = {
|
|
|
2596
2753
|
subtasks: {
|
|
2597
2754
|
type: "array",
|
|
2598
2755
|
items: { type: "string" },
|
|
2599
|
-
description: "List of subtask titles. Used with promote
|
|
2756
|
+
description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
|
|
2600
2757
|
},
|
|
2601
2758
|
template: {
|
|
2602
2759
|
type: "string",
|
|
@@ -2617,92 +2774,151 @@ var planTool = {
|
|
|
2617
2774
|
};
|
|
2618
2775
|
}
|
|
2619
2776
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
2620
|
-
let
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
if (!input.target) {
|
|
2636
|
-
return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2777
|
+
let early = null;
|
|
2778
|
+
const taskifyMeta = { title: "", details: "" };
|
|
2779
|
+
let didTaskify = false;
|
|
2780
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
2781
|
+
switch (input.action) {
|
|
2782
|
+
case "show":
|
|
2783
|
+
break;
|
|
2784
|
+
case "add": {
|
|
2785
|
+
const title = input.title?.trim();
|
|
2786
|
+
if (!title) {
|
|
2787
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
2788
|
+
return p;
|
|
2789
|
+
}
|
|
2790
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
2791
|
+
return updated;
|
|
2637
2792
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
input.target
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2793
|
+
case "start":
|
|
2794
|
+
case "done": {
|
|
2795
|
+
if (!input.target) {
|
|
2796
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2797
|
+
return p;
|
|
2798
|
+
}
|
|
2799
|
+
const next = setPlanItemStatus(
|
|
2800
|
+
p,
|
|
2801
|
+
input.target,
|
|
2802
|
+
input.action === "start" ? "in_progress" : "done"
|
|
2803
|
+
);
|
|
2804
|
+
if (next === p) {
|
|
2805
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2806
|
+
return p;
|
|
2807
|
+
}
|
|
2808
|
+
return next;
|
|
2645
2809
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2810
|
+
case "remove": {
|
|
2811
|
+
if (!input.target) {
|
|
2812
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
2813
|
+
return p;
|
|
2814
|
+
}
|
|
2815
|
+
const next = removePlanItem(p, input.target);
|
|
2816
|
+
if (next === p) {
|
|
2817
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2818
|
+
return p;
|
|
2819
|
+
}
|
|
2820
|
+
return next;
|
|
2653
2821
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2822
|
+
case "promote": {
|
|
2823
|
+
if (!input.target) {
|
|
2824
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2825
|
+
return p;
|
|
2826
|
+
}
|
|
2827
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
2828
|
+
if (!derived) {
|
|
2829
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2830
|
+
return p;
|
|
2831
|
+
}
|
|
2832
|
+
ctx.state.replaceTodos(derived.todos);
|
|
2833
|
+
early = mkResult(
|
|
2834
|
+
derived.plan,
|
|
2835
|
+
true,
|
|
2836
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
2837
|
+
derived.todos
|
|
2838
|
+
);
|
|
2839
|
+
return derived.plan;
|
|
2657
2840
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2841
|
+
case "template_use": {
|
|
2842
|
+
const templateName = input.template?.trim();
|
|
2843
|
+
if (!templateName) {
|
|
2844
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
2845
|
+
return p;
|
|
2846
|
+
}
|
|
2847
|
+
const template = getPlanTemplate(templateName);
|
|
2848
|
+
if (!template) {
|
|
2849
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
2850
|
+
return p;
|
|
2851
|
+
}
|
|
2852
|
+
let updated = p;
|
|
2853
|
+
for (const item of template.items) {
|
|
2854
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
2855
|
+
}
|
|
2856
|
+
early = mkResult(
|
|
2857
|
+
updated,
|
|
2858
|
+
true,
|
|
2859
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
2860
|
+
);
|
|
2861
|
+
return updated;
|
|
2666
2862
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2863
|
+
case "clear":
|
|
2864
|
+
return clearPlan(p);
|
|
2865
|
+
case "taskify": {
|
|
2866
|
+
if (!input.target) {
|
|
2867
|
+
early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
|
|
2868
|
+
return p;
|
|
2869
|
+
}
|
|
2870
|
+
let itemIdx = -1;
|
|
2871
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
2872
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
2873
|
+
itemIdx = asNum - 1;
|
|
2874
|
+
} else {
|
|
2875
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
2876
|
+
if (itemIdx === -1) {
|
|
2877
|
+
const lower = input.target.toLowerCase();
|
|
2878
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
2882
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2883
|
+
return p;
|
|
2884
|
+
}
|
|
2885
|
+
const item = p.items[itemIdx];
|
|
2886
|
+
taskifyMeta.title = item.title;
|
|
2887
|
+
taskifyMeta.details = item.details ?? "";
|
|
2888
|
+
didTaskify = true;
|
|
2889
|
+
break;
|
|
2670
2890
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
return mkResult(
|
|
2675
|
-
plan,
|
|
2676
|
-
true,
|
|
2677
|
-
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
2678
|
-
derived.todos
|
|
2679
|
-
);
|
|
2891
|
+
default:
|
|
2892
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
2893
|
+
return p;
|
|
2680
2894
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
return mkResult(plan, false, `Unknown template "${templateName}".`);
|
|
2689
|
-
}
|
|
2690
|
-
for (const item of template.items) {
|
|
2691
|
-
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
2692
|
-
}
|
|
2693
|
-
await savePlan(planPath, plan);
|
|
2694
|
-
return mkResult(
|
|
2695
|
-
plan,
|
|
2696
|
-
true,
|
|
2697
|
-
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
2698
|
-
);
|
|
2895
|
+
return p;
|
|
2896
|
+
});
|
|
2897
|
+
if (early) return early;
|
|
2898
|
+
if (didTaskify) {
|
|
2899
|
+
const taskPath = ctx.meta["task.path"];
|
|
2900
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
2901
|
+
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
2699
2902
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2903
|
+
const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
|
|
2904
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2905
|
+
taskFile.tasks.push({
|
|
2906
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
2907
|
+
title: taskifyMeta.title,
|
|
2908
|
+
description: taskifyMeta.details || void 0,
|
|
2909
|
+
type: "feature",
|
|
2910
|
+
priority: "medium",
|
|
2911
|
+
status: "pending",
|
|
2912
|
+
createdAt: now,
|
|
2913
|
+
updatedAt: now
|
|
2914
|
+
});
|
|
2915
|
+
await saveTasks(taskPath, taskFile);
|
|
2916
|
+
return mkResult(
|
|
2917
|
+
plan,
|
|
2918
|
+
true,
|
|
2919
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
2920
|
+
${formatTaskList(taskFile.tasks)}`
|
|
2921
|
+
);
|
|
2706
2922
|
}
|
|
2707
2923
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
2708
2924
|
}
|
|
@@ -2944,7 +3160,8 @@ function runGit(args, cwd, signal) {
|
|
|
2944
3160
|
cwd,
|
|
2945
3161
|
signal,
|
|
2946
3162
|
env: buildChildEnv(),
|
|
2947
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3163
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3164
|
+
windowsHide: true
|
|
2948
3165
|
});
|
|
2949
3166
|
child.stdout?.on("data", (chunk) => {
|
|
2950
3167
|
if (stdout.length < MAX_OUTPUT3) {
|
|
@@ -3067,7 +3284,7 @@ function runPatch(args, cwd, signal) {
|
|
|
3067
3284
|
let stdout = "";
|
|
3068
3285
|
let stderr = "";
|
|
3069
3286
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
3070
|
-
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
3287
|
+
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
3071
3288
|
child.stdout?.on("data", (c) => {
|
|
3072
3289
|
stdout += c.toString();
|
|
3073
3290
|
});
|
|
@@ -3306,7 +3523,8 @@ function runGit2(args, cwd, signal) {
|
|
|
3306
3523
|
cwd,
|
|
3307
3524
|
signal,
|
|
3308
3525
|
env: buildChildEnv(),
|
|
3309
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3526
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3527
|
+
windowsHide: true
|
|
3310
3528
|
});
|
|
3311
3529
|
child.stdout?.on("data", (c) => {
|
|
3312
3530
|
stdout += c.toString();
|
|
@@ -3531,9 +3749,10 @@ async function walkDir(dir, depth, opts) {
|
|
|
3531
3749
|
async function* spawnStream(opts) {
|
|
3532
3750
|
const max = opts.maxBytes ?? 2e5;
|
|
3533
3751
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
3752
|
+
const maxQueue = opts.maxQueueSize ?? 500;
|
|
3534
3753
|
let stdout = "";
|
|
3535
3754
|
let stderr = "";
|
|
3536
|
-
let
|
|
3755
|
+
let pending2 = "";
|
|
3537
3756
|
let error;
|
|
3538
3757
|
const cmd = resolveWin32Command(opts.cmd);
|
|
3539
3758
|
const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
|
|
@@ -3542,10 +3761,12 @@ async function* spawnStream(opts) {
|
|
|
3542
3761
|
signal: opts.signal,
|
|
3543
3762
|
env: buildChildEnv(),
|
|
3544
3763
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3764
|
+
windowsHide: true,
|
|
3545
3765
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
3546
3766
|
});
|
|
3547
3767
|
const queue = [];
|
|
3548
3768
|
let waiter;
|
|
3769
|
+
let paused = false;
|
|
3549
3770
|
const wake = () => {
|
|
3550
3771
|
if (waiter) {
|
|
3551
3772
|
const w = waiter;
|
|
@@ -3553,17 +3774,34 @@ async function* spawnStream(opts) {
|
|
|
3553
3774
|
w();
|
|
3554
3775
|
}
|
|
3555
3776
|
};
|
|
3777
|
+
const resume = () => {
|
|
3778
|
+
if (paused && queue.length < maxQueue) {
|
|
3779
|
+
paused = false;
|
|
3780
|
+
child.stdout?.resume();
|
|
3781
|
+
child.stderr?.resume();
|
|
3782
|
+
}
|
|
3783
|
+
};
|
|
3556
3784
|
child.stdout?.on("data", (c) => {
|
|
3557
3785
|
const s = c.toString();
|
|
3558
3786
|
if (stdout.length < max) stdout += s;
|
|
3559
3787
|
queue.push({ kind: "out", data: s });
|
|
3560
3788
|
wake();
|
|
3789
|
+
if (!paused && queue.length >= maxQueue) {
|
|
3790
|
+
paused = true;
|
|
3791
|
+
child.stdout?.pause();
|
|
3792
|
+
child.stderr?.pause();
|
|
3793
|
+
}
|
|
3561
3794
|
});
|
|
3562
3795
|
child.stderr?.on("data", (c) => {
|
|
3563
3796
|
const s = c.toString();
|
|
3564
3797
|
if (stderr.length < max) stderr += s;
|
|
3565
3798
|
queue.push({ kind: "err", data: s });
|
|
3566
3799
|
wake();
|
|
3800
|
+
if (!paused && queue.length >= maxQueue) {
|
|
3801
|
+
paused = true;
|
|
3802
|
+
child.stdout?.pause();
|
|
3803
|
+
child.stderr?.pause();
|
|
3804
|
+
}
|
|
3567
3805
|
});
|
|
3568
3806
|
child.on("error", (e) => {
|
|
3569
3807
|
error = e.message;
|
|
@@ -3583,6 +3821,7 @@ async function* spawnStream(opts) {
|
|
|
3583
3821
|
});
|
|
3584
3822
|
}
|
|
3585
3823
|
const chunk = queue.shift();
|
|
3824
|
+
resume();
|
|
3586
3825
|
if (chunk.kind === "close") {
|
|
3587
3826
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
3588
3827
|
break;
|
|
@@ -3592,14 +3831,14 @@ async function* spawnStream(opts) {
|
|
|
3592
3831
|
exitCode = 1;
|
|
3593
3832
|
continue;
|
|
3594
3833
|
}
|
|
3595
|
-
|
|
3596
|
-
if (
|
|
3597
|
-
yield { type: "partial_output", text:
|
|
3598
|
-
|
|
3834
|
+
pending2 += chunk.data;
|
|
3835
|
+
if (pending2.length >= flushAt) {
|
|
3836
|
+
yield { type: "partial_output", text: pending2 };
|
|
3837
|
+
pending2 = "";
|
|
3599
3838
|
}
|
|
3600
3839
|
}
|
|
3601
|
-
if (
|
|
3602
|
-
yield { type: "partial_output", text:
|
|
3840
|
+
if (pending2.length > 0) {
|
|
3841
|
+
yield { type: "partial_output", text: pending2 };
|
|
3603
3842
|
}
|
|
3604
3843
|
return {
|
|
3605
3844
|
stdout,
|
|
@@ -4035,8 +4274,6 @@ function parseResult(runner, result, duration) {
|
|
|
4035
4274
|
truncated: result.truncated
|
|
4036
4275
|
};
|
|
4037
4276
|
}
|
|
4038
|
-
|
|
4039
|
-
// src/install.ts
|
|
4040
4277
|
var installTool = {
|
|
4041
4278
|
name: "install",
|
|
4042
4279
|
category: "Package Management",
|
|
@@ -4131,18 +4368,48 @@ var installTool = {
|
|
|
4131
4368
|
signal: opts.signal,
|
|
4132
4369
|
maxBytes: 1e5
|
|
4133
4370
|
});
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
dry_run: args.includes("--dry-run"),
|
|
4141
|
-
truncated: result.truncated
|
|
4142
|
-
}
|
|
4371
|
+
const output = {
|
|
4372
|
+
packages: pkgList,
|
|
4373
|
+
exit_code: result.exitCode,
|
|
4374
|
+
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
4375
|
+
dry_run: args.includes("--dry-run"),
|
|
4376
|
+
truncated: result.truncated
|
|
4143
4377
|
};
|
|
4378
|
+
const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
|
|
4379
|
+
if (isSuccess && pkgList.length > 0) {
|
|
4380
|
+
const trackerOpts = ctx.meta?.["packageTrackerOpts"];
|
|
4381
|
+
if (trackerOpts) {
|
|
4382
|
+
const manifestPath = resolveManifestPath(cwd, pkgManager);
|
|
4383
|
+
for (const pkg of pkgList) {
|
|
4384
|
+
try {
|
|
4385
|
+
await recordPackageAction(trackerOpts, {
|
|
4386
|
+
manifestPath,
|
|
4387
|
+
packageName: pkg,
|
|
4388
|
+
versionSpec: "latest",
|
|
4389
|
+
// exact version resolved by package manager at install time
|
|
4390
|
+
ecosystem: detectPackageEcosystem(manifestPath),
|
|
4391
|
+
agentId: ctx.agentId,
|
|
4392
|
+
agentName: ctx.agentName,
|
|
4393
|
+
sessionId: ctx.session.id
|
|
4394
|
+
});
|
|
4395
|
+
} catch {
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
yield { type: "final", output };
|
|
4144
4401
|
}
|
|
4145
4402
|
};
|
|
4403
|
+
function resolveManifestPath(cwd, pkgManager) {
|
|
4404
|
+
switch (pkgManager) {
|
|
4405
|
+
case "pnpm":
|
|
4406
|
+
case "yarn":
|
|
4407
|
+
case "npm":
|
|
4408
|
+
return join(cwd, "package.json");
|
|
4409
|
+
default:
|
|
4410
|
+
return join(cwd, "package.json");
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4146
4413
|
|
|
4147
4414
|
// src/audit.ts
|
|
4148
4415
|
var auditTool = {
|
|
@@ -4284,7 +4551,7 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
4284
4551
|
const MAX = 1e5;
|
|
4285
4552
|
const resolved = resolveWin32Command(manager);
|
|
4286
4553
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4287
|
-
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
4554
|
+
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
4288
4555
|
child.stdout?.on("data", (c) => {
|
|
4289
4556
|
if (stdout.length < MAX) stdout += c.toString();
|
|
4290
4557
|
});
|
|
@@ -4438,7 +4705,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
4438
4705
|
clearTimeout(timer);
|
|
4439
4706
|
resolve7(result);
|
|
4440
4707
|
};
|
|
4441
|
-
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
4708
|
+
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
4442
4709
|
const timer = setTimeout(() => {
|
|
4443
4710
|
child.kill("SIGTERM");
|
|
4444
4711
|
finish(empty());
|
|
@@ -5499,8 +5766,91 @@ ${mode.description}`
|
|
|
5499
5766
|
};
|
|
5500
5767
|
}
|
|
5501
5768
|
|
|
5769
|
+
// src/codebase-index/circuit-breaker.ts
|
|
5770
|
+
var CircuitOpenError = class extends Error {
|
|
5771
|
+
name = "CircuitOpenError";
|
|
5772
|
+
};
|
|
5773
|
+
var IndexTimeoutError = class extends Error {
|
|
5774
|
+
name = "IndexTimeoutError";
|
|
5775
|
+
};
|
|
5776
|
+
var LockError = class extends Error {
|
|
5777
|
+
name = "LockError";
|
|
5778
|
+
};
|
|
5779
|
+
var IndexCircuitBreaker = class {
|
|
5780
|
+
failureThreshold;
|
|
5781
|
+
cooldownMs;
|
|
5782
|
+
now;
|
|
5783
|
+
state = "closed";
|
|
5784
|
+
consecutiveFailures = 0;
|
|
5785
|
+
openedAt = 0;
|
|
5786
|
+
lastFailure = null;
|
|
5787
|
+
probeInFlight = false;
|
|
5788
|
+
constructor(opts = {}) {
|
|
5789
|
+
this.failureThreshold = opts.failureThreshold ?? 3;
|
|
5790
|
+
this.cooldownMs = opts.cooldownMs ?? 6e4;
|
|
5791
|
+
this.now = opts.now ?? Date.now;
|
|
5792
|
+
}
|
|
5793
|
+
/**
|
|
5794
|
+
* True when a run may proceed. An open circuit transitions to half-open once
|
|
5795
|
+
* the cooldown has elapsed, admitting exactly one probe; further requests
|
|
5796
|
+
* are rejected until that probe settles via recordSuccess/recordFailure.
|
|
5797
|
+
*/
|
|
5798
|
+
allowRequest() {
|
|
5799
|
+
if (this.state === "closed") return true;
|
|
5800
|
+
if (this.state === "open") {
|
|
5801
|
+
if (this.now() - this.openedAt < this.cooldownMs) return false;
|
|
5802
|
+
this.state = "half-open";
|
|
5803
|
+
this.probeInFlight = true;
|
|
5804
|
+
return true;
|
|
5805
|
+
}
|
|
5806
|
+
if (this.probeInFlight) return false;
|
|
5807
|
+
this.probeInFlight = true;
|
|
5808
|
+
return true;
|
|
5809
|
+
}
|
|
5810
|
+
recordSuccess() {
|
|
5811
|
+
this.state = "closed";
|
|
5812
|
+
this.consecutiveFailures = 0;
|
|
5813
|
+
this.lastFailure = null;
|
|
5814
|
+
this.probeInFlight = false;
|
|
5815
|
+
}
|
|
5816
|
+
recordFailure(err) {
|
|
5817
|
+
if (err instanceof LockError) {
|
|
5818
|
+
this.lastFailure = `[transient/lock] ${err.message}`;
|
|
5819
|
+
this.probeInFlight = false;
|
|
5820
|
+
return;
|
|
5821
|
+
}
|
|
5822
|
+
this.lastFailure = err instanceof Error ? err.message : String(err);
|
|
5823
|
+
this.probeInFlight = false;
|
|
5824
|
+
this.consecutiveFailures++;
|
|
5825
|
+
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
5826
|
+
this.state = "open";
|
|
5827
|
+
this.openedAt = this.now();
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
/** Force-close the circuit (manual recovery: `/codebase-reindex`). */
|
|
5831
|
+
reset() {
|
|
5832
|
+
this.state = "closed";
|
|
5833
|
+
this.consecutiveFailures = 0;
|
|
5834
|
+
this.lastFailure = null;
|
|
5835
|
+
this.probeInFlight = false;
|
|
5836
|
+
this.openedAt = 0;
|
|
5837
|
+
}
|
|
5838
|
+
snapshot() {
|
|
5839
|
+
return {
|
|
5840
|
+
state: this.state,
|
|
5841
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
5842
|
+
lastFailure: this.lastFailure,
|
|
5843
|
+
cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
|
|
5844
|
+
};
|
|
5845
|
+
}
|
|
5846
|
+
};
|
|
5847
|
+
var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
5848
|
+
function resetIndexCircuitBreaker() {
|
|
5849
|
+
indexCircuitBreaker.reset();
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5502
5852
|
// src/codebase-index/schema.ts
|
|
5503
|
-
var SCHEMA_VERSION =
|
|
5853
|
+
var SCHEMA_VERSION = 2;
|
|
5504
5854
|
|
|
5505
5855
|
// src/codebase-index/lsp-kind.ts
|
|
5506
5856
|
function lspKindToInternalKind(k) {
|
|
@@ -5535,50 +5885,202 @@ function lspKindToInternalKind(k) {
|
|
|
5535
5885
|
}
|
|
5536
5886
|
}
|
|
5537
5887
|
|
|
5538
|
-
// src/codebase-index/
|
|
5539
|
-
var
|
|
5540
|
-
|
|
5541
|
-
|
|
5888
|
+
// src/codebase-index/bm25.ts
|
|
5889
|
+
var K1 = 1.5;
|
|
5890
|
+
var B = 0.75;
|
|
5891
|
+
function tokenise(text) {
|
|
5892
|
+
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
5893
|
+
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
5542
5894
|
}
|
|
5543
|
-
function
|
|
5544
|
-
|
|
5545
|
-
return typeof v === "string" ? v : void 0;
|
|
5895
|
+
function splitName(name) {
|
|
5896
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
5546
5897
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
if (warningSilenced) return;
|
|
5550
|
-
warningSilenced = true;
|
|
5551
|
-
const original = process.emitWarning.bind(process);
|
|
5552
|
-
process.emitWarning = ((warning, ...rest) => {
|
|
5553
|
-
const msg = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
5554
|
-
const name = typeof warning === "string" ? String(rest[0] ?? "") : warning?.name ?? "";
|
|
5555
|
-
if (/sqlite/i.test(msg) && /experimental/i.test(`${name} ${msg}`)) return;
|
|
5556
|
-
original(warning, ...rest);
|
|
5557
|
-
});
|
|
5898
|
+
function buildIndexableText(name, signature, docComment) {
|
|
5899
|
+
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
5558
5900
|
}
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5901
|
+
function buildBm25Index(docs) {
|
|
5902
|
+
const documents = docs.map((d) => {
|
|
5903
|
+
const tokens = tokenise(d.text);
|
|
5904
|
+
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
5905
|
+
});
|
|
5906
|
+
const df = {};
|
|
5907
|
+
for (const doc of documents) {
|
|
5908
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5909
|
+
for (const t of doc.tokens) {
|
|
5910
|
+
if (!seen.has(t)) {
|
|
5911
|
+
df[t] = (df[t] ?? 0) + 1;
|
|
5912
|
+
seen.add(t);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5570
5915
|
}
|
|
5571
|
-
|
|
5916
|
+
const N = documents.length;
|
|
5917
|
+
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
5918
|
+
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
5919
|
+
return new Bm25Index(documents, df, N, avgLen);
|
|
5572
5920
|
}
|
|
5573
|
-
var
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
this.
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5921
|
+
var Bm25Index = class {
|
|
5922
|
+
constructor(documents, df, N, avgLen) {
|
|
5923
|
+
this.documents = documents;
|
|
5924
|
+
this.df = df;
|
|
5925
|
+
this.N = N;
|
|
5926
|
+
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
5927
|
+
}
|
|
5928
|
+
documents;
|
|
5929
|
+
df;
|
|
5930
|
+
N;
|
|
5931
|
+
safeAvgLen;
|
|
5932
|
+
score(query2, filter) {
|
|
5933
|
+
const qTokens = tokenise(query2);
|
|
5934
|
+
if (qTokens.length === 0) return [];
|
|
5935
|
+
const results = [];
|
|
5936
|
+
for (const doc of this.documents) {
|
|
5937
|
+
if (filter && !filter(doc.id)) continue;
|
|
5938
|
+
let docScore = 0;
|
|
5939
|
+
for (const qTerm of qTokens) {
|
|
5940
|
+
let tf = 0;
|
|
5941
|
+
for (const t of doc.tokens) {
|
|
5942
|
+
if (t === qTerm) tf++;
|
|
5943
|
+
}
|
|
5944
|
+
if (tf === 0) continue;
|
|
5945
|
+
const dfVal = this.df[qTerm] ?? 0;
|
|
5946
|
+
if (dfVal === 0) continue;
|
|
5947
|
+
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
5948
|
+
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
5949
|
+
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
5950
|
+
docScore += idf * tfComponent;
|
|
5951
|
+
}
|
|
5952
|
+
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
5953
|
+
}
|
|
5954
|
+
return results;
|
|
5955
|
+
}
|
|
5956
|
+
getDoc(id) {
|
|
5957
|
+
return this.documents.find((d) => d.id === id);
|
|
5958
|
+
}
|
|
5959
|
+
extractSnippet(docId, queryTokens, radius = 40) {
|
|
5960
|
+
const doc = this.getDoc(docId);
|
|
5961
|
+
if (!doc) return "";
|
|
5962
|
+
for (const tok of queryTokens) {
|
|
5963
|
+
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
5964
|
+
if (idx !== -1) {
|
|
5965
|
+
const start = Math.max(0, idx - radius);
|
|
5966
|
+
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
5967
|
+
const excerpt = doc.raw.slice(start, end);
|
|
5968
|
+
const ellipsis = "\u2026";
|
|
5969
|
+
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
5973
|
+
}
|
|
5974
|
+
};
|
|
5975
|
+
|
|
5976
|
+
// src/codebase-index/writer.ts
|
|
5977
|
+
var DB_FILE = "index.db";
|
|
5978
|
+
function resolveIndexDir(projectRoot, override) {
|
|
5979
|
+
return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
|
|
5980
|
+
}
|
|
5981
|
+
function codebaseIndexDirOverride(ctx) {
|
|
5982
|
+
const v = ctx.meta?.["codebaseIndexDir"];
|
|
5983
|
+
return typeof v === "string" ? v : void 0;
|
|
5984
|
+
}
|
|
5985
|
+
var warningSilenced = false;
|
|
5986
|
+
function silenceSqliteExperimentalWarning() {
|
|
5987
|
+
if (warningSilenced) return;
|
|
5988
|
+
warningSilenced = true;
|
|
5989
|
+
const original = process.emitWarning.bind(process);
|
|
5990
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
5991
|
+
const msg = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
5992
|
+
const name = typeof warning === "string" ? String(rest[0] ?? "") : warning?.name ?? "";
|
|
5993
|
+
if (/sqlite/i.test(msg) && /experimental/i.test(`${name} ${msg}`)) return;
|
|
5994
|
+
original(warning, ...rest);
|
|
5995
|
+
});
|
|
5996
|
+
}
|
|
5997
|
+
var DatabaseSyncCtor;
|
|
5998
|
+
function loadDatabaseSync() {
|
|
5999
|
+
if (DatabaseSyncCtor) return DatabaseSyncCtor;
|
|
6000
|
+
silenceSqliteExperimentalWarning();
|
|
6001
|
+
try {
|
|
6002
|
+
const req = createRequire(import.meta.url);
|
|
6003
|
+
DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
|
|
6004
|
+
} catch (err) {
|
|
6005
|
+
throw new Error(
|
|
6006
|
+
`The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${err instanceof Error ? err.message : String(err)}`
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
return DatabaseSyncCtor;
|
|
6010
|
+
}
|
|
6011
|
+
var MAX_LOCK_RETRIES = 3;
|
|
6012
|
+
var LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
6013
|
+
var LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
6014
|
+
function isLockError(err) {
|
|
6015
|
+
if (!(err instanceof Error)) return false;
|
|
6016
|
+
const e = err;
|
|
6017
|
+
const code = e.code ?? e.sqliteCode;
|
|
6018
|
+
if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
|
|
6019
|
+
if (typeof code === "number" && (code === 5 || code === 6)) return true;
|
|
6020
|
+
if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
|
|
6021
|
+
return false;
|
|
6022
|
+
}
|
|
6023
|
+
function sleepSync(ms) {
|
|
6024
|
+
try {
|
|
6025
|
+
const sab = new SharedArrayBuffer(4);
|
|
6026
|
+
const view = new Int32Array(sab);
|
|
6027
|
+
Atomics.wait(view, 0, 0, ms);
|
|
6028
|
+
} catch {
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
var IndexStore = class {
|
|
6032
|
+
db;
|
|
6033
|
+
/** Absolute path to this project's index directory. */
|
|
6034
|
+
indexDir;
|
|
6035
|
+
/**
|
|
6036
|
+
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
6037
|
+
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
6038
|
+
*/
|
|
6039
|
+
ftsAvailable = false;
|
|
6040
|
+
/**
|
|
6041
|
+
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
6042
|
+
*
|
|
6043
|
+
* When another wstack process is holding the write lock the statement first
|
|
6044
|
+
* waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
|
|
6045
|
+
* that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
|
|
6046
|
+
* giving the competing writer time to finish and release the lock.
|
|
6047
|
+
*
|
|
6048
|
+
* @param fn The write operation to execute. Can return a value which is
|
|
6049
|
+
* returned to the caller on success.
|
|
6050
|
+
* @throws {@link LockError} when all retries are exhausted on a lock conflict
|
|
6051
|
+
* (non-lock errors always propagate on the first attempt).
|
|
6052
|
+
*/
|
|
6053
|
+
runWithRetry(fn) {
|
|
6054
|
+
let lastError;
|
|
6055
|
+
for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
6056
|
+
try {
|
|
6057
|
+
return fn();
|
|
6058
|
+
} catch (err) {
|
|
6059
|
+
lastError = err;
|
|
6060
|
+
if (!isLockError(err)) throw err;
|
|
6061
|
+
if (attempt === MAX_LOCK_RETRIES) {
|
|
6062
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
6063
|
+
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
6064
|
+
}
|
|
6065
|
+
const delay = Math.min(
|
|
6066
|
+
LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
6067
|
+
LOCK_RETRY_MAX_DELAY_MS
|
|
6068
|
+
);
|
|
6069
|
+
sleepSync(delay);
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
throw lastError;
|
|
6073
|
+
}
|
|
6074
|
+
constructor(projectRoot, opts = {}) {
|
|
6075
|
+
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
6076
|
+
fs7.mkdirSync(this.indexDir, { recursive: true });
|
|
6077
|
+
const Database = loadDatabaseSync();
|
|
6078
|
+
this.db = new Database(path.join(this.indexDir, DB_FILE));
|
|
6079
|
+
try {
|
|
6080
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
6081
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
6082
|
+
} catch {
|
|
6083
|
+
}
|
|
5582
6084
|
this.initSchema();
|
|
5583
6085
|
}
|
|
5584
6086
|
initSchema() {
|
|
@@ -5587,6 +6089,21 @@ var IndexStore = class {
|
|
|
5587
6089
|
key TEXT PRIMARY KEY,
|
|
5588
6090
|
value TEXT NOT NULL
|
|
5589
6091
|
);
|
|
6092
|
+
`);
|
|
6093
|
+
const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
6094
|
+
const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
|
|
6095
|
+
if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
|
|
6096
|
+
this.db.exec(`
|
|
6097
|
+
DROP TABLE IF EXISTS symbols;
|
|
6098
|
+
DROP TABLE IF EXISTS files;
|
|
6099
|
+
DROP TABLE IF EXISTS refs;
|
|
6100
|
+
`);
|
|
6101
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
6102
|
+
this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
|
|
6103
|
+
} else if (storedVersion === null) {
|
|
6104
|
+
this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
|
|
6105
|
+
}
|
|
6106
|
+
this.db.exec(`
|
|
5590
6107
|
CREATE TABLE IF NOT EXISTS files (
|
|
5591
6108
|
file TEXT PRIMARY KEY,
|
|
5592
6109
|
lang TEXT NOT NULL,
|
|
@@ -5627,53 +6144,76 @@ var IndexStore = class {
|
|
|
5627
6144
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
|
|
5628
6145
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
5629
6146
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
this.
|
|
6147
|
+
try {
|
|
6148
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
|
|
6149
|
+
this.ftsAvailable = true;
|
|
6150
|
+
} catch {
|
|
6151
|
+
this.ftsAvailable = false;
|
|
5633
6152
|
}
|
|
5634
6153
|
}
|
|
5635
6154
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
5636
6155
|
insertSymbols(symbols, nextId) {
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
let id = nextId;
|
|
5642
|
-
for (const s of symbols) {
|
|
5643
|
-
stmt.run(
|
|
5644
|
-
id++,
|
|
5645
|
-
s.lang,
|
|
5646
|
-
s.kind,
|
|
5647
|
-
s.name,
|
|
5648
|
-
s.file,
|
|
5649
|
-
s.line,
|
|
5650
|
-
s.col,
|
|
5651
|
-
s.signature,
|
|
5652
|
-
s.docComment,
|
|
5653
|
-
s.scope,
|
|
5654
|
-
s.text,
|
|
5655
|
-
s.file
|
|
6156
|
+
return this.runWithRetry(() => {
|
|
6157
|
+
const stmt = this.db.prepare(
|
|
6158
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
6159
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5656
6160
|
);
|
|
5657
|
-
|
|
5658
|
-
|
|
6161
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
6162
|
+
let id = nextId;
|
|
6163
|
+
for (const s of symbols) {
|
|
6164
|
+
stmt.run(
|
|
6165
|
+
id,
|
|
6166
|
+
s.lang,
|
|
6167
|
+
s.kind,
|
|
6168
|
+
s.name,
|
|
6169
|
+
s.file,
|
|
6170
|
+
s.line,
|
|
6171
|
+
s.col,
|
|
6172
|
+
s.signature,
|
|
6173
|
+
s.docComment,
|
|
6174
|
+
s.scope,
|
|
6175
|
+
s.text,
|
|
6176
|
+
s.file
|
|
6177
|
+
);
|
|
6178
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
6179
|
+
id++;
|
|
6180
|
+
}
|
|
6181
|
+
return id;
|
|
6182
|
+
});
|
|
5659
6183
|
}
|
|
5660
6184
|
deleteSymbolsForFile(file) {
|
|
5661
|
-
this.
|
|
6185
|
+
this.runWithRetry(() => {
|
|
6186
|
+
if (this.ftsAvailable) {
|
|
6187
|
+
this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
6188
|
+
}
|
|
6189
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
6190
|
+
});
|
|
5662
6191
|
}
|
|
6192
|
+
/**
|
|
6193
|
+
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
6194
|
+
* when a source file disappears between index runs — previously this only
|
|
6195
|
+
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
6196
|
+
*/
|
|
5663
6197
|
deleteFile(file) {
|
|
5664
|
-
this.
|
|
6198
|
+
this.runWithRetry(() => {
|
|
6199
|
+
this.deleteRefsForFile(file);
|
|
6200
|
+
this.deleteSymbolsForFile(file);
|
|
6201
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
6202
|
+
});
|
|
5665
6203
|
}
|
|
5666
6204
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
5667
6205
|
upsertFile(meta) {
|
|
5668
|
-
this.
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
6206
|
+
this.runWithRetry(() => {
|
|
6207
|
+
this.db.prepare(
|
|
6208
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
6209
|
+
VALUES (?, ?, ?, ?, ?)
|
|
6210
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
6211
|
+
lang = excluded.lang,
|
|
6212
|
+
mtime_ms = excluded.mtime_ms,
|
|
6213
|
+
symbol_count = excluded.symbol_count,
|
|
6214
|
+
last_indexed = excluded.last_indexed`
|
|
6215
|
+
).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
|
|
6216
|
+
});
|
|
5677
6217
|
}
|
|
5678
6218
|
getFileMeta(file) {
|
|
5679
6219
|
const rows = this.db.prepare(
|
|
@@ -5740,6 +6280,94 @@ var IndexStore = class {
|
|
|
5740
6280
|
lspKind: filter?.lspKind
|
|
5741
6281
|
}));
|
|
5742
6282
|
}
|
|
6283
|
+
/**
|
|
6284
|
+
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
6285
|
+
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
6286
|
+
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
6287
|
+
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
6288
|
+
*
|
|
6289
|
+
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
6290
|
+
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
6291
|
+
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
6292
|
+
*/
|
|
6293
|
+
searchRanked(query2, filter, limit) {
|
|
6294
|
+
const tokens = tokenise(query2);
|
|
6295
|
+
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
6296
|
+
return this.searchRankedFallback(query2, filter, limit);
|
|
6297
|
+
}
|
|
6298
|
+
let effectiveKind = filter?.kind;
|
|
6299
|
+
if (filter?.lspKind !== void 0) {
|
|
6300
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
6301
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
6302
|
+
effectiveKind = mapped;
|
|
6303
|
+
}
|
|
6304
|
+
const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
|
|
6305
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
6306
|
+
const values = [match];
|
|
6307
|
+
if (effectiveKind) {
|
|
6308
|
+
conditions.push("s.kind = ?");
|
|
6309
|
+
values.push(effectiveKind);
|
|
6310
|
+
}
|
|
6311
|
+
if (filter?.lang) {
|
|
6312
|
+
conditions.push("s.lang = ?");
|
|
6313
|
+
values.push(filter.lang);
|
|
6314
|
+
}
|
|
6315
|
+
if (filter?.file) {
|
|
6316
|
+
conditions.push("s.file LIKE ?");
|
|
6317
|
+
values.push(`%${filter.file}%`);
|
|
6318
|
+
}
|
|
6319
|
+
const where = conditions.join(" AND ");
|
|
6320
|
+
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);
|
|
6321
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
6322
|
+
if (total === 0) return { results: [], total: 0 };
|
|
6323
|
+
const rows = this.db.prepare(
|
|
6324
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
6325
|
+
-bm25(symbols_fts) AS score,
|
|
6326
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
6327
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
6328
|
+
WHERE ${where}
|
|
6329
|
+
ORDER BY bm25(symbols_fts)
|
|
6330
|
+
LIMIT ?`
|
|
6331
|
+
).all(...values, limit);
|
|
6332
|
+
return {
|
|
6333
|
+
results: rows.map((r) => ({
|
|
6334
|
+
id: r.id,
|
|
6335
|
+
lang: r.lang,
|
|
6336
|
+
kind: r.kind,
|
|
6337
|
+
name: r.name,
|
|
6338
|
+
file: r.file,
|
|
6339
|
+
line: r.line,
|
|
6340
|
+
col: r.col,
|
|
6341
|
+
signature: r.signature,
|
|
6342
|
+
docComment: r.doc_comment,
|
|
6343
|
+
// bm25() is negative-is-better; negate so callers keep "higher is
|
|
6344
|
+
// better" and clamp so a match never reports a zero score.
|
|
6345
|
+
score: Math.max(1e-4, r.score),
|
|
6346
|
+
snippet: r.snippet,
|
|
6347
|
+
lspKind: filter?.lspKind
|
|
6348
|
+
})),
|
|
6349
|
+
total
|
|
6350
|
+
};
|
|
6351
|
+
}
|
|
6352
|
+
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
6353
|
+
searchRankedFallback(query2, filter, limit) {
|
|
6354
|
+
const candidates = this.search(query2, filter);
|
|
6355
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
6356
|
+
if (!query2.trim()) {
|
|
6357
|
+
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
6358
|
+
}
|
|
6359
|
+
const bm25 = buildBm25Index(
|
|
6360
|
+
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
6361
|
+
);
|
|
6362
|
+
const scored = bm25.score(query2, (id) => candidates.some((c) => c.id === id));
|
|
6363
|
+
scored.sort((a, b) => b.score - a.score);
|
|
6364
|
+
const qTokens = tokenise(query2);
|
|
6365
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
6366
|
+
const c = expectDefined(candidates.find((cand) => cand.id === id));
|
|
6367
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
6368
|
+
});
|
|
6369
|
+
return { results, total: candidates.length };
|
|
6370
|
+
}
|
|
5743
6371
|
getAllIndexable() {
|
|
5744
6372
|
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
5745
6373
|
({ id, text }) => ({ id, text })
|
|
@@ -5789,14 +6417,19 @@ var IndexStore = class {
|
|
|
5789
6417
|
};
|
|
5790
6418
|
}
|
|
5791
6419
|
setLastIndexed(ts2) {
|
|
5792
|
-
this.
|
|
5793
|
-
|
|
5794
|
-
|
|
6420
|
+
this.runWithRetry(() => {
|
|
6421
|
+
this.db.prepare(
|
|
6422
|
+
"INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
|
|
6423
|
+
).run(String(ts2));
|
|
6424
|
+
});
|
|
5795
6425
|
}
|
|
5796
6426
|
clearAll() {
|
|
5797
|
-
this.
|
|
5798
|
-
|
|
5799
|
-
|
|
6427
|
+
this.runWithRetry(() => {
|
|
6428
|
+
this.db.exec("DELETE FROM symbols");
|
|
6429
|
+
this.db.exec("DELETE FROM files");
|
|
6430
|
+
this.db.exec("DELETE FROM refs");
|
|
6431
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
6432
|
+
});
|
|
5800
6433
|
}
|
|
5801
6434
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
5802
6435
|
/**
|
|
@@ -5804,46 +6437,52 @@ var IndexStore = class {
|
|
|
5804
6437
|
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
5805
6438
|
*/
|
|
5806
6439
|
insertRefs(fromId, refs) {
|
|
5807
|
-
this.
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
6440
|
+
this.runWithRetry(() => {
|
|
6441
|
+
this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
6442
|
+
if (refs.length === 0) return;
|
|
6443
|
+
const stmt = this.db.prepare(
|
|
6444
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
6445
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
6446
|
+
);
|
|
6447
|
+
for (const ref of refs) {
|
|
6448
|
+
stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
6449
|
+
}
|
|
6450
|
+
});
|
|
5816
6451
|
}
|
|
5817
6452
|
/**
|
|
5818
6453
|
* Delete all refs whose source symbols are in a given file.
|
|
5819
6454
|
* Used when re-indexing a file to clear stale refs.
|
|
5820
6455
|
*/
|
|
5821
6456
|
deleteRefsForFile(file) {
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
6457
|
+
this.runWithRetry(() => {
|
|
6458
|
+
const ids = this.db.prepare(
|
|
6459
|
+
"SELECT id FROM symbols WHERE file = ?"
|
|
6460
|
+
).all(file);
|
|
6461
|
+
if (!ids.length) return;
|
|
6462
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
6463
|
+
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
6464
|
+
});
|
|
5828
6465
|
}
|
|
5829
6466
|
/**
|
|
5830
6467
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
5831
6468
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
5832
6469
|
*/
|
|
5833
6470
|
resolveRefs() {
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
6471
|
+
return this.runWithRetry(() => {
|
|
6472
|
+
const unresolved = this.db.prepare(
|
|
6473
|
+
"SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
|
|
6474
|
+
).all();
|
|
6475
|
+
let resolved = 0;
|
|
6476
|
+
for (const row of unresolved) {
|
|
6477
|
+
const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
|
|
6478
|
+
const first = target[0];
|
|
6479
|
+
if (first) {
|
|
6480
|
+
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
6481
|
+
resolved++;
|
|
6482
|
+
}
|
|
5844
6483
|
}
|
|
5845
|
-
|
|
5846
|
-
|
|
6484
|
+
return resolved;
|
|
6485
|
+
});
|
|
5847
6486
|
}
|
|
5848
6487
|
/**
|
|
5849
6488
|
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
@@ -6604,7 +7243,7 @@ function parseSymbols4(opts) {
|
|
|
6604
7243
|
}
|
|
6605
7244
|
function checkNativeParser() {
|
|
6606
7245
|
try {
|
|
6607
|
-
execFileSync("rustc", ["--version"], { stdio: "pipe" });
|
|
7246
|
+
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
6608
7247
|
const toolsDir = path.join(process.cwd(), "tools");
|
|
6609
7248
|
try {
|
|
6610
7249
|
execFileSync(
|
|
@@ -6617,7 +7256,7 @@ function checkNativeParser() {
|
|
|
6617
7256
|
"--manifest-path",
|
|
6618
7257
|
path.join(toolsDir, "Cargo.toml")
|
|
6619
7258
|
],
|
|
6620
|
-
{ stdio: "pipe" }
|
|
7259
|
+
{ stdio: "pipe", windowsHide: true }
|
|
6621
7260
|
);
|
|
6622
7261
|
return true;
|
|
6623
7262
|
} catch {
|
|
@@ -6640,7 +7279,8 @@ function tryNativeParse(file, content) {
|
|
|
6640
7279
|
cwd: process.cwd(),
|
|
6641
7280
|
encoding: "utf8",
|
|
6642
7281
|
timeout: 15e3,
|
|
6643
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
7282
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7283
|
+
windowsHide: true
|
|
6644
7284
|
}
|
|
6645
7285
|
);
|
|
6646
7286
|
if (result.status === 0 && result.stdout) {
|
|
@@ -7054,10 +7694,6 @@ function isScalar(value) {
|
|
|
7054
7694
|
if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
|
|
7055
7695
|
return false;
|
|
7056
7696
|
}
|
|
7057
|
-
function truncate(s, max) {
|
|
7058
|
-
if (s.length <= max) return s;
|
|
7059
|
-
return s.slice(0, max) + "...";
|
|
7060
|
-
}
|
|
7061
7697
|
function makeSymbol2(opts) {
|
|
7062
7698
|
return {
|
|
7063
7699
|
id: 0,
|
|
@@ -7124,140 +7760,20 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
7124
7760
|
return compileGitignore(lines);
|
|
7125
7761
|
}
|
|
7126
7762
|
|
|
7127
|
-
// src/codebase-index/
|
|
7128
|
-
var
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
var _totalFiles = 0;
|
|
7132
|
-
var _lastError = null;
|
|
7133
|
-
function isIndexReady() {
|
|
7134
|
-
return _ready;
|
|
7135
|
-
}
|
|
7136
|
-
function setIndexReady() {
|
|
7137
|
-
_ready = true;
|
|
7138
|
-
}
|
|
7139
|
-
function isIndexing() {
|
|
7140
|
-
return _indexing;
|
|
7141
|
-
}
|
|
7142
|
-
function getIndexState() {
|
|
7143
|
-
return {
|
|
7144
|
-
ready: _ready,
|
|
7145
|
-
indexing: _indexing,
|
|
7146
|
-
currentFile: _currentFile,
|
|
7147
|
-
totalFiles: _totalFiles,
|
|
7148
|
-
lastError: _lastError
|
|
7149
|
-
};
|
|
7150
|
-
}
|
|
7151
|
-
var _listeners = [];
|
|
7152
|
-
function onIndexStateChange(listener) {
|
|
7153
|
-
_listeners.push(listener);
|
|
7154
|
-
return () => {
|
|
7155
|
-
_listeners = _listeners.filter((l) => l !== listener);
|
|
7156
|
-
};
|
|
7157
|
-
}
|
|
7158
|
-
function emitState() {
|
|
7159
|
-
const state = getIndexState();
|
|
7160
|
-
for (const l of _listeners) l(state);
|
|
7161
|
-
}
|
|
7162
|
-
function _setIndexProgress(current, total) {
|
|
7163
|
-
_currentFile = current;
|
|
7164
|
-
_totalFiles = total;
|
|
7165
|
-
emitState();
|
|
7166
|
-
}
|
|
7167
|
-
function stubCtx(projectRoot) {
|
|
7168
|
-
return {
|
|
7169
|
-
projectRoot,
|
|
7170
|
-
cwd: projectRoot,
|
|
7171
|
-
messages: [],
|
|
7172
|
-
todos: [],
|
|
7173
|
-
readFiles: /* @__PURE__ */ new Set(),
|
|
7174
|
-
fileMtimes: /* @__PURE__ */ new Map()
|
|
7175
|
-
};
|
|
7763
|
+
// src/codebase-index/indexer.ts
|
|
7764
|
+
var YIELD_EVERY_N = 50;
|
|
7765
|
+
function yieldEventLoop() {
|
|
7766
|
+
return new Promise((resolve7) => setImmediate(resolve7));
|
|
7176
7767
|
}
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
() => void 0
|
|
7768
|
+
function throwIfAborted(signal) {
|
|
7769
|
+
if (!signal?.aborted) return;
|
|
7770
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
7771
|
+
throw new Error(
|
|
7772
|
+
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
7183
7773
|
);
|
|
7184
|
-
return run;
|
|
7185
|
-
}
|
|
7186
|
-
var DEFAULT_DEBOUNCE_MS = 400;
|
|
7187
|
-
var debounceTimers = /* @__PURE__ */ new Map();
|
|
7188
|
-
function debounceKey(indexDir, file) {
|
|
7189
|
-
return `${indexDir ?? ""}|${file}`;
|
|
7190
7774
|
}
|
|
7191
|
-
function
|
|
7192
|
-
return
|
|
7193
|
-
}
|
|
7194
|
-
async function runStartupIndex(opts) {
|
|
7195
|
-
_indexing = true;
|
|
7196
|
-
_currentFile = 0;
|
|
7197
|
-
_totalFiles = 0;
|
|
7198
|
-
_lastError = null;
|
|
7199
|
-
emitState();
|
|
7200
|
-
try {
|
|
7201
|
-
const result = await withMutex(
|
|
7202
|
-
() => runIndexer(stubCtx(opts.projectRoot), {
|
|
7203
|
-
projectRoot: opts.projectRoot,
|
|
7204
|
-
indexDir: opts.indexDir,
|
|
7205
|
-
force: opts.force,
|
|
7206
|
-
signal: opts.signal
|
|
7207
|
-
})
|
|
7208
|
-
);
|
|
7209
|
-
_ready = true;
|
|
7210
|
-
return result;
|
|
7211
|
-
} catch (err) {
|
|
7212
|
-
_lastError = err instanceof Error ? err.message : String(err);
|
|
7213
|
-
_ready = true;
|
|
7214
|
-
throw err;
|
|
7215
|
-
} finally {
|
|
7216
|
-
_indexing = false;
|
|
7217
|
-
emitState();
|
|
7218
|
-
}
|
|
7219
|
-
}
|
|
7220
|
-
function enqueueReindex(opts) {
|
|
7221
|
-
const files = opts.files.filter(isIndexableFile);
|
|
7222
|
-
if (files.length === 0) return;
|
|
7223
|
-
const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
7224
|
-
for (const file of files) {
|
|
7225
|
-
const key = debounceKey(opts.indexDir, file);
|
|
7226
|
-
const existing = debounceTimers.get(key);
|
|
7227
|
-
if (existing) clearTimeout(existing);
|
|
7228
|
-
const timer = setTimeout(() => {
|
|
7229
|
-
debounceTimers.delete(key);
|
|
7230
|
-
void withMutex(
|
|
7231
|
-
() => runIndexer(stubCtx(opts.projectRoot), {
|
|
7232
|
-
projectRoot: opts.projectRoot,
|
|
7233
|
-
files: [file],
|
|
7234
|
-
indexDir: opts.indexDir
|
|
7235
|
-
})
|
|
7236
|
-
).catch((err) => opts.onError?.(err));
|
|
7237
|
-
}, ms);
|
|
7238
|
-
timer.unref?.();
|
|
7239
|
-
debounceTimers.set(key, timer);
|
|
7240
|
-
}
|
|
7241
|
-
}
|
|
7242
|
-
function cancelPendingReindexes() {
|
|
7243
|
-
for (const t of debounceTimers.values()) clearTimeout(t);
|
|
7244
|
-
debounceTimers.clear();
|
|
7245
|
-
}
|
|
7246
|
-
|
|
7247
|
-
// src/codebase-index/indexer.ts
|
|
7248
|
-
var YIELD_EVERY_N = 50;
|
|
7249
|
-
function yieldEventLoop() {
|
|
7250
|
-
return new Promise((resolve7) => setImmediate(resolve7));
|
|
7251
|
-
}
|
|
7252
|
-
function throwIfAborted(signal) {
|
|
7253
|
-
if (!signal?.aborted) return;
|
|
7254
|
-
if (signal.reason instanceof Error) throw signal.reason;
|
|
7255
|
-
throw new Error(
|
|
7256
|
-
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
7257
|
-
);
|
|
7258
|
-
}
|
|
7259
|
-
function isAbortError(err) {
|
|
7260
|
-
return err instanceof DOMException && err.name === "AbortError";
|
|
7775
|
+
function isAbortError(err) {
|
|
7776
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
7261
7777
|
}
|
|
7262
7778
|
var DEFAULT_IGNORE5 = [
|
|
7263
7779
|
"node_modules",
|
|
@@ -7343,8 +7859,18 @@ async function parseFile(file, content, lang) {
|
|
|
7343
7859
|
}
|
|
7344
7860
|
}
|
|
7345
7861
|
async function runIndexer(_ctx, opts) {
|
|
7346
|
-
const
|
|
7347
|
-
|
|
7862
|
+
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
7863
|
+
try {
|
|
7864
|
+
return await runIndexerWithStore(store, opts);
|
|
7865
|
+
} finally {
|
|
7866
|
+
try {
|
|
7867
|
+
store.close();
|
|
7868
|
+
} catch {
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
}
|
|
7872
|
+
async function runIndexerWithStore(store, opts) {
|
|
7873
|
+
const { projectRoot, force = false, langs, ignore = [], signal } = opts;
|
|
7348
7874
|
const startMs = Date.now();
|
|
7349
7875
|
const errors = [];
|
|
7350
7876
|
const langStats = {};
|
|
@@ -7371,7 +7897,7 @@ async function runIndexer(_ctx, opts) {
|
|
|
7371
7897
|
}
|
|
7372
7898
|
for (let fi = 0; fi < files.length; fi++) {
|
|
7373
7899
|
const file = expectDefined(files[fi]);
|
|
7374
|
-
|
|
7900
|
+
opts.onProgress?.(fi + 1, files.length);
|
|
7375
7901
|
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
7376
7902
|
await yieldEventLoop();
|
|
7377
7903
|
throwIfAborted(signal);
|
|
@@ -7457,7 +7983,6 @@ async function runIndexer(_ctx, opts) {
|
|
|
7457
7983
|
}
|
|
7458
7984
|
const durationMs = Date.now() - startMs;
|
|
7459
7985
|
store.setLastIndexed(Date.now());
|
|
7460
|
-
store.close();
|
|
7461
7986
|
return {
|
|
7462
7987
|
filesIndexed,
|
|
7463
7988
|
symbolsIndexed,
|
|
@@ -7467,6 +7992,349 @@ async function runIndexer(_ctx, opts) {
|
|
|
7467
7992
|
};
|
|
7468
7993
|
}
|
|
7469
7994
|
|
|
7995
|
+
// src/codebase-index/index-service.ts
|
|
7996
|
+
function stubCtx(projectRoot) {
|
|
7997
|
+
return {
|
|
7998
|
+
projectRoot,
|
|
7999
|
+
cwd: projectRoot,
|
|
8000
|
+
messages: [],
|
|
8001
|
+
todos: [],
|
|
8002
|
+
readFiles: /* @__PURE__ */ new Set(),
|
|
8003
|
+
fileMtimes: /* @__PURE__ */ new Map()
|
|
8004
|
+
};
|
|
8005
|
+
}
|
|
8006
|
+
async function indexService(args, hooks = {}) {
|
|
8007
|
+
return runIndexer(stubCtx(args.projectRoot), {
|
|
8008
|
+
projectRoot: args.projectRoot,
|
|
8009
|
+
indexDir: args.indexDir,
|
|
8010
|
+
files: args.files,
|
|
8011
|
+
force: args.force,
|
|
8012
|
+
langs: args.langs,
|
|
8013
|
+
ignore: args.ignore,
|
|
8014
|
+
signal: hooks.signal,
|
|
8015
|
+
onProgress: hooks.onProgress
|
|
8016
|
+
});
|
|
8017
|
+
}
|
|
8018
|
+
function searchService(args) {
|
|
8019
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
8020
|
+
try {
|
|
8021
|
+
return store.searchRanked(
|
|
8022
|
+
args.query,
|
|
8023
|
+
{
|
|
8024
|
+
kind: args.kind,
|
|
8025
|
+
lang: args.lang,
|
|
8026
|
+
file: args.file,
|
|
8027
|
+
lspKind: args.lspKind
|
|
8028
|
+
},
|
|
8029
|
+
args.limit
|
|
8030
|
+
);
|
|
8031
|
+
} finally {
|
|
8032
|
+
store.close();
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
function statsService(args) {
|
|
8036
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
8037
|
+
try {
|
|
8038
|
+
return store.getStats();
|
|
8039
|
+
} finally {
|
|
8040
|
+
store.close();
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
8043
|
+
|
|
8044
|
+
// src/codebase-index/background-indexer.ts
|
|
8045
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
|
|
8046
|
+
var DEFAULT_INCREMENTAL_TIMEOUT_MS = 3e4;
|
|
8047
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
8048
|
+
var _ready = false;
|
|
8049
|
+
var _indexing = false;
|
|
8050
|
+
var _currentFile = 0;
|
|
8051
|
+
var _totalFiles = 0;
|
|
8052
|
+
var _lastError = null;
|
|
8053
|
+
function isIndexReady() {
|
|
8054
|
+
return _ready;
|
|
8055
|
+
}
|
|
8056
|
+
function isIndexing() {
|
|
8057
|
+
return _indexing;
|
|
8058
|
+
}
|
|
8059
|
+
function getIndexState() {
|
|
8060
|
+
return {
|
|
8061
|
+
ready: _ready,
|
|
8062
|
+
indexing: _indexing,
|
|
8063
|
+
currentFile: _currentFile,
|
|
8064
|
+
totalFiles: _totalFiles,
|
|
8065
|
+
lastError: _lastError,
|
|
8066
|
+
circuit: indexCircuitBreaker.snapshot()
|
|
8067
|
+
};
|
|
8068
|
+
}
|
|
8069
|
+
var _listeners = [];
|
|
8070
|
+
function onIndexStateChange(listener) {
|
|
8071
|
+
_listeners.push(listener);
|
|
8072
|
+
return () => {
|
|
8073
|
+
_listeners = _listeners.filter((l) => l !== listener);
|
|
8074
|
+
};
|
|
8075
|
+
}
|
|
8076
|
+
function emitState() {
|
|
8077
|
+
const state = getIndexState();
|
|
8078
|
+
for (const l of _listeners) l(state);
|
|
8079
|
+
}
|
|
8080
|
+
function setIndexProgress(current, total) {
|
|
8081
|
+
_currentFile = current;
|
|
8082
|
+
_totalFiles = total;
|
|
8083
|
+
emitState();
|
|
8084
|
+
}
|
|
8085
|
+
var worker = null;
|
|
8086
|
+
var workerUnavailable = false;
|
|
8087
|
+
var nextRpcId = 1;
|
|
8088
|
+
var pending = /* @__PURE__ */ new Map();
|
|
8089
|
+
function resolveWorkerUrl() {
|
|
8090
|
+
if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
|
|
8091
|
+
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
8092
|
+
try {
|
|
8093
|
+
const url = new URL(rel, import.meta.url);
|
|
8094
|
+
if (url.protocol === "file:" && fs7.existsSync(fileURLToPath(url))) return url;
|
|
8095
|
+
} catch {
|
|
8096
|
+
}
|
|
8097
|
+
}
|
|
8098
|
+
return null;
|
|
8099
|
+
}
|
|
8100
|
+
function failAllPending(err) {
|
|
8101
|
+
const entries = [...pending.values()];
|
|
8102
|
+
pending.clear();
|
|
8103
|
+
for (const p of entries) p.reject(err);
|
|
8104
|
+
}
|
|
8105
|
+
function ensureWorker() {
|
|
8106
|
+
if (worker) return worker;
|
|
8107
|
+
if (workerUnavailable) return null;
|
|
8108
|
+
const url = resolveWorkerUrl();
|
|
8109
|
+
if (!url) {
|
|
8110
|
+
workerUnavailable = true;
|
|
8111
|
+
return null;
|
|
8112
|
+
}
|
|
8113
|
+
try {
|
|
8114
|
+
const w = new Worker(url, { name: "wstack-codebase-index" });
|
|
8115
|
+
w.unref();
|
|
8116
|
+
w.on("message", (msg) => {
|
|
8117
|
+
if (msg.type === "progress") {
|
|
8118
|
+
pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
|
|
8119
|
+
return;
|
|
8120
|
+
}
|
|
8121
|
+
const entry = pending.get(msg.id);
|
|
8122
|
+
if (!entry) return;
|
|
8123
|
+
pending.delete(msg.id);
|
|
8124
|
+
if (msg.ok) entry.resolve(msg.result);
|
|
8125
|
+
else entry.reject(new Error(msg.error));
|
|
8126
|
+
});
|
|
8127
|
+
w.on("error", (err) => {
|
|
8128
|
+
worker = null;
|
|
8129
|
+
failAllPending(err);
|
|
8130
|
+
});
|
|
8131
|
+
w.on("exit", () => {
|
|
8132
|
+
if (worker === w) worker = null;
|
|
8133
|
+
failAllPending(new Error("codebase-index worker exited"));
|
|
8134
|
+
});
|
|
8135
|
+
worker = w;
|
|
8136
|
+
return w;
|
|
8137
|
+
} catch {
|
|
8138
|
+
workerUnavailable = true;
|
|
8139
|
+
return null;
|
|
8140
|
+
}
|
|
8141
|
+
}
|
|
8142
|
+
function terminateWorker(reason) {
|
|
8143
|
+
const w = worker;
|
|
8144
|
+
worker = null;
|
|
8145
|
+
failAllPending(reason);
|
|
8146
|
+
if (w) void w.terminate().catch(() => {
|
|
8147
|
+
});
|
|
8148
|
+
}
|
|
8149
|
+
function shutdownCodebaseIndexHost() {
|
|
8150
|
+
cancelPendingReindexes();
|
|
8151
|
+
terminateWorker(new Error("codebase-index host shut down"));
|
|
8152
|
+
workerUnavailable = false;
|
|
8153
|
+
}
|
|
8154
|
+
function callIndexOp(op, args, opts) {
|
|
8155
|
+
const w = ensureWorker();
|
|
8156
|
+
if (!w) return callInline(op, args, opts);
|
|
8157
|
+
return new Promise((resolve7, reject) => {
|
|
8158
|
+
const id = nextRpcId++;
|
|
8159
|
+
const timer = setTimeout(() => {
|
|
8160
|
+
pending.delete(id);
|
|
8161
|
+
const err = new IndexTimeoutError(
|
|
8162
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
8163
|
+
);
|
|
8164
|
+
terminateWorker(err);
|
|
8165
|
+
reject(err);
|
|
8166
|
+
}, opts.timeoutMs);
|
|
8167
|
+
timer.unref?.();
|
|
8168
|
+
const onAbort = () => {
|
|
8169
|
+
w.postMessage({ type: "cancel", id });
|
|
8170
|
+
};
|
|
8171
|
+
if (opts.signal?.aborted) onAbort();
|
|
8172
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
8173
|
+
const cleanup = () => {
|
|
8174
|
+
clearTimeout(timer);
|
|
8175
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
8176
|
+
};
|
|
8177
|
+
pending.set(id, {
|
|
8178
|
+
resolve: (v) => {
|
|
8179
|
+
cleanup();
|
|
8180
|
+
resolve7(v);
|
|
8181
|
+
},
|
|
8182
|
+
reject: (e) => {
|
|
8183
|
+
cleanup();
|
|
8184
|
+
reject(e);
|
|
8185
|
+
},
|
|
8186
|
+
onProgress: opts.onProgress
|
|
8187
|
+
});
|
|
8188
|
+
w.postMessage({ type: "request", id, op, args });
|
|
8189
|
+
});
|
|
8190
|
+
}
|
|
8191
|
+
async function callInline(op, args, opts) {
|
|
8192
|
+
const ac = new AbortController();
|
|
8193
|
+
const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
|
|
8194
|
+
if (opts.signal?.aborted) onOuterAbort();
|
|
8195
|
+
else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
8196
|
+
let timer;
|
|
8197
|
+
const watchdog = new Promise((_, reject) => {
|
|
8198
|
+
timer = setTimeout(() => {
|
|
8199
|
+
const err = new IndexTimeoutError(
|
|
8200
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
8201
|
+
);
|
|
8202
|
+
ac.abort(err);
|
|
8203
|
+
reject(err);
|
|
8204
|
+
}, opts.timeoutMs);
|
|
8205
|
+
timer.unref?.();
|
|
8206
|
+
});
|
|
8207
|
+
const job = async () => {
|
|
8208
|
+
switch (op) {
|
|
8209
|
+
case "index":
|
|
8210
|
+
return await indexService(args, {
|
|
8211
|
+
signal: ac.signal,
|
|
8212
|
+
onProgress: opts.onProgress
|
|
8213
|
+
});
|
|
8214
|
+
case "search":
|
|
8215
|
+
return searchService(args);
|
|
8216
|
+
case "stats":
|
|
8217
|
+
return statsService(args);
|
|
8218
|
+
default:
|
|
8219
|
+
throw new Error(`unknown index op: ${String(op)}`);
|
|
8220
|
+
}
|
|
8221
|
+
};
|
|
8222
|
+
try {
|
|
8223
|
+
return await Promise.race([job(), watchdog]);
|
|
8224
|
+
} finally {
|
|
8225
|
+
if (timer) clearTimeout(timer);
|
|
8226
|
+
opts.signal?.removeEventListener("abort", onOuterAbort);
|
|
8227
|
+
}
|
|
8228
|
+
}
|
|
8229
|
+
var chain = Promise.resolve();
|
|
8230
|
+
function withMutex(job) {
|
|
8231
|
+
const run = chain.then(job, job);
|
|
8232
|
+
chain = run.then(
|
|
8233
|
+
() => void 0,
|
|
8234
|
+
() => void 0
|
|
8235
|
+
);
|
|
8236
|
+
return run;
|
|
8237
|
+
}
|
|
8238
|
+
function circuitOpenError() {
|
|
8239
|
+
const c = indexCircuitBreaker.snapshot();
|
|
8240
|
+
return new CircuitOpenError(
|
|
8241
|
+
"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."
|
|
8242
|
+
);
|
|
8243
|
+
}
|
|
8244
|
+
var DEFAULT_DEBOUNCE_MS = 400;
|
|
8245
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
8246
|
+
function debounceKey(indexDir, file) {
|
|
8247
|
+
return `${indexDir ?? ""}|${file}`;
|
|
8248
|
+
}
|
|
8249
|
+
function isIndexableFile(filePath) {
|
|
8250
|
+
return detectLang(filePath) !== null;
|
|
8251
|
+
}
|
|
8252
|
+
async function runStartupIndex(opts) {
|
|
8253
|
+
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
8254
|
+
_indexing = true;
|
|
8255
|
+
emitState();
|
|
8256
|
+
try {
|
|
8257
|
+
const result = await withMutex(() => {
|
|
8258
|
+
_currentFile = 0;
|
|
8259
|
+
_totalFiles = 0;
|
|
8260
|
+
_lastError = null;
|
|
8261
|
+
return callIndexOp(
|
|
8262
|
+
"index",
|
|
8263
|
+
{
|
|
8264
|
+
projectRoot: opts.projectRoot,
|
|
8265
|
+
indexDir: opts.indexDir,
|
|
8266
|
+
force: opts.force,
|
|
8267
|
+
langs: opts.langs
|
|
8268
|
+
},
|
|
8269
|
+
{
|
|
8270
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
|
|
8271
|
+
signal: opts.signal,
|
|
8272
|
+
onProgress: setIndexProgress
|
|
8273
|
+
}
|
|
8274
|
+
);
|
|
8275
|
+
});
|
|
8276
|
+
_ready = true;
|
|
8277
|
+
indexCircuitBreaker.recordSuccess();
|
|
8278
|
+
return result;
|
|
8279
|
+
} catch (err) {
|
|
8280
|
+
_lastError = err instanceof Error ? err.message : String(err);
|
|
8281
|
+
_ready = true;
|
|
8282
|
+
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
8283
|
+
throw err;
|
|
8284
|
+
} finally {
|
|
8285
|
+
_indexing = false;
|
|
8286
|
+
emitState();
|
|
8287
|
+
}
|
|
8288
|
+
}
|
|
8289
|
+
function enqueueReindex(opts) {
|
|
8290
|
+
const files = opts.files.filter(isIndexableFile);
|
|
8291
|
+
if (files.length === 0) return;
|
|
8292
|
+
const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
8293
|
+
for (const file of files) {
|
|
8294
|
+
const key = debounceKey(opts.indexDir, file);
|
|
8295
|
+
const existing = debounceTimers.get(key);
|
|
8296
|
+
if (existing) clearTimeout(existing);
|
|
8297
|
+
const timer = setTimeout(() => {
|
|
8298
|
+
debounceTimers.delete(key);
|
|
8299
|
+
if (!indexCircuitBreaker.allowRequest()) {
|
|
8300
|
+
opts.onError?.(circuitOpenError());
|
|
8301
|
+
return;
|
|
8302
|
+
}
|
|
8303
|
+
void withMutex(
|
|
8304
|
+
() => callIndexOp(
|
|
8305
|
+
"index",
|
|
8306
|
+
{ projectRoot: opts.projectRoot, files: [file], indexDir: opts.indexDir },
|
|
8307
|
+
{ timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS }
|
|
8308
|
+
)
|
|
8309
|
+
).then(
|
|
8310
|
+
() => indexCircuitBreaker.recordSuccess(),
|
|
8311
|
+
(err) => {
|
|
8312
|
+
indexCircuitBreaker.recordFailure(err);
|
|
8313
|
+
opts.onError?.(err);
|
|
8314
|
+
}
|
|
8315
|
+
);
|
|
8316
|
+
}, ms);
|
|
8317
|
+
timer.unref?.();
|
|
8318
|
+
debounceTimers.set(key, timer);
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
function cancelPendingReindexes() {
|
|
8322
|
+
for (const t of debounceTimers.values()) clearTimeout(t);
|
|
8323
|
+
debounceTimers.clear();
|
|
8324
|
+
}
|
|
8325
|
+
async function searchCodebaseIndex(args, opts = {}) {
|
|
8326
|
+
return callIndexOp("search", args, {
|
|
8327
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
8328
|
+
signal: opts.signal
|
|
8329
|
+
});
|
|
8330
|
+
}
|
|
8331
|
+
async function codebaseIndexStats(args, opts = {}) {
|
|
8332
|
+
return callIndexOp("stats", args, {
|
|
8333
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
8334
|
+
signal: opts.signal
|
|
8335
|
+
});
|
|
8336
|
+
}
|
|
8337
|
+
|
|
7470
8338
|
// src/codebase-index/codebase-index-tool.ts
|
|
7471
8339
|
var codebaseIndexTool = {
|
|
7472
8340
|
name: "codebase-index",
|
|
@@ -7502,103 +8370,24 @@ var codebaseIndexTool = {
|
|
|
7502
8370
|
note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
|
|
7503
8371
|
};
|
|
7504
8372
|
}
|
|
7505
|
-
const
|
|
8373
|
+
const circuit = indexCircuitBreaker.snapshot();
|
|
8374
|
+
if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
|
|
8375
|
+
return {
|
|
8376
|
+
filesIndexed: 0,
|
|
8377
|
+
symbolsIndexed: 0,
|
|
8378
|
+
langStats: {},
|
|
8379
|
+
durationMs: 0,
|
|
8380
|
+
errors: [],
|
|
8381
|
+
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.`
|
|
8382
|
+
};
|
|
8383
|
+
}
|
|
8384
|
+
return await runStartupIndex({
|
|
7506
8385
|
projectRoot: ctx.projectRoot,
|
|
7507
8386
|
force: input.force ?? false,
|
|
7508
8387
|
langs: input.langs,
|
|
7509
8388
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
7510
8389
|
signal: execOpts?.signal
|
|
7511
8390
|
});
|
|
7512
|
-
setIndexReady();
|
|
7513
|
-
return result;
|
|
7514
|
-
}
|
|
7515
|
-
};
|
|
7516
|
-
|
|
7517
|
-
// src/codebase-index/bm25.ts
|
|
7518
|
-
var K1 = 1.5;
|
|
7519
|
-
var B = 0.75;
|
|
7520
|
-
function tokenise(text) {
|
|
7521
|
-
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
7522
|
-
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
7523
|
-
}
|
|
7524
|
-
function splitName(name) {
|
|
7525
|
-
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
7526
|
-
}
|
|
7527
|
-
function buildIndexableText(name, signature, docComment) {
|
|
7528
|
-
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
7529
|
-
}
|
|
7530
|
-
function buildBm25Index(docs) {
|
|
7531
|
-
const documents = docs.map((d) => {
|
|
7532
|
-
const tokens = tokenise(d.text);
|
|
7533
|
-
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
7534
|
-
});
|
|
7535
|
-
const df = {};
|
|
7536
|
-
for (const doc of documents) {
|
|
7537
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7538
|
-
for (const t of doc.tokens) {
|
|
7539
|
-
if (!seen.has(t)) {
|
|
7540
|
-
df[t] = (df[t] ?? 0) + 1;
|
|
7541
|
-
seen.add(t);
|
|
7542
|
-
}
|
|
7543
|
-
}
|
|
7544
|
-
}
|
|
7545
|
-
const N = documents.length;
|
|
7546
|
-
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
7547
|
-
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
7548
|
-
return new Bm25Index(documents, df, N, avgLen);
|
|
7549
|
-
}
|
|
7550
|
-
var Bm25Index = class {
|
|
7551
|
-
constructor(documents, df, N, avgLen) {
|
|
7552
|
-
this.documents = documents;
|
|
7553
|
-
this.df = df;
|
|
7554
|
-
this.N = N;
|
|
7555
|
-
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
7556
|
-
}
|
|
7557
|
-
documents;
|
|
7558
|
-
df;
|
|
7559
|
-
N;
|
|
7560
|
-
safeAvgLen;
|
|
7561
|
-
score(query2, filter) {
|
|
7562
|
-
const qTokens = tokenise(query2);
|
|
7563
|
-
if (qTokens.length === 0) return [];
|
|
7564
|
-
const results = [];
|
|
7565
|
-
for (const doc of this.documents) {
|
|
7566
|
-
if (filter && !filter(doc.id)) continue;
|
|
7567
|
-
let docScore = 0;
|
|
7568
|
-
for (const qTerm of qTokens) {
|
|
7569
|
-
let tf = 0;
|
|
7570
|
-
for (const t of doc.tokens) {
|
|
7571
|
-
if (t === qTerm) tf++;
|
|
7572
|
-
}
|
|
7573
|
-
if (tf === 0) continue;
|
|
7574
|
-
const dfVal = this.df[qTerm] ?? 0;
|
|
7575
|
-
if (dfVal === 0) continue;
|
|
7576
|
-
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
7577
|
-
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
7578
|
-
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
7579
|
-
docScore += idf * tfComponent;
|
|
7580
|
-
}
|
|
7581
|
-
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
7582
|
-
}
|
|
7583
|
-
return results;
|
|
7584
|
-
}
|
|
7585
|
-
getDoc(id) {
|
|
7586
|
-
return this.documents.find((d) => d.id === id);
|
|
7587
|
-
}
|
|
7588
|
-
extractSnippet(docId, queryTokens, radius = 40) {
|
|
7589
|
-
const doc = this.getDoc(docId);
|
|
7590
|
-
if (!doc) return "";
|
|
7591
|
-
for (const tok of queryTokens) {
|
|
7592
|
-
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
7593
|
-
if (idx !== -1) {
|
|
7594
|
-
const start = Math.max(0, idx - radius);
|
|
7595
|
-
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
7596
|
-
const excerpt = doc.raw.slice(start, end);
|
|
7597
|
-
const ellipsis = "\u2026";
|
|
7598
|
-
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
7599
|
-
}
|
|
7600
|
-
}
|
|
7601
|
-
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
7602
8391
|
}
|
|
7603
8392
|
};
|
|
7604
8393
|
|
|
@@ -7644,7 +8433,7 @@ var codebaseSearchTool = {
|
|
|
7644
8433
|
},
|
|
7645
8434
|
required: ["query"]
|
|
7646
8435
|
},
|
|
7647
|
-
async execute(input, ctx) {
|
|
8436
|
+
async execute(input, ctx, execOpts) {
|
|
7648
8437
|
const state = getIndexState();
|
|
7649
8438
|
if (!state.ready) {
|
|
7650
8439
|
return {
|
|
@@ -7663,51 +8452,30 @@ var codebaseSearchTool = {
|
|
|
7663
8452
|
};
|
|
7664
8453
|
}
|
|
7665
8454
|
if (state.lastError) {
|
|
8455
|
+
const circuit = state.circuit;
|
|
8456
|
+
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.";
|
|
7666
8457
|
return {
|
|
7667
8458
|
results: [],
|
|
7668
8459
|
total: 0,
|
|
7669
8460
|
query: input.query,
|
|
7670
|
-
indexStatus: `Index build failed: ${state.lastError}.
|
|
8461
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
7671
8462
|
};
|
|
7672
8463
|
}
|
|
7673
|
-
const
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
8464
|
+
const limit = Math.min(input.limit ?? 20, 100);
|
|
8465
|
+
const { results, total } = await searchCodebaseIndex(
|
|
8466
|
+
{
|
|
8467
|
+
projectRoot: ctx.projectRoot,
|
|
8468
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
8469
|
+
query: input.query,
|
|
7677
8470
|
kind: input.kind,
|
|
7678
8471
|
lang: input.lang,
|
|
7679
8472
|
file: input.file,
|
|
7680
|
-
lspKind: input.lspKind
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
id: c.id,
|
|
7687
|
-
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
7688
|
-
}));
|
|
7689
|
-
const bm25 = buildBm25Index(indexable);
|
|
7690
|
-
const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
|
|
7691
|
-
scored.sort((a, b) => b.score - a.score);
|
|
7692
|
-
const top = scored.slice(0, limit);
|
|
7693
|
-
const qTokens = tokenise(input.query);
|
|
7694
|
-
const results = top.map(({ id, score }) => {
|
|
7695
|
-
const c = expectDefined(candidates.find((c2) => c2.id === id));
|
|
7696
|
-
const snippet = bm25.extractSnippet(id, qTokens);
|
|
7697
|
-
return {
|
|
7698
|
-
...c,
|
|
7699
|
-
score,
|
|
7700
|
-
snippet
|
|
7701
|
-
};
|
|
7702
|
-
});
|
|
7703
|
-
return {
|
|
7704
|
-
results,
|
|
7705
|
-
total: candidates.length,
|
|
7706
|
-
query: input.query
|
|
7707
|
-
};
|
|
7708
|
-
} finally {
|
|
7709
|
-
store.close();
|
|
7710
|
-
}
|
|
8473
|
+
lspKind: input.lspKind,
|
|
8474
|
+
limit
|
|
8475
|
+
},
|
|
8476
|
+
{ signal: execOpts?.signal }
|
|
8477
|
+
);
|
|
8478
|
+
return { results, total, query: input.query };
|
|
7711
8479
|
}
|
|
7712
8480
|
};
|
|
7713
8481
|
|
|
@@ -7726,7 +8494,7 @@ var codebaseStatsTool = {
|
|
|
7726
8494
|
properties: {},
|
|
7727
8495
|
additionalProperties: false
|
|
7728
8496
|
},
|
|
7729
|
-
async execute(_input, ctx) {
|
|
8497
|
+
async execute(_input, ctx, execOpts) {
|
|
7730
8498
|
const idxState = getIndexState();
|
|
7731
8499
|
if (!idxState.ready) {
|
|
7732
8500
|
return {
|
|
@@ -7741,51 +8509,113 @@ var codebaseStatsTool = {
|
|
|
7741
8509
|
indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
|
|
7742
8510
|
};
|
|
7743
8511
|
}
|
|
8512
|
+
const stats = await codebaseIndexStats(
|
|
8513
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
8514
|
+
{ signal: execOpts?.signal }
|
|
8515
|
+
);
|
|
7744
8516
|
if (idxState.indexing) {
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
8517
|
+
return {
|
|
8518
|
+
...stats,
|
|
8519
|
+
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
8520
|
+
};
|
|
8521
|
+
}
|
|
8522
|
+
const circuit = idxState.circuit;
|
|
8523
|
+
return {
|
|
8524
|
+
totalSymbols: stats.totalSymbols,
|
|
8525
|
+
totalFiles: stats.totalFiles,
|
|
8526
|
+
byLang: stats.byLang,
|
|
8527
|
+
byKind: stats.byKind,
|
|
8528
|
+
lastIndexed: stats.lastIndexed,
|
|
8529
|
+
sizeBytes: stats.sizeBytes,
|
|
8530
|
+
indexPath: stats.indexPath,
|
|
8531
|
+
version: stats.version,
|
|
8532
|
+
...circuit.state === "open" ? {
|
|
8533
|
+
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.`
|
|
8534
|
+
} : {}
|
|
8535
|
+
};
|
|
8536
|
+
}
|
|
8537
|
+
};
|
|
8538
|
+
var setWorkingDirTool = {
|
|
8539
|
+
name: "set_working_dir",
|
|
8540
|
+
category: "Context",
|
|
8541
|
+
description: "Change the current working directory for all subsequent file operations. The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
|
|
8542
|
+
usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
|
|
8543
|
+
permission: "confirm",
|
|
8544
|
+
mutating: true,
|
|
8545
|
+
capabilities: ["fs.read"],
|
|
8546
|
+
timeoutMs: 5e3,
|
|
8547
|
+
inputSchema: {
|
|
8548
|
+
type: "object",
|
|
8549
|
+
properties: {
|
|
8550
|
+
path: {
|
|
8551
|
+
type: "string",
|
|
8552
|
+
description: "Directory to navigate to. Can be relative (to projectRoot) or absolute. If omitted, returns the current working directory without changing it."
|
|
7754
8553
|
}
|
|
7755
8554
|
}
|
|
7756
|
-
|
|
8555
|
+
},
|
|
8556
|
+
async execute(input, ctx, _opts) {
|
|
8557
|
+
if (!input.path) {
|
|
8558
|
+
return {
|
|
8559
|
+
current: ctx.workingDir,
|
|
8560
|
+
message: `Current working directory is ${ctx.workingDir}`
|
|
8561
|
+
};
|
|
8562
|
+
}
|
|
8563
|
+
const previous = ctx.workingDir;
|
|
8564
|
+
let resolved;
|
|
7757
8565
|
try {
|
|
7758
|
-
|
|
8566
|
+
resolved = ctx.setWorkingDir(input.path);
|
|
8567
|
+
} catch (err) {
|
|
7759
8568
|
return {
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
byLang: stats.byLang,
|
|
7763
|
-
byKind: stats.byKind,
|
|
7764
|
-
lastIndexed: stats.lastIndexed,
|
|
7765
|
-
sizeBytes: stats.sizeBytes,
|
|
7766
|
-
indexPath: stats.indexPath,
|
|
7767
|
-
version: stats.version
|
|
8569
|
+
current: ctx.workingDir,
|
|
8570
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7768
8571
|
};
|
|
7769
|
-
} finally {
|
|
7770
|
-
store.close();
|
|
7771
8572
|
}
|
|
8573
|
+
try {
|
|
8574
|
+
await fs4.access(resolved);
|
|
8575
|
+
} catch {
|
|
8576
|
+
try {
|
|
8577
|
+
ctx.setWorkingDir(previous);
|
|
8578
|
+
} catch {
|
|
8579
|
+
}
|
|
8580
|
+
return {
|
|
8581
|
+
current: ctx.workingDir,
|
|
8582
|
+
error: `Directory does not exist: ${resolved}`
|
|
8583
|
+
};
|
|
8584
|
+
}
|
|
8585
|
+
return {
|
|
8586
|
+
current: resolved,
|
|
8587
|
+
previous,
|
|
8588
|
+
message: `Working directory changed to ${resolved}`
|
|
8589
|
+
};
|
|
7772
8590
|
}
|
|
7773
8591
|
};
|
|
8592
|
+
function findTaskIndex(tasks, query2) {
|
|
8593
|
+
const asNum = Number.parseInt(query2, 10);
|
|
8594
|
+
if (!Number.isNaN(asNum)) {
|
|
8595
|
+
const idx = asNum - 1;
|
|
8596
|
+
if (tasks[idx]) return idx;
|
|
8597
|
+
}
|
|
8598
|
+
const byId = tasks.findIndex((t) => t.id === query2);
|
|
8599
|
+
if (byId >= 0) return byId;
|
|
8600
|
+
const lower = query2.toLowerCase();
|
|
8601
|
+
return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
|
|
8602
|
+
}
|
|
7774
8603
|
var taskTool = {
|
|
7775
8604
|
name: "task",
|
|
7776
8605
|
category: "Session",
|
|
7777
8606
|
description: "Manage structured work items with dependencies, types, and priorities. Use this for complex, multi-step work where tasks have ordering constraints. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. The task list persists across session resumes.",
|
|
7778
|
-
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate',
|
|
7779
|
-
permission: "
|
|
7780
|
-
mutating:
|
|
8607
|
+
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate',
|
|
8608
|
+
permission: "confirm",
|
|
8609
|
+
mutating: true,
|
|
8610
|
+
capabilities: ["fs.write"],
|
|
7781
8611
|
timeoutMs: 2e3,
|
|
7782
8612
|
inputSchema: {
|
|
7783
8613
|
type: "object",
|
|
7784
8614
|
properties: {
|
|
7785
8615
|
action: {
|
|
7786
8616
|
type: "string",
|
|
7787
|
-
enum: ["replace", "add", "status", "show"],
|
|
7788
|
-
description: "replace = set full list, add = append, status = update task status, show = view only."
|
|
8617
|
+
enum: ["replace", "add", "status", "show", "promote", "planify"],
|
|
8618
|
+
description: "replace = set full list, add = append, status = update task status, show = view only, promote = convert task to todos, planify = convert task to plan item."
|
|
7789
8619
|
},
|
|
7790
8620
|
tasks: {
|
|
7791
8621
|
type: "array",
|
|
@@ -7829,11 +8659,20 @@ var taskTool = {
|
|
|
7829
8659
|
required: ["title", "type", "priority"],
|
|
7830
8660
|
description: "Single task to append (id/createdAt/updatedAt auto-generated)."
|
|
7831
8661
|
},
|
|
7832
|
-
id: { type: "string", description: "Task id for action=status." },
|
|
8662
|
+
id: { type: "string", description: "Task id for action=status or target for action=promote." },
|
|
7833
8663
|
status: {
|
|
7834
8664
|
type: "string",
|
|
7835
8665
|
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
|
|
7836
8666
|
description: "New status for action=status."
|
|
8667
|
+
},
|
|
8668
|
+
target: {
|
|
8669
|
+
type: "string",
|
|
8670
|
+
description: "Target task identifier (id, 1-based index, or title substring) for action=promote."
|
|
8671
|
+
},
|
|
8672
|
+
subtasks: {
|
|
8673
|
+
type: "array",
|
|
8674
|
+
items: { type: "string" },
|
|
8675
|
+
description: "Optional subtask titles for action=promote. Each becomes a pending todo."
|
|
7837
8676
|
}
|
|
7838
8677
|
},
|
|
7839
8678
|
required: ["action"]
|
|
@@ -7844,65 +8683,196 @@ var taskTool = {
|
|
|
7844
8683
|
return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
|
|
7845
8684
|
}
|
|
7846
8685
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
8686
|
+
let early = null;
|
|
8687
|
+
const promoteMeta = { count: 0, title: "" };
|
|
8688
|
+
const planifyMeta = { title: "", details: "" };
|
|
8689
|
+
let didPlanify = false;
|
|
8690
|
+
const file = await mutateTasks(taskPath, sessionId, async (f) => {
|
|
8691
|
+
switch (input.action) {
|
|
8692
|
+
case "show":
|
|
8693
|
+
break;
|
|
8694
|
+
case "replace": {
|
|
8695
|
+
if (!Array.isArray(input.tasks)) {
|
|
8696
|
+
early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
|
|
8697
|
+
return f;
|
|
8698
|
+
}
|
|
8699
|
+
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
8700
|
+
for (const t of input.tasks) {
|
|
8701
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
8702
|
+
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
8703
|
+
if (missing.length > 0) {
|
|
8704
|
+
early = {
|
|
8705
|
+
ok: false,
|
|
8706
|
+
message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
|
|
8707
|
+
count: 0,
|
|
8708
|
+
completed: 0,
|
|
8709
|
+
inProgress: 0
|
|
8710
|
+
};
|
|
8711
|
+
return f;
|
|
8712
|
+
}
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8716
|
+
f.tasks = input.tasks.map((t) => ({
|
|
8717
|
+
...t,
|
|
8718
|
+
createdAt: t.createdAt || now,
|
|
8719
|
+
updatedAt: now
|
|
8720
|
+
}));
|
|
8721
|
+
break;
|
|
7854
8722
|
}
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
8723
|
+
case "add": {
|
|
8724
|
+
const t = input.task;
|
|
8725
|
+
if (!t || !t.title) {
|
|
8726
|
+
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
8727
|
+
return f;
|
|
8728
|
+
}
|
|
8729
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
8730
|
+
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
8731
|
+
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
8732
|
+
if (missing.length > 0) {
|
|
8733
|
+
early = {
|
|
8734
|
+
ok: false,
|
|
8735
|
+
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
8736
|
+
count: 0,
|
|
8737
|
+
completed: 0,
|
|
8738
|
+
inProgress: 0
|
|
8739
|
+
};
|
|
8740
|
+
return f;
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8743
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8744
|
+
const newTask = {
|
|
8745
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
8746
|
+
title: t.title,
|
|
8747
|
+
description: t.description,
|
|
8748
|
+
type: t.type || "feature",
|
|
8749
|
+
priority: t.priority || "medium",
|
|
8750
|
+
status: t.status || "pending",
|
|
8751
|
+
dependsOn: t.dependsOn,
|
|
8752
|
+
assignee: t.assignee,
|
|
8753
|
+
estimateHours: t.estimateHours,
|
|
8754
|
+
tags: t.tags,
|
|
8755
|
+
createdAt: now,
|
|
8756
|
+
updatedAt: now
|
|
8757
|
+
};
|
|
8758
|
+
f.tasks.push(newTask);
|
|
8759
|
+
break;
|
|
7868
8760
|
}
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
8761
|
+
case "status": {
|
|
8762
|
+
if (!input.id || !input.status) {
|
|
8763
|
+
early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
8764
|
+
return f;
|
|
8765
|
+
}
|
|
8766
|
+
const task = f.tasks.find((t) => t.id === input.id);
|
|
8767
|
+
if (!task) {
|
|
8768
|
+
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
8769
|
+
return f;
|
|
8770
|
+
}
|
|
8771
|
+
task.status = input.status;
|
|
8772
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8773
|
+
break;
|
|
8774
|
+
}
|
|
8775
|
+
case "promote": {
|
|
8776
|
+
const target = input.target?.trim();
|
|
8777
|
+
if (!target) {
|
|
8778
|
+
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
8779
|
+
return f;
|
|
8780
|
+
}
|
|
8781
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
8782
|
+
if (idx === -1) {
|
|
8783
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8784
|
+
return f;
|
|
8785
|
+
}
|
|
8786
|
+
const match = f.tasks[idx];
|
|
8787
|
+
if (!match) {
|
|
8788
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8789
|
+
return f;
|
|
8790
|
+
}
|
|
8791
|
+
if (match.status !== "completed" && match.status !== "failed") {
|
|
8792
|
+
match.status = "in_progress";
|
|
8793
|
+
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8794
|
+
}
|
|
8795
|
+
const todos = [];
|
|
8796
|
+
const ts2 = Date.now();
|
|
8797
|
+
todos.push({
|
|
8798
|
+
id: `todo_${ts2}_task`,
|
|
8799
|
+
content: match.title,
|
|
8800
|
+
status: "in_progress",
|
|
8801
|
+
activeForm: match.title,
|
|
8802
|
+
promotedFromTask: match.id
|
|
8803
|
+
});
|
|
8804
|
+
if (match.description) {
|
|
8805
|
+
todos.push({
|
|
8806
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
8807
|
+
content: match.description.slice(0, 200),
|
|
8808
|
+
status: "pending",
|
|
8809
|
+
promotedFromTask: match.id
|
|
8810
|
+
});
|
|
8811
|
+
}
|
|
8812
|
+
if (input.subtasks && input.subtasks.length > 0) {
|
|
8813
|
+
for (const st of input.subtasks) {
|
|
8814
|
+
todos.push({
|
|
8815
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
8816
|
+
content: st,
|
|
8817
|
+
status: "pending",
|
|
8818
|
+
promotedFromTask: match.id
|
|
8819
|
+
});
|
|
8820
|
+
}
|
|
8821
|
+
}
|
|
8822
|
+
ctx.state.replaceTodos(todos);
|
|
8823
|
+
promoteMeta.count = todos.length;
|
|
8824
|
+
promoteMeta.title = match.title;
|
|
8825
|
+
break;
|
|
7891
8826
|
}
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
8827
|
+
case "planify": {
|
|
8828
|
+
const target = input.target?.trim();
|
|
8829
|
+
if (!target) {
|
|
8830
|
+
early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
8831
|
+
return f;
|
|
8832
|
+
}
|
|
8833
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
8834
|
+
if (idx === -1) {
|
|
8835
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8836
|
+
return f;
|
|
8837
|
+
}
|
|
8838
|
+
const match = f.tasks[idx];
|
|
8839
|
+
if (!match) {
|
|
8840
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8841
|
+
return f;
|
|
8842
|
+
}
|
|
8843
|
+
planifyMeta.title = match.title;
|
|
8844
|
+
planifyMeta.details = match.description ?? "";
|
|
8845
|
+
didPlanify = true;
|
|
8846
|
+
break;
|
|
7895
8847
|
}
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
break;
|
|
8848
|
+
default:
|
|
8849
|
+
early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
|
|
8850
|
+
return f;
|
|
7900
8851
|
}
|
|
7901
|
-
|
|
7902
|
-
|
|
8852
|
+
return f;
|
|
8853
|
+
});
|
|
8854
|
+
if (early) return early;
|
|
8855
|
+
if (didPlanify) {
|
|
8856
|
+
const { title, details } = planifyMeta;
|
|
8857
|
+
const planPath = ctx.meta["plan.path"];
|
|
8858
|
+
if (typeof planPath === "string" && planPath) {
|
|
8859
|
+
const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
8860
|
+
const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
|
|
8861
|
+
await savePlan(planPath, updated);
|
|
8862
|
+
return {
|
|
8863
|
+
ok: true,
|
|
8864
|
+
message: `planify ok \u2014 added "${title}" to plan.
|
|
8865
|
+
${formatPlan(updated)}`,
|
|
8866
|
+
count: file.tasks.length,
|
|
8867
|
+
completed: computeTaskItemProgress(file.tasks).completed,
|
|
8868
|
+
inProgress: computeTaskItemProgress(file.tasks).inProgress
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8871
|
+
return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
|
|
7903
8872
|
}
|
|
7904
8873
|
const p = computeTaskItemProgress(file.tasks);
|
|
7905
|
-
const summary =
|
|
8874
|
+
const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
|
|
8875
|
+
${formatTaskList(file.tasks)}` : file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
|
|
7906
8876
|
return {
|
|
7907
8877
|
ok: true,
|
|
7908
8878
|
message: summary,
|
|
@@ -7949,7 +8919,8 @@ var builtinTools = [
|
|
|
7949
8919
|
toolHelpTool,
|
|
7950
8920
|
codebaseIndexTool,
|
|
7951
8921
|
codebaseSearchTool,
|
|
7952
|
-
codebaseStatsTool
|
|
8922
|
+
codebaseStatsTool,
|
|
8923
|
+
setWorkingDirTool
|
|
7953
8924
|
];
|
|
7954
8925
|
|
|
7955
8926
|
// src/pack.ts
|
|
@@ -7959,6 +8930,6 @@ var builtinToolsPack = {
|
|
|
7959
8930
|
tools: builtinTools
|
|
7960
8931
|
};
|
|
7961
8932
|
|
|
7962
|
-
export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, runStartupIndex, scaffoldTool, searchMemoryTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
8933
|
+
export { CircuitBreaker, CircuitOpenError, IndexCircuitBreaker, IndexTimeoutError, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
7963
8934
|
//# sourceMappingURL=index.js.map
|
|
7964
8935
|
//# sourceMappingURL=index.js.map
|