@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/pack.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv,
|
|
3
|
+
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
6
6
|
import * as path2 from 'node:path';
|
|
7
|
-
import { resolve, sep, dirname } from 'node:path';
|
|
8
|
-
import * as
|
|
7
|
+
import { resolve, sep, dirname, join } from 'node:path';
|
|
8
|
+
import * as fs14 from 'node:fs/promises';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { Worker } from 'node:worker_threads';
|
|
11
13
|
import * as ts from 'typescript';
|
|
12
14
|
import * as dns from 'node:dns/promises';
|
|
13
15
|
import * as net from 'node:net';
|
|
14
16
|
import { Agent } from 'undici';
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
15
18
|
|
|
16
19
|
// src/_spawn-stream.ts
|
|
17
20
|
function resolveWin32Command(cmd) {
|
|
@@ -39,9 +42,10 @@ function resolveWin32Command(cmd) {
|
|
|
39
42
|
async function* spawnStream(opts) {
|
|
40
43
|
const max = opts.maxBytes ?? 2e5;
|
|
41
44
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
45
|
+
const maxQueue = opts.maxQueueSize ?? 500;
|
|
42
46
|
let stdout = "";
|
|
43
47
|
let stderr = "";
|
|
44
|
-
let
|
|
48
|
+
let pending2 = "";
|
|
45
49
|
let error;
|
|
46
50
|
const cmd = resolveWin32Command(opts.cmd);
|
|
47
51
|
const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
|
|
@@ -50,10 +54,12 @@ async function* spawnStream(opts) {
|
|
|
50
54
|
signal: opts.signal,
|
|
51
55
|
env: buildChildEnv(),
|
|
52
56
|
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
windowsHide: true,
|
|
53
58
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
54
59
|
});
|
|
55
60
|
const queue = [];
|
|
56
61
|
let waiter;
|
|
62
|
+
let paused = false;
|
|
57
63
|
const wake = () => {
|
|
58
64
|
if (waiter) {
|
|
59
65
|
const w = waiter;
|
|
@@ -61,17 +67,34 @@ async function* spawnStream(opts) {
|
|
|
61
67
|
w();
|
|
62
68
|
}
|
|
63
69
|
};
|
|
70
|
+
const resume = () => {
|
|
71
|
+
if (paused && queue.length < maxQueue) {
|
|
72
|
+
paused = false;
|
|
73
|
+
child.stdout?.resume();
|
|
74
|
+
child.stderr?.resume();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
64
77
|
child.stdout?.on("data", (c) => {
|
|
65
78
|
const s = c.toString();
|
|
66
79
|
if (stdout.length < max) stdout += s;
|
|
67
80
|
queue.push({ kind: "out", data: s });
|
|
68
81
|
wake();
|
|
82
|
+
if (!paused && queue.length >= maxQueue) {
|
|
83
|
+
paused = true;
|
|
84
|
+
child.stdout?.pause();
|
|
85
|
+
child.stderr?.pause();
|
|
86
|
+
}
|
|
69
87
|
});
|
|
70
88
|
child.stderr?.on("data", (c) => {
|
|
71
89
|
const s = c.toString();
|
|
72
90
|
if (stderr.length < max) stderr += s;
|
|
73
91
|
queue.push({ kind: "err", data: s });
|
|
74
92
|
wake();
|
|
93
|
+
if (!paused && queue.length >= maxQueue) {
|
|
94
|
+
paused = true;
|
|
95
|
+
child.stdout?.pause();
|
|
96
|
+
child.stderr?.pause();
|
|
97
|
+
}
|
|
75
98
|
});
|
|
76
99
|
child.on("error", (e) => {
|
|
77
100
|
error = e.message;
|
|
@@ -91,6 +114,7 @@ async function* spawnStream(opts) {
|
|
|
91
114
|
});
|
|
92
115
|
}
|
|
93
116
|
const chunk = queue.shift();
|
|
117
|
+
resume();
|
|
94
118
|
if (chunk.kind === "close") {
|
|
95
119
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
96
120
|
break;
|
|
@@ -100,14 +124,14 @@ async function* spawnStream(opts) {
|
|
|
100
124
|
exitCode = 1;
|
|
101
125
|
continue;
|
|
102
126
|
}
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
yield { type: "partial_output", text:
|
|
106
|
-
|
|
127
|
+
pending2 += chunk.data;
|
|
128
|
+
if (pending2.length >= flushAt) {
|
|
129
|
+
yield { type: "partial_output", text: pending2 };
|
|
130
|
+
pending2 = "";
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
|
-
if (
|
|
110
|
-
yield { type: "partial_output", text:
|
|
133
|
+
if (pending2.length > 0) {
|
|
134
|
+
yield { type: "partial_output", text: pending2 };
|
|
111
135
|
}
|
|
112
136
|
return {
|
|
113
137
|
stdout,
|
|
@@ -132,7 +156,7 @@ async function detectPackageManager(cwd) {
|
|
|
132
156
|
return "npm";
|
|
133
157
|
}
|
|
134
158
|
function resolvePath(input, ctx) {
|
|
135
|
-
return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.cwd, input);
|
|
159
|
+
return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
136
160
|
}
|
|
137
161
|
function ensureInsideRoot(absPath, ctx) {
|
|
138
162
|
const root = path2.resolve(ctx.projectRoot);
|
|
@@ -147,12 +171,12 @@ function safeResolve(input, ctx) {
|
|
|
147
171
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
148
172
|
}
|
|
149
173
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
150
|
-
const realRoot = await
|
|
174
|
+
const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
|
|
151
175
|
let probe = absPath;
|
|
152
176
|
for (; ; ) {
|
|
153
177
|
let real;
|
|
154
178
|
try {
|
|
155
|
-
real = await
|
|
179
|
+
real = await fs14.realpath(probe);
|
|
156
180
|
} catch (err) {
|
|
157
181
|
if (err.code === "ENOENT") {
|
|
158
182
|
const parent = path2.dirname(probe);
|
|
@@ -423,8 +447,13 @@ var CircuitBreaker = class {
|
|
|
423
447
|
* Call this BEFORE spawning a bash/exec process.
|
|
424
448
|
* Returns true if the call is allowed; false if the breaker is open.
|
|
425
449
|
* When false, callers MUST NOT spawn a process.
|
|
450
|
+
*
|
|
451
|
+
* @param bypass - If true, skip the circuit breaker check entirely.
|
|
452
|
+
* Use for background/fire-and-forget processes that should
|
|
453
|
+
* not affect breaker state.
|
|
426
454
|
*/
|
|
427
|
-
beforeCall() {
|
|
455
|
+
beforeCall(bypass = false) {
|
|
456
|
+
if (bypass) return true;
|
|
428
457
|
this._checkStateTransition();
|
|
429
458
|
if (this.state === "open") return false;
|
|
430
459
|
return true;
|
|
@@ -434,8 +463,12 @@ var CircuitBreaker = class {
|
|
|
434
463
|
* `durationMs` is the wall-clock time the process ran.
|
|
435
464
|
* `failed` is true when the process returned a non-zero exit code or
|
|
436
465
|
* threw an exception before spawning.
|
|
466
|
+
*
|
|
467
|
+
* @param bypass - If true, do not update breaker state.
|
|
468
|
+
* Use for background/fire-and-forget processes.
|
|
437
469
|
*/
|
|
438
|
-
afterCall(durationMs, failed) {
|
|
470
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
471
|
+
if (bypass) return;
|
|
439
472
|
const now = Date.now();
|
|
440
473
|
if (this.state === "half-open") {
|
|
441
474
|
if (failed) {
|
|
@@ -534,6 +567,17 @@ function redactCommand(cmd) {
|
|
|
534
567
|
return result;
|
|
535
568
|
}
|
|
536
569
|
var DEFAULT_GRACE_MS = 2e3;
|
|
570
|
+
function killWin32Tree(pid) {
|
|
571
|
+
try {
|
|
572
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
573
|
+
stdio: "ignore",
|
|
574
|
+
windowsHide: true
|
|
575
|
+
}).unref();
|
|
576
|
+
return true;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
537
581
|
var ProcessRegistryImpl = class {
|
|
538
582
|
processes = /* @__PURE__ */ new Map();
|
|
539
583
|
breaker;
|
|
@@ -591,16 +635,20 @@ var ProcessRegistryImpl = class {
|
|
|
591
635
|
/**
|
|
592
636
|
* Called before spawning a process. Returns true if allowed; false if
|
|
593
637
|
* the circuit breaker is open.
|
|
638
|
+
*
|
|
639
|
+
* @param bypass - If true, skip circuit breaker check (for background processes).
|
|
594
640
|
*/
|
|
595
|
-
beforeCall() {
|
|
596
|
-
return this.breaker.beforeCall();
|
|
641
|
+
beforeCall(bypass = false) {
|
|
642
|
+
return this.breaker.beforeCall(bypass);
|
|
597
643
|
}
|
|
598
644
|
/**
|
|
599
645
|
* Called after a process finishes. `durationMs` is wall-clock time;
|
|
600
646
|
* `failed` is true for non-zero exit codes.
|
|
647
|
+
*
|
|
648
|
+
* @param bypass - If true, do not update circuit breaker state (for background processes).
|
|
601
649
|
*/
|
|
602
|
-
afterCall(durationMs, failed) {
|
|
603
|
-
this.breaker.afterCall(durationMs, failed);
|
|
650
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
651
|
+
this.breaker.afterCall(durationMs, failed, bypass);
|
|
604
652
|
}
|
|
605
653
|
/** Force-open the circuit breaker (Ctrl+C, /kill force). */
|
|
606
654
|
forceBreakerOpen() {
|
|
@@ -631,9 +679,22 @@ var ProcessRegistryImpl = class {
|
|
|
631
679
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
632
680
|
const isWin = os.platform() === "win32";
|
|
633
681
|
if (isWin) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
682
|
+
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
683
|
+
if (liveRealChild && killWin32Tree(pid)) {
|
|
684
|
+
const fallback = setTimeout(() => {
|
|
685
|
+
if (p.child.exitCode === null) {
|
|
686
|
+
try {
|
|
687
|
+
p.child.kill("SIGKILL");
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}, graceMs);
|
|
692
|
+
fallback.unref?.();
|
|
693
|
+
} else {
|
|
694
|
+
try {
|
|
695
|
+
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
637
698
|
}
|
|
638
699
|
p.killed = true;
|
|
639
700
|
return true;
|
|
@@ -709,6 +770,7 @@ var MAX_OUTPUT = 32768;
|
|
|
709
770
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
710
771
|
var STREAM_FLUSH_INTERVAL_MS = 200;
|
|
711
772
|
var STREAM_FLUSH_BYTES = 4 * 1024;
|
|
773
|
+
var MAX_QUEUE_CHUNKS = 500;
|
|
712
774
|
var bashTool = {
|
|
713
775
|
name: "bash",
|
|
714
776
|
category: "Shell",
|
|
@@ -756,7 +818,8 @@ var bashTool = {
|
|
|
756
818
|
async *executeStream(input, ctx, opts) {
|
|
757
819
|
if (!input?.command) throw new Error("bash: command is required");
|
|
758
820
|
const registry = getProcessRegistry();
|
|
759
|
-
|
|
821
|
+
const bypassBreaker = !!input.background;
|
|
822
|
+
if (!registry.beforeCall(bypassBreaker)) {
|
|
760
823
|
yield {
|
|
761
824
|
type: "final",
|
|
762
825
|
output: {
|
|
@@ -769,6 +832,17 @@ var bashTool = {
|
|
|
769
832
|
};
|
|
770
833
|
return;
|
|
771
834
|
}
|
|
835
|
+
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
836
|
+
if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
|
|
837
|
+
console.warn(JSON.stringify({
|
|
838
|
+
level: "warn",
|
|
839
|
+
event: "bash.pipe_to_shell_detected",
|
|
840
|
+
message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
|
|
841
|
+
command_prefix: input.command.slice(0, 100),
|
|
842
|
+
// Log first 100 chars for review
|
|
843
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
772
846
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
773
847
|
const isWin = os.platform() === "win32";
|
|
774
848
|
const shell = (() => {
|
|
@@ -794,6 +868,10 @@ var bashTool = {
|
|
|
794
868
|
env,
|
|
795
869
|
stdio: ["ignore", "pipe", "pipe"],
|
|
796
870
|
detached: true,
|
|
871
|
+
// Detached console children on Windows allocate their own VISIBLE
|
|
872
|
+
// console window (one per background command — test suites flash
|
|
873
|
+
// dozens). CREATE_NO_WINDOW suppresses it; no-op elsewhere.
|
|
874
|
+
windowsHide: true,
|
|
797
875
|
signal: opts.signal
|
|
798
876
|
});
|
|
799
877
|
const pid2 = child2.pid;
|
|
@@ -827,7 +905,7 @@ var bashTool = {
|
|
|
827
905
|
}
|
|
828
906
|
});
|
|
829
907
|
child2.on("close", () => {
|
|
830
|
-
registry.afterCall(Date.now() - startedAt, false);
|
|
908
|
+
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
831
909
|
});
|
|
832
910
|
if (typeof pid2 === "number") child2.unref();
|
|
833
911
|
yield {
|
|
@@ -846,7 +924,8 @@ var bashTool = {
|
|
|
846
924
|
env,
|
|
847
925
|
stdio: ["ignore", "pipe", "pipe"],
|
|
848
926
|
detached,
|
|
849
|
-
|
|
927
|
+
windowsHide: true,
|
|
928
|
+
...isWin ? {} : { signal: opts.signal }
|
|
850
929
|
});
|
|
851
930
|
const pid = child.pid;
|
|
852
931
|
if (typeof pid === "number") {
|
|
@@ -860,14 +939,27 @@ var bashTool = {
|
|
|
860
939
|
});
|
|
861
940
|
}
|
|
862
941
|
let buf = "";
|
|
863
|
-
let
|
|
942
|
+
let pending2 = "";
|
|
864
943
|
let timedOut = false;
|
|
865
944
|
const timers = [];
|
|
866
945
|
function killWithTimeout(child2, timeoutMs2) {
|
|
867
946
|
if (isWin) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
947
|
+
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
948
|
+
const fallback = setTimeout(() => {
|
|
949
|
+
if (child2.exitCode === null) {
|
|
950
|
+
try {
|
|
951
|
+
child2.kill();
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}, 2e3);
|
|
956
|
+
timers.push(fallback);
|
|
957
|
+
fallback.unref?.();
|
|
958
|
+
} else {
|
|
959
|
+
try {
|
|
960
|
+
child2.kill();
|
|
961
|
+
} catch {
|
|
962
|
+
}
|
|
871
963
|
}
|
|
872
964
|
return;
|
|
873
965
|
}
|
|
@@ -906,6 +998,11 @@ var bashTool = {
|
|
|
906
998
|
}, timeoutMs);
|
|
907
999
|
timers.push(timer);
|
|
908
1000
|
timer.unref?.();
|
|
1001
|
+
const onAbort = () => killWithTimeout(child, 2e3);
|
|
1002
|
+
if (isWin) {
|
|
1003
|
+
if (opts.signal.aborted) onAbort();
|
|
1004
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
1005
|
+
}
|
|
909
1006
|
const queue = [];
|
|
910
1007
|
let resolveNext = null;
|
|
911
1008
|
const push = (c) => {
|
|
@@ -924,24 +1021,38 @@ var bashTool = {
|
|
|
924
1021
|
});
|
|
925
1022
|
let lastFlush = Date.now();
|
|
926
1023
|
const flush = () => {
|
|
927
|
-
if (
|
|
928
|
-
const text =
|
|
929
|
-
|
|
1024
|
+
if (pending2.length === 0) return null;
|
|
1025
|
+
const text = pending2;
|
|
1026
|
+
pending2 = "";
|
|
930
1027
|
lastFlush = Date.now();
|
|
931
1028
|
return text;
|
|
932
1029
|
};
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1030
|
+
let paused = false;
|
|
1031
|
+
const pauseIfFlooded = () => {
|
|
1032
|
+
if (!paused && queue.length >= MAX_QUEUE_CHUNKS) {
|
|
1033
|
+
paused = true;
|
|
1034
|
+
child.stdout?.pause();
|
|
1035
|
+
child.stderr?.pause();
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
const resumeIfDrained = () => {
|
|
1039
|
+
if (paused && queue.length < MAX_QUEUE_CHUNKS) {
|
|
1040
|
+
paused = false;
|
|
1041
|
+
child.stdout?.resume();
|
|
1042
|
+
child.stderr?.resume();
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
const onData = (chunk) => {
|
|
940
1046
|
const text = chunk.toString();
|
|
941
|
-
buf
|
|
942
|
-
|
|
1047
|
+
if (buf.length < MAX_OUTPUT) {
|
|
1048
|
+
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1049
|
+
}
|
|
1050
|
+
pending2 += text;
|
|
943
1051
|
push({ kind: "data", text });
|
|
944
|
-
|
|
1052
|
+
pauseIfFlooded();
|
|
1053
|
+
};
|
|
1054
|
+
child.stdout?.on("data", onData);
|
|
1055
|
+
child.stderr?.on("data", onData);
|
|
945
1056
|
child.on("error", (err) => {
|
|
946
1057
|
for (const t of timers) clearTimeout(t);
|
|
947
1058
|
registry.afterCall(Date.now() - startedAt, true);
|
|
@@ -956,6 +1067,7 @@ var bashTool = {
|
|
|
956
1067
|
try {
|
|
957
1068
|
while (true) {
|
|
958
1069
|
const c = await next();
|
|
1070
|
+
resumeIfDrained();
|
|
959
1071
|
if (c.kind === "error") throw c.err;
|
|
960
1072
|
if (c.kind === "end") {
|
|
961
1073
|
const remainder = flush();
|
|
@@ -973,13 +1085,22 @@ var bashTool = {
|
|
|
973
1085
|
return;
|
|
974
1086
|
}
|
|
975
1087
|
const now = Date.now();
|
|
976
|
-
if (
|
|
1088
|
+
if (pending2.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
|
|
977
1089
|
const text = flush();
|
|
978
1090
|
if (text) yield { type: "partial_output", text };
|
|
979
1091
|
}
|
|
980
1092
|
}
|
|
981
1093
|
} finally {
|
|
982
1094
|
for (const t of timers) clearTimeout(t);
|
|
1095
|
+
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1096
|
+
child.stdout?.off("data", onData);
|
|
1097
|
+
child.stderr?.off("data", onData);
|
|
1098
|
+
child.stdout?.destroy();
|
|
1099
|
+
child.stderr?.destroy();
|
|
1100
|
+
if (child.exitCode === null && !child.killed) {
|
|
1101
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
1102
|
+
else killWithTimeout(child, 2e3);
|
|
1103
|
+
}
|
|
983
1104
|
}
|
|
984
1105
|
}
|
|
985
1106
|
};
|
|
@@ -1088,8 +1209,88 @@ async function executeSingle(call, ctx, opts) {
|
|
|
1088
1209
|
}
|
|
1089
1210
|
}
|
|
1090
1211
|
|
|
1212
|
+
// src/codebase-index/circuit-breaker.ts
|
|
1213
|
+
var CircuitOpenError = class extends Error {
|
|
1214
|
+
name = "CircuitOpenError";
|
|
1215
|
+
};
|
|
1216
|
+
var IndexTimeoutError = class extends Error {
|
|
1217
|
+
name = "IndexTimeoutError";
|
|
1218
|
+
};
|
|
1219
|
+
var LockError = class extends Error {
|
|
1220
|
+
name = "LockError";
|
|
1221
|
+
};
|
|
1222
|
+
var IndexCircuitBreaker = class {
|
|
1223
|
+
failureThreshold;
|
|
1224
|
+
cooldownMs;
|
|
1225
|
+
now;
|
|
1226
|
+
state = "closed";
|
|
1227
|
+
consecutiveFailures = 0;
|
|
1228
|
+
openedAt = 0;
|
|
1229
|
+
lastFailure = null;
|
|
1230
|
+
probeInFlight = false;
|
|
1231
|
+
constructor(opts = {}) {
|
|
1232
|
+
this.failureThreshold = opts.failureThreshold ?? 3;
|
|
1233
|
+
this.cooldownMs = opts.cooldownMs ?? 6e4;
|
|
1234
|
+
this.now = opts.now ?? Date.now;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* True when a run may proceed. An open circuit transitions to half-open once
|
|
1238
|
+
* the cooldown has elapsed, admitting exactly one probe; further requests
|
|
1239
|
+
* are rejected until that probe settles via recordSuccess/recordFailure.
|
|
1240
|
+
*/
|
|
1241
|
+
allowRequest() {
|
|
1242
|
+
if (this.state === "closed") return true;
|
|
1243
|
+
if (this.state === "open") {
|
|
1244
|
+
if (this.now() - this.openedAt < this.cooldownMs) return false;
|
|
1245
|
+
this.state = "half-open";
|
|
1246
|
+
this.probeInFlight = true;
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
if (this.probeInFlight) return false;
|
|
1250
|
+
this.probeInFlight = true;
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
recordSuccess() {
|
|
1254
|
+
this.state = "closed";
|
|
1255
|
+
this.consecutiveFailures = 0;
|
|
1256
|
+
this.lastFailure = null;
|
|
1257
|
+
this.probeInFlight = false;
|
|
1258
|
+
}
|
|
1259
|
+
recordFailure(err) {
|
|
1260
|
+
if (err instanceof LockError) {
|
|
1261
|
+
this.lastFailure = `[transient/lock] ${err.message}`;
|
|
1262
|
+
this.probeInFlight = false;
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
this.lastFailure = err instanceof Error ? err.message : String(err);
|
|
1266
|
+
this.probeInFlight = false;
|
|
1267
|
+
this.consecutiveFailures++;
|
|
1268
|
+
if (this.state === "half-open" || this.consecutiveFailures >= this.failureThreshold) {
|
|
1269
|
+
this.state = "open";
|
|
1270
|
+
this.openedAt = this.now();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/** Force-close the circuit (manual recovery: `/codebase-reindex`). */
|
|
1274
|
+
reset() {
|
|
1275
|
+
this.state = "closed";
|
|
1276
|
+
this.consecutiveFailures = 0;
|
|
1277
|
+
this.lastFailure = null;
|
|
1278
|
+
this.probeInFlight = false;
|
|
1279
|
+
this.openedAt = 0;
|
|
1280
|
+
}
|
|
1281
|
+
snapshot() {
|
|
1282
|
+
return {
|
|
1283
|
+
state: this.state,
|
|
1284
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
1285
|
+
lastFailure: this.lastFailure,
|
|
1286
|
+
cooldownRemainingMs: this.state === "open" ? Math.max(0, this.cooldownMs - (this.now() - this.openedAt)) : 0
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
1291
|
+
|
|
1091
1292
|
// src/codebase-index/schema.ts
|
|
1092
|
-
var SCHEMA_VERSION =
|
|
1293
|
+
var SCHEMA_VERSION = 2;
|
|
1093
1294
|
|
|
1094
1295
|
// src/codebase-index/lsp-kind.ts
|
|
1095
1296
|
function lspKindToInternalKind(k) {
|
|
@@ -1124,6 +1325,94 @@ function lspKindToInternalKind(k) {
|
|
|
1124
1325
|
}
|
|
1125
1326
|
}
|
|
1126
1327
|
|
|
1328
|
+
// src/codebase-index/bm25.ts
|
|
1329
|
+
var K1 = 1.5;
|
|
1330
|
+
var B = 0.75;
|
|
1331
|
+
function tokenise(text) {
|
|
1332
|
+
const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
|
|
1333
|
+
return sanitised.toLowerCase().split(" ").filter(Boolean);
|
|
1334
|
+
}
|
|
1335
|
+
function splitName(name) {
|
|
1336
|
+
return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1337
|
+
}
|
|
1338
|
+
function buildIndexableText(name, signature, docComment) {
|
|
1339
|
+
return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
|
|
1340
|
+
}
|
|
1341
|
+
function buildBm25Index(docs) {
|
|
1342
|
+
const documents = docs.map((d) => {
|
|
1343
|
+
const tokens = tokenise(d.text);
|
|
1344
|
+
return { id: d.id, tokens, raw: d.text, len: tokens.length };
|
|
1345
|
+
});
|
|
1346
|
+
const df = {};
|
|
1347
|
+
for (const doc of documents) {
|
|
1348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1349
|
+
for (const t of doc.tokens) {
|
|
1350
|
+
if (!seen.has(t)) {
|
|
1351
|
+
df[t] = (df[t] ?? 0) + 1;
|
|
1352
|
+
seen.add(t);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const N = documents.length;
|
|
1357
|
+
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
1358
|
+
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
1359
|
+
return new Bm25Index(documents, df, N, avgLen);
|
|
1360
|
+
}
|
|
1361
|
+
var Bm25Index = class {
|
|
1362
|
+
constructor(documents, df, N, avgLen) {
|
|
1363
|
+
this.documents = documents;
|
|
1364
|
+
this.df = df;
|
|
1365
|
+
this.N = N;
|
|
1366
|
+
this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
|
|
1367
|
+
}
|
|
1368
|
+
documents;
|
|
1369
|
+
df;
|
|
1370
|
+
N;
|
|
1371
|
+
safeAvgLen;
|
|
1372
|
+
score(query2, filter) {
|
|
1373
|
+
const qTokens = tokenise(query2);
|
|
1374
|
+
if (qTokens.length === 0) return [];
|
|
1375
|
+
const results = [];
|
|
1376
|
+
for (const doc of this.documents) {
|
|
1377
|
+
if (filter && !filter(doc.id)) continue;
|
|
1378
|
+
let docScore = 0;
|
|
1379
|
+
for (const qTerm of qTokens) {
|
|
1380
|
+
let tf = 0;
|
|
1381
|
+
for (const t of doc.tokens) {
|
|
1382
|
+
if (t === qTerm) tf++;
|
|
1383
|
+
}
|
|
1384
|
+
if (tf === 0) continue;
|
|
1385
|
+
const dfVal = this.df[qTerm] ?? 0;
|
|
1386
|
+
if (dfVal === 0) continue;
|
|
1387
|
+
const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
|
|
1388
|
+
const lenRatio = B * (doc.len / this.safeAvgLen);
|
|
1389
|
+
const tfComponent = tf * (K1 + 1) / (tf + K1 * (1 - B + lenRatio));
|
|
1390
|
+
docScore += idf * tfComponent;
|
|
1391
|
+
}
|
|
1392
|
+
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
1393
|
+
}
|
|
1394
|
+
return results;
|
|
1395
|
+
}
|
|
1396
|
+
getDoc(id) {
|
|
1397
|
+
return this.documents.find((d) => d.id === id);
|
|
1398
|
+
}
|
|
1399
|
+
extractSnippet(docId, queryTokens, radius = 40) {
|
|
1400
|
+
const doc = this.getDoc(docId);
|
|
1401
|
+
if (!doc) return "";
|
|
1402
|
+
for (const tok of queryTokens) {
|
|
1403
|
+
const idx = doc.raw.toLowerCase().indexOf(tok);
|
|
1404
|
+
if (idx !== -1) {
|
|
1405
|
+
const start = Math.max(0, idx - radius);
|
|
1406
|
+
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
1407
|
+
const excerpt = doc.raw.slice(start, end);
|
|
1408
|
+
const ellipsis = "\u2026";
|
|
1409
|
+
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return doc.raw.slice(0, radius * 2) + (doc.raw.length > radius * 2 ? "\u2026" : "");
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1127
1416
|
// src/codebase-index/writer.ts
|
|
1128
1417
|
var DB_FILE = "index.db";
|
|
1129
1418
|
function resolveIndexDir(projectRoot, override) {
|
|
@@ -1159,15 +1448,79 @@ function loadDatabaseSync() {
|
|
|
1159
1448
|
}
|
|
1160
1449
|
return DatabaseSyncCtor;
|
|
1161
1450
|
}
|
|
1451
|
+
var MAX_LOCK_RETRIES = 3;
|
|
1452
|
+
var LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
1453
|
+
var LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
1454
|
+
function isLockError(err) {
|
|
1455
|
+
if (!(err instanceof Error)) return false;
|
|
1456
|
+
const e = err;
|
|
1457
|
+
const code = e.code ?? e.sqliteCode;
|
|
1458
|
+
if (typeof code === "string" && /SQLITE_(BUSY|LOCKED)/.test(code)) return true;
|
|
1459
|
+
if (typeof code === "number" && (code === 5 || code === 6)) return true;
|
|
1460
|
+
if (/SQLITE_(BUSY|LOCKED)/.test(err.message)) return true;
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
function sleepSync(ms) {
|
|
1464
|
+
try {
|
|
1465
|
+
const sab = new SharedArrayBuffer(4);
|
|
1466
|
+
const view = new Int32Array(sab);
|
|
1467
|
+
Atomics.wait(view, 0, 0, ms);
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1162
1471
|
var IndexStore = class {
|
|
1163
1472
|
db;
|
|
1164
1473
|
/** Absolute path to this project's index directory. */
|
|
1165
1474
|
indexDir;
|
|
1475
|
+
/**
|
|
1476
|
+
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
1477
|
+
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
1478
|
+
*/
|
|
1479
|
+
ftsAvailable = false;
|
|
1480
|
+
/**
|
|
1481
|
+
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
1482
|
+
*
|
|
1483
|
+
* When another wstack process is holding the write lock the statement first
|
|
1484
|
+
* waits up to `busy_timeout` ms, then throws SQLITE_BUSY. This wrapper catches
|
|
1485
|
+
* that error and retries (up to MAX_LOCK_RETRIES) with exponential backoff,
|
|
1486
|
+
* giving the competing writer time to finish and release the lock.
|
|
1487
|
+
*
|
|
1488
|
+
* @param fn The write operation to execute. Can return a value which is
|
|
1489
|
+
* returned to the caller on success.
|
|
1490
|
+
* @throws {@link LockError} when all retries are exhausted on a lock conflict
|
|
1491
|
+
* (non-lock errors always propagate on the first attempt).
|
|
1492
|
+
*/
|
|
1493
|
+
runWithRetry(fn) {
|
|
1494
|
+
let lastError;
|
|
1495
|
+
for (let attempt = 0; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
1496
|
+
try {
|
|
1497
|
+
return fn();
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
lastError = err;
|
|
1500
|
+
if (!isLockError(err)) throw err;
|
|
1501
|
+
if (attempt === MAX_LOCK_RETRIES) {
|
|
1502
|
+
const msg = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1503
|
+
throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
|
|
1504
|
+
}
|
|
1505
|
+
const delay = Math.min(
|
|
1506
|
+
LOCK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
1507
|
+
LOCK_RETRY_MAX_DELAY_MS
|
|
1508
|
+
);
|
|
1509
|
+
sleepSync(delay);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
throw lastError;
|
|
1513
|
+
}
|
|
1166
1514
|
constructor(projectRoot, opts = {}) {
|
|
1167
1515
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
1168
1516
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
1169
1517
|
const Database = loadDatabaseSync();
|
|
1170
1518
|
this.db = new Database(path2.join(this.indexDir, DB_FILE));
|
|
1519
|
+
try {
|
|
1520
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1521
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1171
1524
|
this.initSchema();
|
|
1172
1525
|
}
|
|
1173
1526
|
initSchema() {
|
|
@@ -1176,6 +1529,21 @@ var IndexStore = class {
|
|
|
1176
1529
|
key TEXT PRIMARY KEY,
|
|
1177
1530
|
value TEXT NOT NULL
|
|
1178
1531
|
);
|
|
1532
|
+
`);
|
|
1533
|
+
const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
|
|
1534
|
+
const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
|
|
1535
|
+
if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
|
|
1536
|
+
this.db.exec(`
|
|
1537
|
+
DROP TABLE IF EXISTS symbols;
|
|
1538
|
+
DROP TABLE IF EXISTS files;
|
|
1539
|
+
DROP TABLE IF EXISTS refs;
|
|
1540
|
+
`);
|
|
1541
|
+
this.db.exec("DROP TABLE IF EXISTS symbols_fts");
|
|
1542
|
+
this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
|
|
1543
|
+
} else if (storedVersion === null) {
|
|
1544
|
+
this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
|
|
1545
|
+
}
|
|
1546
|
+
this.db.exec(`
|
|
1179
1547
|
CREATE TABLE IF NOT EXISTS files (
|
|
1180
1548
|
file TEXT PRIMARY KEY,
|
|
1181
1549
|
lang TEXT NOT NULL,
|
|
@@ -1216,53 +1584,76 @@ var IndexStore = class {
|
|
|
1216
1584
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)");
|
|
1217
1585
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
|
|
1218
1586
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
this.
|
|
1587
|
+
try {
|
|
1588
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
|
|
1589
|
+
this.ftsAvailable = true;
|
|
1590
|
+
} catch {
|
|
1591
|
+
this.ftsAvailable = false;
|
|
1222
1592
|
}
|
|
1223
1593
|
}
|
|
1224
1594
|
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
1225
1595
|
insertSymbols(symbols, nextId) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
let id = nextId;
|
|
1231
|
-
for (const s of symbols) {
|
|
1232
|
-
stmt.run(
|
|
1233
|
-
id++,
|
|
1234
|
-
s.lang,
|
|
1235
|
-
s.kind,
|
|
1236
|
-
s.name,
|
|
1237
|
-
s.file,
|
|
1238
|
-
s.line,
|
|
1239
|
-
s.col,
|
|
1240
|
-
s.signature,
|
|
1241
|
-
s.docComment,
|
|
1242
|
-
s.scope,
|
|
1243
|
-
s.text,
|
|
1244
|
-
s.file
|
|
1596
|
+
return this.runWithRetry(() => {
|
|
1597
|
+
const stmt = this.db.prepare(
|
|
1598
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
1599
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1245
1600
|
);
|
|
1246
|
-
|
|
1247
|
-
|
|
1601
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
1602
|
+
let id = nextId;
|
|
1603
|
+
for (const s of symbols) {
|
|
1604
|
+
stmt.run(
|
|
1605
|
+
id,
|
|
1606
|
+
s.lang,
|
|
1607
|
+
s.kind,
|
|
1608
|
+
s.name,
|
|
1609
|
+
s.file,
|
|
1610
|
+
s.line,
|
|
1611
|
+
s.col,
|
|
1612
|
+
s.signature,
|
|
1613
|
+
s.docComment,
|
|
1614
|
+
s.scope,
|
|
1615
|
+
s.text,
|
|
1616
|
+
s.file
|
|
1617
|
+
);
|
|
1618
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
1619
|
+
id++;
|
|
1620
|
+
}
|
|
1621
|
+
return id;
|
|
1622
|
+
});
|
|
1248
1623
|
}
|
|
1249
1624
|
deleteSymbolsForFile(file) {
|
|
1250
|
-
this.
|
|
1625
|
+
this.runWithRetry(() => {
|
|
1626
|
+
if (this.ftsAvailable) {
|
|
1627
|
+
this.db.prepare("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
1628
|
+
}
|
|
1629
|
+
this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
1630
|
+
});
|
|
1251
1631
|
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
1634
|
+
* when a source file disappears between index runs — previously this only
|
|
1635
|
+
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
1636
|
+
*/
|
|
1252
1637
|
deleteFile(file) {
|
|
1253
|
-
this.
|
|
1638
|
+
this.runWithRetry(() => {
|
|
1639
|
+
this.deleteRefsForFile(file);
|
|
1640
|
+
this.deleteSymbolsForFile(file);
|
|
1641
|
+
this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
|
|
1642
|
+
});
|
|
1254
1643
|
}
|
|
1255
1644
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
1256
1645
|
upsertFile(meta) {
|
|
1257
|
-
this.
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1646
|
+
this.runWithRetry(() => {
|
|
1647
|
+
this.db.prepare(
|
|
1648
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
1649
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1650
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
1651
|
+
lang = excluded.lang,
|
|
1652
|
+
mtime_ms = excluded.mtime_ms,
|
|
1653
|
+
symbol_count = excluded.symbol_count,
|
|
1654
|
+
last_indexed = excluded.last_indexed`
|
|
1655
|
+
).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
|
|
1656
|
+
});
|
|
1266
1657
|
}
|
|
1267
1658
|
getFileMeta(file) {
|
|
1268
1659
|
const rows = this.db.prepare(
|
|
@@ -1329,6 +1720,94 @@ var IndexStore = class {
|
|
|
1329
1720
|
lspKind: filter?.lspKind
|
|
1330
1721
|
}));
|
|
1331
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
1725
|
+
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
1726
|
+
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
1727
|
+
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
1728
|
+
*
|
|
1729
|
+
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
1730
|
+
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
1731
|
+
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
1732
|
+
*/
|
|
1733
|
+
searchRanked(query2, filter, limit) {
|
|
1734
|
+
const tokens = tokenise(query2);
|
|
1735
|
+
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
1736
|
+
return this.searchRankedFallback(query2, filter, limit);
|
|
1737
|
+
}
|
|
1738
|
+
let effectiveKind = filter?.kind;
|
|
1739
|
+
if (filter?.lspKind !== void 0) {
|
|
1740
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
1741
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
1742
|
+
effectiveKind = mapped;
|
|
1743
|
+
}
|
|
1744
|
+
const match = tokens.map((t) => `"${t.replaceAll('"', "")}"*`).join(" OR ");
|
|
1745
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
1746
|
+
const values = [match];
|
|
1747
|
+
if (effectiveKind) {
|
|
1748
|
+
conditions.push("s.kind = ?");
|
|
1749
|
+
values.push(effectiveKind);
|
|
1750
|
+
}
|
|
1751
|
+
if (filter?.lang) {
|
|
1752
|
+
conditions.push("s.lang = ?");
|
|
1753
|
+
values.push(filter.lang);
|
|
1754
|
+
}
|
|
1755
|
+
if (filter?.file) {
|
|
1756
|
+
conditions.push("s.file LIKE ?");
|
|
1757
|
+
values.push(`%${filter.file}%`);
|
|
1758
|
+
}
|
|
1759
|
+
const where = conditions.join(" AND ");
|
|
1760
|
+
const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
|
|
1761
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
1762
|
+
if (total === 0) return { results: [], total: 0 };
|
|
1763
|
+
const rows = this.db.prepare(
|
|
1764
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
1765
|
+
-bm25(symbols_fts) AS score,
|
|
1766
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
1767
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
1768
|
+
WHERE ${where}
|
|
1769
|
+
ORDER BY bm25(symbols_fts)
|
|
1770
|
+
LIMIT ?`
|
|
1771
|
+
).all(...values, limit);
|
|
1772
|
+
return {
|
|
1773
|
+
results: rows.map((r) => ({
|
|
1774
|
+
id: r.id,
|
|
1775
|
+
lang: r.lang,
|
|
1776
|
+
kind: r.kind,
|
|
1777
|
+
name: r.name,
|
|
1778
|
+
file: r.file,
|
|
1779
|
+
line: r.line,
|
|
1780
|
+
col: r.col,
|
|
1781
|
+
signature: r.signature,
|
|
1782
|
+
docComment: r.doc_comment,
|
|
1783
|
+
// bm25() is negative-is-better; negate so callers keep "higher is
|
|
1784
|
+
// better" and clamp so a match never reports a zero score.
|
|
1785
|
+
score: Math.max(1e-4, r.score),
|
|
1786
|
+
snippet: r.snippet,
|
|
1787
|
+
lspKind: filter?.lspKind
|
|
1788
|
+
})),
|
|
1789
|
+
total
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
1793
|
+
searchRankedFallback(query2, filter, limit) {
|
|
1794
|
+
const candidates = this.search(query2, filter);
|
|
1795
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
1796
|
+
if (!query2.trim()) {
|
|
1797
|
+
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
1798
|
+
}
|
|
1799
|
+
const bm25 = buildBm25Index(
|
|
1800
|
+
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
1801
|
+
);
|
|
1802
|
+
const scored = bm25.score(query2, (id) => candidates.some((c) => c.id === id));
|
|
1803
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1804
|
+
const qTokens = tokenise(query2);
|
|
1805
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
1806
|
+
const c = expectDefined(candidates.find((cand) => cand.id === id));
|
|
1807
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
1808
|
+
});
|
|
1809
|
+
return { results, total: candidates.length };
|
|
1810
|
+
}
|
|
1332
1811
|
getAllIndexable() {
|
|
1333
1812
|
return this.db.prepare("SELECT id, text FROM symbols").all().map(
|
|
1334
1813
|
({ id, text }) => ({ id, text })
|
|
@@ -1378,14 +1857,19 @@ var IndexStore = class {
|
|
|
1378
1857
|
};
|
|
1379
1858
|
}
|
|
1380
1859
|
setLastIndexed(ts2) {
|
|
1381
|
-
this.
|
|
1382
|
-
|
|
1383
|
-
|
|
1860
|
+
this.runWithRetry(() => {
|
|
1861
|
+
this.db.prepare(
|
|
1862
|
+
"INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
|
|
1863
|
+
).run(String(ts2));
|
|
1864
|
+
});
|
|
1384
1865
|
}
|
|
1385
1866
|
clearAll() {
|
|
1386
|
-
this.
|
|
1387
|
-
|
|
1388
|
-
|
|
1867
|
+
this.runWithRetry(() => {
|
|
1868
|
+
this.db.exec("DELETE FROM symbols");
|
|
1869
|
+
this.db.exec("DELETE FROM files");
|
|
1870
|
+
this.db.exec("DELETE FROM refs");
|
|
1871
|
+
if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
|
|
1872
|
+
});
|
|
1389
1873
|
}
|
|
1390
1874
|
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
1391
1875
|
/**
|
|
@@ -1393,46 +1877,52 @@ var IndexStore = class {
|
|
|
1393
1877
|
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
1394
1878
|
*/
|
|
1395
1879
|
insertRefs(fromId, refs) {
|
|
1396
|
-
this.
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1880
|
+
this.runWithRetry(() => {
|
|
1881
|
+
this.db.prepare("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
1882
|
+
if (refs.length === 0) return;
|
|
1883
|
+
const stmt = this.db.prepare(
|
|
1884
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
1885
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1886
|
+
);
|
|
1887
|
+
for (const ref of refs) {
|
|
1888
|
+
stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1405
1891
|
}
|
|
1406
1892
|
/**
|
|
1407
1893
|
* Delete all refs whose source symbols are in a given file.
|
|
1408
1894
|
* Used when re-indexing a file to clear stale refs.
|
|
1409
1895
|
*/
|
|
1410
1896
|
deleteRefsForFile(file) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1897
|
+
this.runWithRetry(() => {
|
|
1898
|
+
const ids = this.db.prepare(
|
|
1899
|
+
"SELECT id FROM symbols WHERE file = ?"
|
|
1900
|
+
).all(file);
|
|
1901
|
+
if (!ids.length) return;
|
|
1902
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1903
|
+
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
1904
|
+
});
|
|
1417
1905
|
}
|
|
1418
1906
|
/**
|
|
1419
1907
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
1420
1908
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
1421
1909
|
*/
|
|
1422
1910
|
resolveRefs() {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1911
|
+
return this.runWithRetry(() => {
|
|
1912
|
+
const unresolved = this.db.prepare(
|
|
1913
|
+
"SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
|
|
1914
|
+
).all();
|
|
1915
|
+
let resolved = 0;
|
|
1916
|
+
for (const row of unresolved) {
|
|
1917
|
+
const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
|
|
1918
|
+
const first = target[0];
|
|
1919
|
+
if (first) {
|
|
1920
|
+
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
1921
|
+
resolved++;
|
|
1922
|
+
}
|
|
1433
1923
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1924
|
+
return resolved;
|
|
1925
|
+
});
|
|
1436
1926
|
}
|
|
1437
1927
|
/**
|
|
1438
1928
|
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
@@ -2193,7 +2683,7 @@ function parseSymbols4(opts) {
|
|
|
2193
2683
|
}
|
|
2194
2684
|
function checkNativeParser() {
|
|
2195
2685
|
try {
|
|
2196
|
-
execFileSync("rustc", ["--version"], { stdio: "pipe" });
|
|
2686
|
+
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
2197
2687
|
const toolsDir = path2.join(process.cwd(), "tools");
|
|
2198
2688
|
try {
|
|
2199
2689
|
execFileSync(
|
|
@@ -2206,7 +2696,7 @@ function checkNativeParser() {
|
|
|
2206
2696
|
"--manifest-path",
|
|
2207
2697
|
path2.join(toolsDir, "Cargo.toml")
|
|
2208
2698
|
],
|
|
2209
|
-
{ stdio: "pipe" }
|
|
2699
|
+
{ stdio: "pipe", windowsHide: true }
|
|
2210
2700
|
);
|
|
2211
2701
|
return true;
|
|
2212
2702
|
} catch {
|
|
@@ -2229,7 +2719,8 @@ function tryNativeParse(file, content) {
|
|
|
2229
2719
|
cwd: process.cwd(),
|
|
2230
2720
|
encoding: "utf8",
|
|
2231
2721
|
timeout: 15e3,
|
|
2232
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2722
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2723
|
+
windowsHide: true
|
|
2233
2724
|
}
|
|
2234
2725
|
);
|
|
2235
2726
|
if (result.status === 0 && result.stdout) {
|
|
@@ -2643,10 +3134,6 @@ function isScalar(value) {
|
|
|
2643
3134
|
if (/^'[^']*'$/.test(value) || /^"[^"]*"$/.test(value)) return true;
|
|
2644
3135
|
return false;
|
|
2645
3136
|
}
|
|
2646
|
-
function truncate(s, max) {
|
|
2647
|
-
if (s.length <= max) return s;
|
|
2648
|
-
return s.slice(0, max) + "...";
|
|
2649
|
-
}
|
|
2650
3137
|
function makeSymbol2(opts) {
|
|
2651
3138
|
return {
|
|
2652
3139
|
id: 0,
|
|
@@ -2706,43 +3193,13 @@ function compileGitignore(lines) {
|
|
|
2706
3193
|
async function loadGitignoreMatcher(projectRoot) {
|
|
2707
3194
|
let lines = [];
|
|
2708
3195
|
try {
|
|
2709
|
-
const raw = await
|
|
3196
|
+
const raw = await fs14.readFile(path2.join(projectRoot, ".gitignore"), "utf8");
|
|
2710
3197
|
lines = raw.split("\n");
|
|
2711
3198
|
} catch {
|
|
2712
3199
|
}
|
|
2713
3200
|
return compileGitignore(lines);
|
|
2714
3201
|
}
|
|
2715
3202
|
|
|
2716
|
-
// src/codebase-index/background-indexer.ts
|
|
2717
|
-
var _ready = false;
|
|
2718
|
-
var _indexing = false;
|
|
2719
|
-
var _currentFile = 0;
|
|
2720
|
-
var _totalFiles = 0;
|
|
2721
|
-
var _lastError = null;
|
|
2722
|
-
function setIndexReady() {
|
|
2723
|
-
_ready = true;
|
|
2724
|
-
}
|
|
2725
|
-
function getIndexState() {
|
|
2726
|
-
return {
|
|
2727
|
-
ready: _ready,
|
|
2728
|
-
indexing: _indexing,
|
|
2729
|
-
currentFile: _currentFile,
|
|
2730
|
-
totalFiles: _totalFiles,
|
|
2731
|
-
lastError: _lastError
|
|
2732
|
-
};
|
|
2733
|
-
}
|
|
2734
|
-
var _listeners = [];
|
|
2735
|
-
function emitState() {
|
|
2736
|
-
const state = getIndexState();
|
|
2737
|
-
for (const l of _listeners) l(state);
|
|
2738
|
-
}
|
|
2739
|
-
function _setIndexProgress(current, total) {
|
|
2740
|
-
_currentFile = current;
|
|
2741
|
-
_totalFiles = total;
|
|
2742
|
-
emitState();
|
|
2743
|
-
}
|
|
2744
|
-
Promise.resolve();
|
|
2745
|
-
|
|
2746
3203
|
// src/codebase-index/indexer.ts
|
|
2747
3204
|
var YIELD_EVERY_N = 50;
|
|
2748
3205
|
function yieldEventLoop() {
|
|
@@ -2793,7 +3250,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
2793
3250
|
}
|
|
2794
3251
|
let entries;
|
|
2795
3252
|
try {
|
|
2796
|
-
entries = await
|
|
3253
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
2797
3254
|
} catch {
|
|
2798
3255
|
return;
|
|
2799
3256
|
}
|
|
@@ -2842,8 +3299,18 @@ async function parseFile(file, content, lang) {
|
|
|
2842
3299
|
}
|
|
2843
3300
|
}
|
|
2844
3301
|
async function runIndexer(_ctx, opts) {
|
|
2845
|
-
const
|
|
2846
|
-
|
|
3302
|
+
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
3303
|
+
try {
|
|
3304
|
+
return await runIndexerWithStore(store, opts);
|
|
3305
|
+
} finally {
|
|
3306
|
+
try {
|
|
3307
|
+
store.close();
|
|
3308
|
+
} catch {
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
async function runIndexerWithStore(store, opts) {
|
|
3313
|
+
const { projectRoot, force = false, langs, ignore = [], signal } = opts;
|
|
2847
3314
|
const startMs = Date.now();
|
|
2848
3315
|
const errors = [];
|
|
2849
3316
|
const langStats = {};
|
|
@@ -2870,7 +3337,7 @@ async function runIndexer(_ctx, opts) {
|
|
|
2870
3337
|
}
|
|
2871
3338
|
for (let fi = 0; fi < files.length; fi++) {
|
|
2872
3339
|
const file = expectDefined(files[fi]);
|
|
2873
|
-
|
|
3340
|
+
opts.onProgress?.(fi + 1, files.length);
|
|
2874
3341
|
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
2875
3342
|
await yieldEventLoop();
|
|
2876
3343
|
throwIfAborted(signal);
|
|
@@ -2878,7 +3345,7 @@ async function runIndexer(_ctx, opts) {
|
|
|
2878
3345
|
let stat10;
|
|
2879
3346
|
try {
|
|
2880
3347
|
const statOpts = signal ? { signal } : {};
|
|
2881
|
-
stat10 = await
|
|
3348
|
+
stat10 = await fs14.stat(file, statOpts);
|
|
2882
3349
|
} catch (e) {
|
|
2883
3350
|
if (isAbortError(e)) throw e;
|
|
2884
3351
|
store.deleteFile(file);
|
|
@@ -2898,7 +3365,7 @@ async function runIndexer(_ctx, opts) {
|
|
|
2898
3365
|
store.deleteSymbolsForFile(file);
|
|
2899
3366
|
let content;
|
|
2900
3367
|
try {
|
|
2901
|
-
content = await
|
|
3368
|
+
content = await fs14.readFile(file, { encoding: "utf8", signal });
|
|
2902
3369
|
} catch (e) {
|
|
2903
3370
|
if (isAbortError(e)) throw e;
|
|
2904
3371
|
errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -2949,14 +3416,13 @@ async function runIndexer(_ctx, opts) {
|
|
|
2949
3416
|
}
|
|
2950
3417
|
for (const [file_] of existingMeta) {
|
|
2951
3418
|
try {
|
|
2952
|
-
await
|
|
3419
|
+
await fs14.stat(file_);
|
|
2953
3420
|
} catch {
|
|
2954
3421
|
store.deleteFile(file_);
|
|
2955
3422
|
}
|
|
2956
3423
|
}
|
|
2957
3424
|
const durationMs = Date.now() - startMs;
|
|
2958
3425
|
store.setLastIndexed(Date.now());
|
|
2959
|
-
store.close();
|
|
2960
3426
|
return {
|
|
2961
3427
|
filesIndexed,
|
|
2962
3428
|
symbolsIndexed,
|
|
@@ -2966,128 +3432,343 @@ async function runIndexer(_ctx, opts) {
|
|
|
2966
3432
|
};
|
|
2967
3433
|
}
|
|
2968
3434
|
|
|
2969
|
-
// src/codebase-index/
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
3435
|
+
// src/codebase-index/index-service.ts
|
|
3436
|
+
function stubCtx(projectRoot) {
|
|
3437
|
+
return {
|
|
3438
|
+
projectRoot,
|
|
3439
|
+
cwd: projectRoot,
|
|
3440
|
+
messages: [],
|
|
3441
|
+
todos: [],
|
|
3442
|
+
readFiles: /* @__PURE__ */ new Set(),
|
|
3443
|
+
fileMtimes: /* @__PURE__ */ new Map()
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
async function indexService(args, hooks = {}) {
|
|
3447
|
+
return runIndexer(stubCtx(args.projectRoot), {
|
|
3448
|
+
projectRoot: args.projectRoot,
|
|
3449
|
+
indexDir: args.indexDir,
|
|
3450
|
+
files: args.files,
|
|
3451
|
+
force: args.force,
|
|
3452
|
+
langs: args.langs,
|
|
3453
|
+
ignore: args.ignore,
|
|
3454
|
+
signal: hooks.signal,
|
|
3455
|
+
onProgress: hooks.onProgress
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
function searchService(args) {
|
|
3459
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3460
|
+
try {
|
|
3461
|
+
return store.searchRanked(
|
|
3462
|
+
args.query,
|
|
3463
|
+
{
|
|
3464
|
+
kind: args.kind,
|
|
3465
|
+
lang: args.lang,
|
|
3466
|
+
file: args.file,
|
|
3467
|
+
lspKind: args.lspKind
|
|
2985
3468
|
},
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
},
|
|
2993
|
-
async execute(input, ctx, execOpts) {
|
|
2994
|
-
const result = await runIndexer(ctx, {
|
|
2995
|
-
projectRoot: ctx.projectRoot,
|
|
2996
|
-
force: input.force ?? false,
|
|
2997
|
-
langs: input.langs,
|
|
2998
|
-
indexDir: codebaseIndexDirOverride(ctx),
|
|
2999
|
-
signal: execOpts?.signal
|
|
3000
|
-
});
|
|
3001
|
-
setIndexReady();
|
|
3002
|
-
return result;
|
|
3469
|
+
args.limit
|
|
3470
|
+
);
|
|
3471
|
+
} finally {
|
|
3472
|
+
store.close();
|
|
3003
3473
|
}
|
|
3004
|
-
}
|
|
3474
|
+
}
|
|
3475
|
+
function statsService(args) {
|
|
3476
|
+
const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
|
|
3477
|
+
try {
|
|
3478
|
+
return store.getStats();
|
|
3479
|
+
} finally {
|
|
3480
|
+
store.close();
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3005
3483
|
|
|
3006
|
-
// src/codebase-index/
|
|
3007
|
-
var
|
|
3008
|
-
var
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3484
|
+
// src/codebase-index/background-indexer.ts
|
|
3485
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 12e4;
|
|
3486
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
3487
|
+
var _ready = false;
|
|
3488
|
+
var _indexing = false;
|
|
3489
|
+
var _currentFile = 0;
|
|
3490
|
+
var _totalFiles = 0;
|
|
3491
|
+
var _lastError = null;
|
|
3492
|
+
function isIndexing() {
|
|
3493
|
+
return _indexing;
|
|
3012
3494
|
}
|
|
3013
|
-
function
|
|
3014
|
-
return
|
|
3495
|
+
function getIndexState() {
|
|
3496
|
+
return {
|
|
3497
|
+
ready: _ready,
|
|
3498
|
+
indexing: _indexing,
|
|
3499
|
+
currentFile: _currentFile,
|
|
3500
|
+
totalFiles: _totalFiles,
|
|
3501
|
+
lastError: _lastError,
|
|
3502
|
+
circuit: indexCircuitBreaker.snapshot()
|
|
3503
|
+
};
|
|
3015
3504
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3505
|
+
var _listeners = [];
|
|
3506
|
+
function emitState() {
|
|
3507
|
+
const state = getIndexState();
|
|
3508
|
+
for (const l of _listeners) l(state);
|
|
3018
3509
|
}
|
|
3019
|
-
function
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3510
|
+
function setIndexProgress(current, total) {
|
|
3511
|
+
_currentFile = current;
|
|
3512
|
+
_totalFiles = total;
|
|
3513
|
+
emitState();
|
|
3514
|
+
}
|
|
3515
|
+
var worker = null;
|
|
3516
|
+
var workerUnavailable = false;
|
|
3517
|
+
var nextRpcId = 1;
|
|
3518
|
+
var pending = /* @__PURE__ */ new Map();
|
|
3519
|
+
function resolveWorkerUrl() {
|
|
3520
|
+
if (process.env["WRONGSTACK_INDEX_INLINE"]) return null;
|
|
3521
|
+
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
3522
|
+
try {
|
|
3523
|
+
const url = new URL(rel, import.meta.url);
|
|
3524
|
+
if (url.protocol === "file:" && fs.existsSync(fileURLToPath(url))) return url;
|
|
3525
|
+
} catch {
|
|
3032
3526
|
}
|
|
3033
3527
|
}
|
|
3034
|
-
|
|
3035
|
-
const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
|
|
3036
|
-
const avgLen = N === 0 ? 0 : totalLen / N;
|
|
3037
|
-
return new Bm25Index(documents, df, N, avgLen);
|
|
3528
|
+
return null;
|
|
3038
3529
|
}
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3530
|
+
function failAllPending(err) {
|
|
3531
|
+
const entries = [...pending.values()];
|
|
3532
|
+
pending.clear();
|
|
3533
|
+
for (const p of entries) p.reject(err);
|
|
3534
|
+
}
|
|
3535
|
+
function ensureWorker() {
|
|
3536
|
+
if (worker) return worker;
|
|
3537
|
+
if (workerUnavailable) return null;
|
|
3538
|
+
const url = resolveWorkerUrl();
|
|
3539
|
+
if (!url) {
|
|
3540
|
+
workerUnavailable = true;
|
|
3541
|
+
return null;
|
|
3045
3542
|
}
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
if (
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3543
|
+
try {
|
|
3544
|
+
const w = new Worker(url, { name: "wstack-codebase-index" });
|
|
3545
|
+
w.unref();
|
|
3546
|
+
w.on("message", (msg) => {
|
|
3547
|
+
if (msg.type === "progress") {
|
|
3548
|
+
pending.get(msg.id)?.onProgress?.(msg.current, msg.total);
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
const entry = pending.get(msg.id);
|
|
3552
|
+
if (!entry) return;
|
|
3553
|
+
pending.delete(msg.id);
|
|
3554
|
+
if (msg.ok) entry.resolve(msg.result);
|
|
3555
|
+
else entry.reject(new Error(msg.error));
|
|
3556
|
+
});
|
|
3557
|
+
w.on("error", (err) => {
|
|
3558
|
+
worker = null;
|
|
3559
|
+
failAllPending(err);
|
|
3560
|
+
});
|
|
3561
|
+
w.on("exit", () => {
|
|
3562
|
+
if (worker === w) worker = null;
|
|
3563
|
+
failAllPending(new Error("codebase-index worker exited"));
|
|
3564
|
+
});
|
|
3565
|
+
worker = w;
|
|
3566
|
+
return w;
|
|
3567
|
+
} catch {
|
|
3568
|
+
workerUnavailable = true;
|
|
3569
|
+
return null;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
function terminateWorker(reason) {
|
|
3573
|
+
const w = worker;
|
|
3574
|
+
worker = null;
|
|
3575
|
+
failAllPending(reason);
|
|
3576
|
+
if (w) void w.terminate().catch(() => {
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
function callIndexOp(op, args, opts) {
|
|
3580
|
+
const w = ensureWorker();
|
|
3581
|
+
if (!w) return callInline(op, args, opts);
|
|
3582
|
+
return new Promise((resolve7, reject) => {
|
|
3583
|
+
const id = nextRpcId++;
|
|
3584
|
+
const timer = setTimeout(() => {
|
|
3585
|
+
pending.delete(id);
|
|
3586
|
+
const err = new IndexTimeoutError(
|
|
3587
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
3588
|
+
);
|
|
3589
|
+
terminateWorker(err);
|
|
3590
|
+
reject(err);
|
|
3591
|
+
}, opts.timeoutMs);
|
|
3592
|
+
timer.unref?.();
|
|
3593
|
+
const onAbort = () => {
|
|
3594
|
+
w.postMessage({ type: "cancel", id });
|
|
3595
|
+
};
|
|
3596
|
+
if (opts.signal?.aborted) onAbort();
|
|
3597
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3598
|
+
const cleanup = () => {
|
|
3599
|
+
clearTimeout(timer);
|
|
3600
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
3601
|
+
};
|
|
3602
|
+
pending.set(id, {
|
|
3603
|
+
resolve: (v) => {
|
|
3604
|
+
cleanup();
|
|
3605
|
+
resolve7(v);
|
|
3606
|
+
},
|
|
3607
|
+
reject: (e) => {
|
|
3608
|
+
cleanup();
|
|
3609
|
+
reject(e);
|
|
3610
|
+
},
|
|
3611
|
+
onProgress: opts.onProgress
|
|
3612
|
+
});
|
|
3613
|
+
w.postMessage({ type: "request", id, op, args });
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3616
|
+
async function callInline(op, args, opts) {
|
|
3617
|
+
const ac = new AbortController();
|
|
3618
|
+
const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
|
|
3619
|
+
if (opts.signal?.aborted) onOuterAbort();
|
|
3620
|
+
else opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
3621
|
+
let timer;
|
|
3622
|
+
const watchdog = new Promise((_, reject) => {
|
|
3623
|
+
timer = setTimeout(() => {
|
|
3624
|
+
const err = new IndexTimeoutError(
|
|
3625
|
+
`Index ${op} exceeded its ${opts.timeoutMs}ms watchdog timeout`
|
|
3626
|
+
);
|
|
3627
|
+
ac.abort(err);
|
|
3628
|
+
reject(err);
|
|
3629
|
+
}, opts.timeoutMs);
|
|
3630
|
+
timer.unref?.();
|
|
3631
|
+
});
|
|
3632
|
+
const job = async () => {
|
|
3633
|
+
switch (op) {
|
|
3634
|
+
case "index":
|
|
3635
|
+
return await indexService(args, {
|
|
3636
|
+
signal: ac.signal,
|
|
3637
|
+
onProgress: opts.onProgress
|
|
3638
|
+
});
|
|
3639
|
+
case "search":
|
|
3640
|
+
return searchService(args);
|
|
3641
|
+
case "stats":
|
|
3642
|
+
return statsService(args);
|
|
3643
|
+
default:
|
|
3644
|
+
throw new Error(`unknown index op: ${String(op)}`);
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
try {
|
|
3648
|
+
return await Promise.race([job(), watchdog]);
|
|
3649
|
+
} finally {
|
|
3650
|
+
if (timer) clearTimeout(timer);
|
|
3651
|
+
opts.signal?.removeEventListener("abort", onOuterAbort);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
var chain = Promise.resolve();
|
|
3655
|
+
function withMutex(job) {
|
|
3656
|
+
const run = chain.then(job, job);
|
|
3657
|
+
chain = run.then(
|
|
3658
|
+
() => void 0,
|
|
3659
|
+
() => void 0
|
|
3660
|
+
);
|
|
3661
|
+
return run;
|
|
3662
|
+
}
|
|
3663
|
+
function circuitOpenError() {
|
|
3664
|
+
const c = indexCircuitBreaker.snapshot();
|
|
3665
|
+
return new CircuitOpenError(
|
|
3666
|
+
"Codebase indexing is temporarily paused after repeated failures" + (c.lastFailure ? ` (last: ${c.lastFailure})` : "") + (c.cooldownRemainingMs > 0 ? `; auto-retry in ${Math.ceil(c.cooldownRemainingMs / 1e3)}s` : "") + ". Use /codebase-reindex to retry now."
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
async function runStartupIndex(opts) {
|
|
3670
|
+
if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
|
|
3671
|
+
_indexing = true;
|
|
3672
|
+
emitState();
|
|
3673
|
+
try {
|
|
3674
|
+
const result = await withMutex(() => {
|
|
3675
|
+
_currentFile = 0;
|
|
3676
|
+
_totalFiles = 0;
|
|
3677
|
+
_lastError = null;
|
|
3678
|
+
return callIndexOp(
|
|
3679
|
+
"index",
|
|
3680
|
+
{
|
|
3681
|
+
projectRoot: opts.projectRoot,
|
|
3682
|
+
indexDir: opts.indexDir,
|
|
3683
|
+
force: opts.force,
|
|
3684
|
+
langs: opts.langs
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_FULL_INDEX_TIMEOUT_MS,
|
|
3688
|
+
signal: opts.signal,
|
|
3689
|
+
onProgress: setIndexProgress
|
|
3061
3690
|
}
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3691
|
+
);
|
|
3692
|
+
});
|
|
3693
|
+
_ready = true;
|
|
3694
|
+
indexCircuitBreaker.recordSuccess();
|
|
3695
|
+
return result;
|
|
3696
|
+
} catch (err) {
|
|
3697
|
+
_lastError = err instanceof Error ? err.message : String(err);
|
|
3698
|
+
_ready = true;
|
|
3699
|
+
if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
|
|
3700
|
+
throw err;
|
|
3701
|
+
} finally {
|
|
3702
|
+
_indexing = false;
|
|
3703
|
+
emitState();
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
async function searchCodebaseIndex(args, opts = {}) {
|
|
3707
|
+
return callIndexOp("search", args, {
|
|
3708
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3709
|
+
signal: opts.signal
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
async function codebaseIndexStats(args, opts = {}) {
|
|
3713
|
+
return callIndexOp("stats", args, {
|
|
3714
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3715
|
+
signal: opts.signal
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/codebase-index/codebase-index-tool.ts
|
|
3720
|
+
var codebaseIndexTool = {
|
|
3721
|
+
name: "codebase-index",
|
|
3722
|
+
category: "Project",
|
|
3723
|
+
description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
|
|
3724
|
+
usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
|
|
3725
|
+
permission: "confirm",
|
|
3726
|
+
mutating: true,
|
|
3727
|
+
capabilities: ["fs.write.outside-project"],
|
|
3728
|
+
timeoutMs: 12e4,
|
|
3729
|
+
inputSchema: {
|
|
3730
|
+
type: "object",
|
|
3731
|
+
properties: {
|
|
3732
|
+
force: {
|
|
3733
|
+
type: "boolean",
|
|
3734
|
+
description: "Force a full reindex \u2014 clears the index first and reindexes all files."
|
|
3735
|
+
},
|
|
3736
|
+
langs: {
|
|
3737
|
+
type: "array",
|
|
3738
|
+
items: { type: "string" },
|
|
3739
|
+
description: "Limit reindex to specific languages: ts, tsx, js, jsx, go, py, rs"
|
|
3069
3740
|
}
|
|
3070
|
-
if (docScore > 0) results.push({ id: doc.id, score: docScore });
|
|
3071
3741
|
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
const start = Math.max(0, idx - radius);
|
|
3084
|
-
const end = Math.min(doc.raw.length, idx + tok.length + radius);
|
|
3085
|
-
const excerpt = doc.raw.slice(start, end);
|
|
3086
|
-
const ellipsis = "\u2026";
|
|
3087
|
-
return (start > 0 ? ellipsis : "") + excerpt + (end < doc.raw.length ? ellipsis : "");
|
|
3088
|
-
}
|
|
3742
|
+
},
|
|
3743
|
+
async execute(input, ctx, execOpts) {
|
|
3744
|
+
if (isIndexing()) {
|
|
3745
|
+
return {
|
|
3746
|
+
filesIndexed: 0,
|
|
3747
|
+
symbolsIndexed: 0,
|
|
3748
|
+
langStats: {},
|
|
3749
|
+
durationMs: 0,
|
|
3750
|
+
errors: [],
|
|
3751
|
+
note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
|
|
3752
|
+
};
|
|
3089
3753
|
}
|
|
3090
|
-
|
|
3754
|
+
const circuit = indexCircuitBreaker.snapshot();
|
|
3755
|
+
if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
|
|
3756
|
+
return {
|
|
3757
|
+
filesIndexed: 0,
|
|
3758
|
+
symbolsIndexed: 0,
|
|
3759
|
+
langStats: {},
|
|
3760
|
+
durationMs: 0,
|
|
3761
|
+
errors: [],
|
|
3762
|
+
note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
return await runStartupIndex({
|
|
3766
|
+
projectRoot: ctx.projectRoot,
|
|
3767
|
+
force: input.force ?? false,
|
|
3768
|
+
langs: input.langs,
|
|
3769
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
3770
|
+
signal: execOpts?.signal
|
|
3771
|
+
});
|
|
3091
3772
|
}
|
|
3092
3773
|
};
|
|
3093
3774
|
|
|
@@ -3133,7 +3814,7 @@ var codebaseSearchTool = {
|
|
|
3133
3814
|
},
|
|
3134
3815
|
required: ["query"]
|
|
3135
3816
|
},
|
|
3136
|
-
async execute(input, ctx) {
|
|
3817
|
+
async execute(input, ctx, execOpts) {
|
|
3137
3818
|
const state = getIndexState();
|
|
3138
3819
|
if (!state.ready) {
|
|
3139
3820
|
return {
|
|
@@ -3152,51 +3833,30 @@ var codebaseSearchTool = {
|
|
|
3152
3833
|
};
|
|
3153
3834
|
}
|
|
3154
3835
|
if (state.lastError) {
|
|
3836
|
+
const circuit = state.circuit;
|
|
3837
|
+
const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
|
|
3155
3838
|
return {
|
|
3156
3839
|
results: [],
|
|
3157
3840
|
total: 0,
|
|
3158
3841
|
query: input.query,
|
|
3159
|
-
indexStatus: `Index build failed: ${state.lastError}.
|
|
3842
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
3160
3843
|
};
|
|
3161
3844
|
}
|
|
3162
|
-
const
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3845
|
+
const limit = Math.min(input.limit ?? 20, 100);
|
|
3846
|
+
const { results, total } = await searchCodebaseIndex(
|
|
3847
|
+
{
|
|
3848
|
+
projectRoot: ctx.projectRoot,
|
|
3849
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
3850
|
+
query: input.query,
|
|
3166
3851
|
kind: input.kind,
|
|
3167
3852
|
lang: input.lang,
|
|
3168
3853
|
file: input.file,
|
|
3169
|
-
lspKind: input.lspKind
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
id: c.id,
|
|
3176
|
-
text: buildIndexableText(c.name, c.signature, c.docComment)
|
|
3177
|
-
}));
|
|
3178
|
-
const bm25 = buildBm25Index(indexable);
|
|
3179
|
-
const scored = bm25.score(input.query, (id) => candidates.some((c) => c.id === id));
|
|
3180
|
-
scored.sort((a, b) => b.score - a.score);
|
|
3181
|
-
const top = scored.slice(0, limit);
|
|
3182
|
-
const qTokens = tokenise(input.query);
|
|
3183
|
-
const results = top.map(({ id, score }) => {
|
|
3184
|
-
const c = expectDefined(candidates.find((c2) => c2.id === id));
|
|
3185
|
-
const snippet = bm25.extractSnippet(id, qTokens);
|
|
3186
|
-
return {
|
|
3187
|
-
...c,
|
|
3188
|
-
score,
|
|
3189
|
-
snippet
|
|
3190
|
-
};
|
|
3191
|
-
});
|
|
3192
|
-
return {
|
|
3193
|
-
results,
|
|
3194
|
-
total: candidates.length,
|
|
3195
|
-
query: input.query
|
|
3196
|
-
};
|
|
3197
|
-
} finally {
|
|
3198
|
-
store.close();
|
|
3199
|
-
}
|
|
3854
|
+
lspKind: input.lspKind,
|
|
3855
|
+
limit
|
|
3856
|
+
},
|
|
3857
|
+
{ signal: execOpts?.signal }
|
|
3858
|
+
);
|
|
3859
|
+
return { results, total, query: input.query };
|
|
3200
3860
|
}
|
|
3201
3861
|
};
|
|
3202
3862
|
|
|
@@ -3215,7 +3875,7 @@ var codebaseStatsTool = {
|
|
|
3215
3875
|
properties: {},
|
|
3216
3876
|
additionalProperties: false
|
|
3217
3877
|
},
|
|
3218
|
-
async execute(_input, ctx) {
|
|
3878
|
+
async execute(_input, ctx, execOpts) {
|
|
3219
3879
|
const idxState = getIndexState();
|
|
3220
3880
|
if (!idxState.ready) {
|
|
3221
3881
|
return {
|
|
@@ -3230,34 +3890,30 @@ var codebaseStatsTool = {
|
|
|
3230
3890
|
indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
|
|
3231
3891
|
};
|
|
3232
3892
|
}
|
|
3893
|
+
const stats = await codebaseIndexStats(
|
|
3894
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
3895
|
+
{ signal: execOpts?.signal }
|
|
3896
|
+
);
|
|
3233
3897
|
if (idxState.indexing) {
|
|
3234
|
-
const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3235
|
-
try {
|
|
3236
|
-
const stats = store2.getStats();
|
|
3237
|
-
return {
|
|
3238
|
-
...stats,
|
|
3239
|
-
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
3240
|
-
};
|
|
3241
|
-
} finally {
|
|
3242
|
-
store2.close();
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
|
|
3246
|
-
try {
|
|
3247
|
-
const stats = store.getStats();
|
|
3248
3898
|
return {
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
byLang: stats.byLang,
|
|
3252
|
-
byKind: stats.byKind,
|
|
3253
|
-
lastIndexed: stats.lastIndexed,
|
|
3254
|
-
sizeBytes: stats.sizeBytes,
|
|
3255
|
-
indexPath: stats.indexPath,
|
|
3256
|
-
version: stats.version
|
|
3899
|
+
...stats,
|
|
3900
|
+
indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
|
|
3257
3901
|
};
|
|
3258
|
-
} finally {
|
|
3259
|
-
store.close();
|
|
3260
3902
|
}
|
|
3903
|
+
const circuit = idxState.circuit;
|
|
3904
|
+
return {
|
|
3905
|
+
totalSymbols: stats.totalSymbols,
|
|
3906
|
+
totalFiles: stats.totalFiles,
|
|
3907
|
+
byLang: stats.byLang,
|
|
3908
|
+
byKind: stats.byKind,
|
|
3909
|
+
lastIndexed: stats.lastIndexed,
|
|
3910
|
+
sizeBytes: stats.sizeBytes,
|
|
3911
|
+
indexPath: stats.indexPath,
|
|
3912
|
+
version: stats.version,
|
|
3913
|
+
...circuit.state === "open" ? {
|
|
3914
|
+
indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
|
|
3915
|
+
} : {}
|
|
3916
|
+
};
|
|
3261
3917
|
}
|
|
3262
3918
|
};
|
|
3263
3919
|
var diffTool = {
|
|
@@ -3359,7 +4015,8 @@ function runGit(args, cwd, signal) {
|
|
|
3359
4015
|
cwd,
|
|
3360
4016
|
signal,
|
|
3361
4017
|
env: buildChildEnv(),
|
|
3362
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4018
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4019
|
+
windowsHide: true
|
|
3363
4020
|
});
|
|
3364
4021
|
child.stdout?.on("data", (c) => {
|
|
3365
4022
|
stdout += c.toString();
|
|
@@ -3385,9 +4042,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
3385
4042
|
const results = [];
|
|
3386
4043
|
for (const file of files) {
|
|
3387
4044
|
const absPath = safeResolve(file, ctx);
|
|
3388
|
-
const stat10 = await
|
|
4045
|
+
const stat10 = await fs14.stat(absPath).catch(() => null);
|
|
3389
4046
|
if (!stat10?.isFile()) continue;
|
|
3390
|
-
const content = await
|
|
4047
|
+
const content = await fs14.readFile(absPath, "utf8");
|
|
3391
4048
|
const lines = content.split(/\r?\n/);
|
|
3392
4049
|
results.push(formatWithLineNumbers(file, lines));
|
|
3393
4050
|
}
|
|
@@ -3449,7 +4106,7 @@ var documentTool = {
|
|
|
3449
4106
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
3450
4107
|
for (const absPath of fileList) {
|
|
3451
4108
|
try {
|
|
3452
|
-
const content = await
|
|
4109
|
+
const content = await fs14.readFile(absPath, "utf8");
|
|
3453
4110
|
filesProcessed++;
|
|
3454
4111
|
const processed = processFile(
|
|
3455
4112
|
content,
|
|
@@ -3485,7 +4142,7 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
3485
4142
|
for (const f of files) {
|
|
3486
4143
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
3487
4144
|
try {
|
|
3488
|
-
const stat10 = await
|
|
4145
|
+
const stat10 = await fs14.stat(absPath);
|
|
3489
4146
|
if (stat10.isFile()) resolved.push(absPath);
|
|
3490
4147
|
} catch {
|
|
3491
4148
|
}
|
|
@@ -3577,7 +4234,7 @@ var editTool = {
|
|
|
3577
4234
|
if (input.new_string === void 0) throw new Error("edit: new_string is required");
|
|
3578
4235
|
if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
|
|
3579
4236
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
3580
|
-
const stat10 = await
|
|
4237
|
+
const stat10 = await fs14.stat(absPath).catch((err) => {
|
|
3581
4238
|
if (err.code === "ENOENT") {
|
|
3582
4239
|
throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
|
|
3583
4240
|
}
|
|
@@ -3587,8 +4244,8 @@ var editTool = {
|
|
|
3587
4244
|
if (!ctx.hasRead(absPath)) {
|
|
3588
4245
|
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
3589
4246
|
}
|
|
3590
|
-
const original = await
|
|
3591
|
-
const updated = await
|
|
4247
|
+
const original = await fs14.readFile(absPath, "utf8");
|
|
4248
|
+
const updated = await fs14.stat(absPath);
|
|
3592
4249
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
3593
4250
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
3594
4251
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
@@ -3628,7 +4285,7 @@ var editTool = {
|
|
|
3628
4285
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
3629
4286
|
const newFile = toStyle(newFileLf, style);
|
|
3630
4287
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
3631
|
-
const written = await
|
|
4288
|
+
const written = await fs14.stat(absPath);
|
|
3632
4289
|
ctx.recordRead(absPath, written.mtimeMs);
|
|
3633
4290
|
ctx.session.recordFileChange({
|
|
3634
4291
|
path: absPath,
|
|
@@ -3878,12 +4535,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3878
4535
|
let killed = false;
|
|
3879
4536
|
const startedAt = Date.now();
|
|
3880
4537
|
const resolved = resolveWin32Command(cmd);
|
|
3881
|
-
const
|
|
4538
|
+
const isWin = process.platform === "win32";
|
|
4539
|
+
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
3882
4540
|
const child = spawn(resolved, args, {
|
|
3883
4541
|
cwd,
|
|
3884
|
-
signal,
|
|
3885
4542
|
env: buildChildEnv(sessionId),
|
|
3886
4543
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4544
|
+
windowsHide: true,
|
|
4545
|
+
...isWin ? {} : { signal },
|
|
3887
4546
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
3888
4547
|
});
|
|
3889
4548
|
const registry = getProcessRegistry();
|
|
@@ -3897,6 +4556,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3897
4556
|
if (typeof pid === "number") registry.kill(pid);
|
|
3898
4557
|
else child.kill("SIGTERM");
|
|
3899
4558
|
}, timeout);
|
|
4559
|
+
const onAbort = () => {
|
|
4560
|
+
killed = true;
|
|
4561
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
4562
|
+
else child.kill("SIGTERM");
|
|
4563
|
+
};
|
|
4564
|
+
if (isWin) {
|
|
4565
|
+
if (signal.aborted) onAbort();
|
|
4566
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
4567
|
+
}
|
|
3900
4568
|
child.stdout?.on("data", (chunk) => {
|
|
3901
4569
|
if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
|
|
3902
4570
|
});
|
|
@@ -3905,6 +4573,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3905
4573
|
});
|
|
3906
4574
|
child.on("close", (code) => {
|
|
3907
4575
|
clearTimeout(timer);
|
|
4576
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
3908
4577
|
if (typeof pid === "number") registry.unregister(pid);
|
|
3909
4578
|
const durationMs = Date.now() - startedAt;
|
|
3910
4579
|
const exitCode = killed ? 124 : code ?? 1;
|
|
@@ -3921,6 +4590,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3921
4590
|
});
|
|
3922
4591
|
child.on("error", (err) => {
|
|
3923
4592
|
clearTimeout(timer);
|
|
4593
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
3924
4594
|
if (typeof pid === "number") registry.unregister(pid);
|
|
3925
4595
|
registry.afterCall(Date.now() - startedAt, true);
|
|
3926
4596
|
resolve7({
|
|
@@ -4591,7 +5261,8 @@ function runGit2(args, cwd, signal) {
|
|
|
4591
5261
|
cwd,
|
|
4592
5262
|
signal,
|
|
4593
5263
|
env: buildChildEnv(),
|
|
4594
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5264
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5265
|
+
windowsHide: true
|
|
4595
5266
|
});
|
|
4596
5267
|
child.stdout?.on("data", (chunk) => {
|
|
4597
5268
|
if (stdout.length < MAX_OUTPUT3) {
|
|
@@ -4667,7 +5338,7 @@ var globTool = {
|
|
|
4667
5338
|
}
|
|
4668
5339
|
let entries;
|
|
4669
5340
|
try {
|
|
4670
|
-
entries = await
|
|
5341
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
4671
5342
|
} catch {
|
|
4672
5343
|
return;
|
|
4673
5344
|
}
|
|
@@ -4683,7 +5354,7 @@ var globTool = {
|
|
|
4683
5354
|
} else if (e.isFile()) {
|
|
4684
5355
|
if (re.test(rel) || re.test(name)) {
|
|
4685
5356
|
try {
|
|
4686
|
-
const st = await
|
|
5357
|
+
const st = await fs14.stat(full);
|
|
4687
5358
|
results.push({ rel: full, mtime: st.mtimeMs });
|
|
4688
5359
|
if (results.length >= limit) {
|
|
4689
5360
|
truncated = true;
|
|
@@ -4702,7 +5373,7 @@ var globTool = {
|
|
|
4702
5373
|
};
|
|
4703
5374
|
async function readGitignore(dir) {
|
|
4704
5375
|
try {
|
|
4705
|
-
const raw = await
|
|
5376
|
+
const raw = await fs14.readFile(path2.join(dir, ".gitignore"), "utf8");
|
|
4706
5377
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
4707
5378
|
} catch {
|
|
4708
5379
|
return [];
|
|
@@ -4836,7 +5507,7 @@ var grepTool = {
|
|
|
4836
5507
|
async function detectRg(signal) {
|
|
4837
5508
|
return new Promise((resolve7) => {
|
|
4838
5509
|
try {
|
|
4839
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
|
|
5510
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
|
|
4840
5511
|
p.on("error", () => resolve7(false));
|
|
4841
5512
|
p.on("close", (code) => resolve7(code === 0));
|
|
4842
5513
|
} catch {
|
|
@@ -4866,7 +5537,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
4866
5537
|
const FLUSH_AT = 16;
|
|
4867
5538
|
const MAX_BUF_BYTES = 1e6;
|
|
4868
5539
|
let bufOverflow = false;
|
|
4869
|
-
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
5540
|
+
const child = spawn("rg", args, { signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
4870
5541
|
const queue = [];
|
|
4871
5542
|
let waiter;
|
|
4872
5543
|
const wake = () => {
|
|
@@ -4986,7 +5657,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
4986
5657
|
if (stopped || signal.aborted) return;
|
|
4987
5658
|
let entries;
|
|
4988
5659
|
try {
|
|
4989
|
-
entries = await
|
|
5660
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
4990
5661
|
} catch {
|
|
4991
5662
|
return;
|
|
4992
5663
|
}
|
|
@@ -5001,9 +5672,9 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5001
5672
|
if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
|
|
5002
5673
|
if (globRe) globRe.lastIndex = 0;
|
|
5003
5674
|
try {
|
|
5004
|
-
const stat10 = await
|
|
5675
|
+
const stat10 = await fs14.stat(full);
|
|
5005
5676
|
if (stat10.size > 1e6) continue;
|
|
5006
|
-
const head = await
|
|
5677
|
+
const head = await fs14.readFile(full);
|
|
5007
5678
|
if (isBinaryBuffer(head)) continue;
|
|
5008
5679
|
const text = head.toString("utf8");
|
|
5009
5680
|
const lines = text.split(/\r?\n/);
|
|
@@ -5042,8 +5713,6 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5042
5713
|
used: "native"
|
|
5043
5714
|
};
|
|
5044
5715
|
}
|
|
5045
|
-
|
|
5046
|
-
// src/install.ts
|
|
5047
5716
|
var installTool = {
|
|
5048
5717
|
name: "install",
|
|
5049
5718
|
category: "Package Management",
|
|
@@ -5138,18 +5807,48 @@ var installTool = {
|
|
|
5138
5807
|
signal: opts.signal,
|
|
5139
5808
|
maxBytes: 1e5
|
|
5140
5809
|
});
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
dry_run: args.includes("--dry-run"),
|
|
5148
|
-
truncated: result.truncated
|
|
5149
|
-
}
|
|
5810
|
+
const output = {
|
|
5811
|
+
packages: pkgList,
|
|
5812
|
+
exit_code: result.exitCode,
|
|
5813
|
+
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
5814
|
+
dry_run: args.includes("--dry-run"),
|
|
5815
|
+
truncated: result.truncated
|
|
5150
5816
|
};
|
|
5817
|
+
const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
|
|
5818
|
+
if (isSuccess && pkgList.length > 0) {
|
|
5819
|
+
const trackerOpts = ctx.meta?.["packageTrackerOpts"];
|
|
5820
|
+
if (trackerOpts) {
|
|
5821
|
+
const manifestPath = resolveManifestPath(cwd, pkgManager);
|
|
5822
|
+
for (const pkg of pkgList) {
|
|
5823
|
+
try {
|
|
5824
|
+
await recordPackageAction(trackerOpts, {
|
|
5825
|
+
manifestPath,
|
|
5826
|
+
packageName: pkg,
|
|
5827
|
+
versionSpec: "latest",
|
|
5828
|
+
// exact version resolved by package manager at install time
|
|
5829
|
+
ecosystem: detectPackageEcosystem(manifestPath),
|
|
5830
|
+
agentId: ctx.agentId,
|
|
5831
|
+
agentName: ctx.agentName,
|
|
5832
|
+
sessionId: ctx.session.id
|
|
5833
|
+
});
|
|
5834
|
+
} catch {
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
yield { type: "final", output };
|
|
5151
5840
|
}
|
|
5152
5841
|
};
|
|
5842
|
+
function resolveManifestPath(cwd, pkgManager) {
|
|
5843
|
+
switch (pkgManager) {
|
|
5844
|
+
case "pnpm":
|
|
5845
|
+
case "yarn":
|
|
5846
|
+
case "npm":
|
|
5847
|
+
return join(cwd, "package.json");
|
|
5848
|
+
default:
|
|
5849
|
+
return join(cwd, "package.json");
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5153
5852
|
var jsonTool = {
|
|
5154
5853
|
name: "json",
|
|
5155
5854
|
category: "Data",
|
|
@@ -5184,7 +5883,7 @@ var jsonTool = {
|
|
|
5184
5883
|
let raw;
|
|
5185
5884
|
if (input.file) {
|
|
5186
5885
|
try {
|
|
5187
|
-
raw = await
|
|
5886
|
+
raw = await fs14.readFile(input.file, "utf8");
|
|
5188
5887
|
} catch {
|
|
5189
5888
|
return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
|
|
5190
5889
|
}
|
|
@@ -5463,7 +6162,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
5463
6162
|
clearTimeout(timer);
|
|
5464
6163
|
resolve7(result);
|
|
5465
6164
|
};
|
|
5466
|
-
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
6165
|
+
const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
5467
6166
|
const timer = setTimeout(() => {
|
|
5468
6167
|
child.kill("SIGTERM");
|
|
5469
6168
|
finish(empty());
|
|
@@ -5609,7 +6308,7 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
5609
6308
|
const MAX = 1e5;
|
|
5610
6309
|
const resolved = resolveWin32Command(manager);
|
|
5611
6310
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
5612
|
-
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
6311
|
+
const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
5613
6312
|
child.stdout?.on("data", (c) => {
|
|
5614
6313
|
if (stdout.length < MAX) stdout += c.toString();
|
|
5615
6314
|
});
|
|
@@ -5705,12 +6404,12 @@ var patchTool = {
|
|
|
5705
6404
|
};
|
|
5706
6405
|
}
|
|
5707
6406
|
}
|
|
5708
|
-
const tmpDir = await
|
|
6407
|
+
const tmpDir = await fs14.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
|
|
5709
6408
|
try {
|
|
5710
|
-
await
|
|
6409
|
+
await fs14.chmod(tmpDir, 448).catch(() => {
|
|
5711
6410
|
});
|
|
5712
6411
|
const patchFile = path2.join(tmpDir, "in.diff");
|
|
5713
|
-
await
|
|
6412
|
+
await fs14.writeFile(patchFile, input.patch, { mode: 384 });
|
|
5714
6413
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
5715
6414
|
const result = await runPatch(args, dir, opts.signal);
|
|
5716
6415
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -5731,7 +6430,7 @@ var patchTool = {
|
|
|
5731
6430
|
message: result.stdout || "patch applied"
|
|
5732
6431
|
};
|
|
5733
6432
|
} finally {
|
|
5734
|
-
await
|
|
6433
|
+
await fs14.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
5735
6434
|
});
|
|
5736
6435
|
}
|
|
5737
6436
|
}
|
|
@@ -5756,7 +6455,7 @@ function runPatch(args, cwd, signal) {
|
|
|
5756
6455
|
let stdout = "";
|
|
5757
6456
|
let stderr = "";
|
|
5758
6457
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
5759
|
-
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
6458
|
+
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
5760
6459
|
child.stdout?.on("data", (c) => {
|
|
5761
6460
|
stdout += c.toString();
|
|
5762
6461
|
});
|
|
@@ -5779,7 +6478,7 @@ var planTool = {
|
|
|
5779
6478
|
name: "plan",
|
|
5780
6479
|
category: "Session",
|
|
5781
6480
|
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.",
|
|
5782
|
-
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.',
|
|
6481
|
+
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.',
|
|
5783
6482
|
permission: "confirm",
|
|
5784
6483
|
mutating: true,
|
|
5785
6484
|
capabilities: ["fs.write"],
|
|
@@ -5796,9 +6495,9 @@ var planTool = {
|
|
|
5796
6495
|
"done",
|
|
5797
6496
|
"remove",
|
|
5798
6497
|
"promote",
|
|
5799
|
-
"derive",
|
|
5800
6498
|
"template_use",
|
|
5801
|
-
"clear"
|
|
6499
|
+
"clear",
|
|
6500
|
+
"taskify"
|
|
5802
6501
|
],
|
|
5803
6502
|
description: "The operation to perform on the plan board."
|
|
5804
6503
|
},
|
|
@@ -5817,7 +6516,7 @@ var planTool = {
|
|
|
5817
6516
|
subtasks: {
|
|
5818
6517
|
type: "array",
|
|
5819
6518
|
items: { type: "string" },
|
|
5820
|
-
description: "List of subtask titles. Used with promote
|
|
6519
|
+
description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
|
|
5821
6520
|
},
|
|
5822
6521
|
template: {
|
|
5823
6522
|
type: "string",
|
|
@@ -5838,92 +6537,151 @@ var planTool = {
|
|
|
5838
6537
|
};
|
|
5839
6538
|
}
|
|
5840
6539
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
5841
|
-
let
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
if (!input.target) {
|
|
5857
|
-
return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6540
|
+
let early = null;
|
|
6541
|
+
const taskifyMeta = { title: "", details: "" };
|
|
6542
|
+
let didTaskify = false;
|
|
6543
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
6544
|
+
switch (input.action) {
|
|
6545
|
+
case "show":
|
|
6546
|
+
break;
|
|
6547
|
+
case "add": {
|
|
6548
|
+
const title = input.title?.trim();
|
|
6549
|
+
if (!title) {
|
|
6550
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
6551
|
+
return p;
|
|
6552
|
+
}
|
|
6553
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
6554
|
+
return updated;
|
|
5858
6555
|
}
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
input.target
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
6556
|
+
case "start":
|
|
6557
|
+
case "done": {
|
|
6558
|
+
if (!input.target) {
|
|
6559
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6560
|
+
return p;
|
|
6561
|
+
}
|
|
6562
|
+
const next = setPlanItemStatus(
|
|
6563
|
+
p,
|
|
6564
|
+
input.target,
|
|
6565
|
+
input.action === "start" ? "in_progress" : "done"
|
|
6566
|
+
);
|
|
6567
|
+
if (next === p) {
|
|
6568
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6569
|
+
return p;
|
|
6570
|
+
}
|
|
6571
|
+
return next;
|
|
5866
6572
|
}
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
6573
|
+
case "remove": {
|
|
6574
|
+
if (!input.target) {
|
|
6575
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
6576
|
+
return p;
|
|
6577
|
+
}
|
|
6578
|
+
const next = removePlanItem(p, input.target);
|
|
6579
|
+
if (next === p) {
|
|
6580
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6581
|
+
return p;
|
|
6582
|
+
}
|
|
6583
|
+
return next;
|
|
5874
6584
|
}
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
6585
|
+
case "promote": {
|
|
6586
|
+
if (!input.target) {
|
|
6587
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6588
|
+
return p;
|
|
6589
|
+
}
|
|
6590
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
6591
|
+
if (!derived) {
|
|
6592
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6593
|
+
return p;
|
|
6594
|
+
}
|
|
6595
|
+
ctx.state.replaceTodos(derived.todos);
|
|
6596
|
+
early = mkResult(
|
|
6597
|
+
derived.plan,
|
|
6598
|
+
true,
|
|
6599
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
6600
|
+
derived.todos
|
|
6601
|
+
);
|
|
6602
|
+
return derived.plan;
|
|
5878
6603
|
}
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
6604
|
+
case "template_use": {
|
|
6605
|
+
const templateName = input.template?.trim();
|
|
6606
|
+
if (!templateName) {
|
|
6607
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
6608
|
+
return p;
|
|
6609
|
+
}
|
|
6610
|
+
const template = getPlanTemplate(templateName);
|
|
6611
|
+
if (!template) {
|
|
6612
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
6613
|
+
return p;
|
|
6614
|
+
}
|
|
6615
|
+
let updated = p;
|
|
6616
|
+
for (const item of template.items) {
|
|
6617
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
6618
|
+
}
|
|
6619
|
+
early = mkResult(
|
|
6620
|
+
updated,
|
|
6621
|
+
true,
|
|
6622
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
6623
|
+
);
|
|
6624
|
+
return updated;
|
|
5887
6625
|
}
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
6626
|
+
case "clear":
|
|
6627
|
+
return clearPlan(p);
|
|
6628
|
+
case "taskify": {
|
|
6629
|
+
if (!input.target) {
|
|
6630
|
+
early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
|
|
6631
|
+
return p;
|
|
6632
|
+
}
|
|
6633
|
+
let itemIdx = -1;
|
|
6634
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
6635
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
6636
|
+
itemIdx = asNum - 1;
|
|
6637
|
+
} else {
|
|
6638
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
6639
|
+
if (itemIdx === -1) {
|
|
6640
|
+
const lower = input.target.toLowerCase();
|
|
6641
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
6645
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6646
|
+
return p;
|
|
6647
|
+
}
|
|
6648
|
+
const item = p.items[itemIdx];
|
|
6649
|
+
taskifyMeta.title = item.title;
|
|
6650
|
+
taskifyMeta.details = item.details ?? "";
|
|
6651
|
+
didTaskify = true;
|
|
6652
|
+
break;
|
|
5891
6653
|
}
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
return mkResult(
|
|
5896
|
-
plan,
|
|
5897
|
-
true,
|
|
5898
|
-
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
5899
|
-
derived.todos
|
|
5900
|
-
);
|
|
6654
|
+
default:
|
|
6655
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
6656
|
+
return p;
|
|
5901
6657
|
}
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
return mkResult(plan, false, `Unknown template "${templateName}".`);
|
|
5910
|
-
}
|
|
5911
|
-
for (const item of template.items) {
|
|
5912
|
-
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
5913
|
-
}
|
|
5914
|
-
await savePlan(planPath, plan);
|
|
5915
|
-
return mkResult(
|
|
5916
|
-
plan,
|
|
5917
|
-
true,
|
|
5918
|
-
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
5919
|
-
);
|
|
6658
|
+
return p;
|
|
6659
|
+
});
|
|
6660
|
+
if (early) return early;
|
|
6661
|
+
if (didTaskify) {
|
|
6662
|
+
const taskPath = ctx.meta["task.path"];
|
|
6663
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
6664
|
+
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
5920
6665
|
}
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
6666
|
+
const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
|
|
6667
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6668
|
+
taskFile.tasks.push({
|
|
6669
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
6670
|
+
title: taskifyMeta.title,
|
|
6671
|
+
description: taskifyMeta.details || void 0,
|
|
6672
|
+
type: "feature",
|
|
6673
|
+
priority: "medium",
|
|
6674
|
+
status: "pending",
|
|
6675
|
+
createdAt: now,
|
|
6676
|
+
updatedAt: now
|
|
6677
|
+
});
|
|
6678
|
+
await saveTasks(taskPath, taskFile);
|
|
6679
|
+
return mkResult(
|
|
6680
|
+
plan,
|
|
6681
|
+
true,
|
|
6682
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
6683
|
+
${formatTaskList(taskFile.tasks)}`
|
|
6684
|
+
);
|
|
5927
6685
|
}
|
|
5928
6686
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
5929
6687
|
}
|
|
@@ -5974,7 +6732,7 @@ var readTool = {
|
|
|
5974
6732
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
5975
6733
|
let stat10;
|
|
5976
6734
|
try {
|
|
5977
|
-
stat10 = await
|
|
6735
|
+
stat10 = await fs14.stat(absPath);
|
|
5978
6736
|
} catch (err) {
|
|
5979
6737
|
const code = err.code;
|
|
5980
6738
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
@@ -5986,7 +6744,7 @@ var readTool = {
|
|
|
5986
6744
|
if (stat10.size > MAX_BYTES2) {
|
|
5987
6745
|
throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
|
|
5988
6746
|
}
|
|
5989
|
-
const buf = await
|
|
6747
|
+
const buf = await fs14.readFile(absPath);
|
|
5990
6748
|
if (isBinaryBuffer(buf)) {
|
|
5991
6749
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
5992
6750
|
}
|
|
@@ -6054,11 +6812,11 @@ var replaceTool = {
|
|
|
6054
6812
|
const dryRun = input.dry_run ?? false;
|
|
6055
6813
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
6056
6814
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
6057
|
-
const realRoot = await
|
|
6815
|
+
const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
6058
6816
|
const results = [];
|
|
6059
6817
|
let totalReplacements = 0;
|
|
6060
6818
|
for (const absPath of fileList) {
|
|
6061
|
-
const lstat2 = await
|
|
6819
|
+
const lstat2 = await fs14.lstat(absPath).catch((err) => {
|
|
6062
6820
|
if (err.code === "ENOENT") return null;
|
|
6063
6821
|
throw err;
|
|
6064
6822
|
});
|
|
@@ -6066,17 +6824,17 @@ var replaceTool = {
|
|
|
6066
6824
|
if (lstat2.isSymbolicLink()) continue;
|
|
6067
6825
|
let realPath;
|
|
6068
6826
|
try {
|
|
6069
|
-
realPath = await
|
|
6827
|
+
realPath = await fs14.realpath(absPath);
|
|
6070
6828
|
} catch {
|
|
6071
6829
|
continue;
|
|
6072
6830
|
}
|
|
6073
6831
|
const rel = path2.relative(realRoot, realPath);
|
|
6074
6832
|
if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
|
|
6075
|
-
const stat10 = await
|
|
6833
|
+
const stat10 = await fs14.stat(realPath).catch(() => null);
|
|
6076
6834
|
if (!stat10 || !stat10.isFile()) continue;
|
|
6077
6835
|
let content;
|
|
6078
6836
|
try {
|
|
6079
|
-
const buf = await
|
|
6837
|
+
const buf = await fs14.readFile(realPath);
|
|
6080
6838
|
if (isBinaryBuffer(buf)) continue;
|
|
6081
6839
|
content = buf.toString("utf8");
|
|
6082
6840
|
} catch {
|
|
@@ -6128,7 +6886,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
6128
6886
|
const resolved = [];
|
|
6129
6887
|
for (const p of parts) {
|
|
6130
6888
|
const absPath = safeResolve(p, ctx);
|
|
6131
|
-
const stat10 = await
|
|
6889
|
+
const stat10 = await fs14.stat(absPath).catch(() => null);
|
|
6132
6890
|
if (stat10?.isFile()) {
|
|
6133
6891
|
resolved.push(absPath);
|
|
6134
6892
|
}
|
|
@@ -6149,7 +6907,7 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
6149
6907
|
function checkRg() {
|
|
6150
6908
|
return new Promise((resolve7) => {
|
|
6151
6909
|
try {
|
|
6152
|
-
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
|
|
6910
|
+
const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
|
|
6153
6911
|
p.on("error", () => resolve7(false));
|
|
6154
6912
|
p.on("close", (code) => resolve7(code === 0));
|
|
6155
6913
|
} catch {
|
|
@@ -6162,7 +6920,8 @@ function spawnRgFind(pattern, base) {
|
|
|
6162
6920
|
const child = spawn("rg", args, {
|
|
6163
6921
|
signal: AbortSignal.timeout(3e4),
|
|
6164
6922
|
env: buildChildEnv(),
|
|
6165
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
6923
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6924
|
+
windowsHide: true
|
|
6166
6925
|
});
|
|
6167
6926
|
let buf = "";
|
|
6168
6927
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -6183,7 +6942,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
6183
6942
|
const walk = async (dir) => {
|
|
6184
6943
|
let entries;
|
|
6185
6944
|
try {
|
|
6186
|
-
entries = await
|
|
6945
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
6187
6946
|
} catch {
|
|
6188
6947
|
return;
|
|
6189
6948
|
}
|
|
@@ -6191,7 +6950,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
6191
6950
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
6192
6951
|
const full = path2.join(dir, e.name);
|
|
6193
6952
|
try {
|
|
6194
|
-
const stat10 = await
|
|
6953
|
+
const stat10 = await fs14.lstat(full);
|
|
6195
6954
|
if (stat10.isSymbolicLink()) continue;
|
|
6196
6955
|
} catch {
|
|
6197
6956
|
continue;
|
|
@@ -6368,7 +7127,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
6368
7127
|
}
|
|
6369
7128
|
const fullPath = target;
|
|
6370
7129
|
if (!dryRun) {
|
|
6371
|
-
await
|
|
7130
|
+
await fs14.mkdir(path2.dirname(fullPath), { recursive: true });
|
|
6372
7131
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
6373
7132
|
}
|
|
6374
7133
|
files.push(resolvedPath);
|
|
@@ -6469,13 +7228,24 @@ var searchTool = {
|
|
|
6469
7228
|
async function duckduckgoSearch(query2, num, signal) {
|
|
6470
7229
|
const encoded = encodeURIComponent(query2);
|
|
6471
7230
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
results,
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
7231
|
+
try {
|
|
7232
|
+
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
7233
|
+
const html = await response.text();
|
|
7234
|
+
const results = parseDuckDuckGo(html, num);
|
|
7235
|
+
return {
|
|
7236
|
+
query: query2,
|
|
7237
|
+
results,
|
|
7238
|
+
source: "duckduckgo",
|
|
7239
|
+
truncated: results.length >= num
|
|
7240
|
+
};
|
|
7241
|
+
} catch {
|
|
7242
|
+
return {
|
|
7243
|
+
query: query2,
|
|
7244
|
+
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
7245
|
+
source: "duckduckgo",
|
|
7246
|
+
truncated: false
|
|
7247
|
+
};
|
|
7248
|
+
}
|
|
6479
7249
|
}
|
|
6480
7250
|
function takeFrom(iter, max) {
|
|
6481
7251
|
const out = [];
|
|
@@ -6594,34 +7364,92 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
|
6594
7364
|
}
|
|
6595
7365
|
}
|
|
6596
7366
|
function anySignal(...signals) {
|
|
6597
|
-
|
|
6598
|
-
for (const s of signals) {
|
|
6599
|
-
if (s.aborted) {
|
|
6600
|
-
controller.abort();
|
|
6601
|
-
break;
|
|
6602
|
-
}
|
|
6603
|
-
s.addEventListener("abort", () => controller.abort());
|
|
6604
|
-
}
|
|
6605
|
-
return controller.signal;
|
|
7367
|
+
return AbortSignal.any(signals);
|
|
6606
7368
|
}
|
|
6607
7369
|
function stripTags2(html) {
|
|
6608
7370
|
return html.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
6609
7371
|
}
|
|
7372
|
+
var setWorkingDirTool = {
|
|
7373
|
+
name: "set_working_dir",
|
|
7374
|
+
category: "Context",
|
|
7375
|
+
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.",
|
|
7376
|
+
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.",
|
|
7377
|
+
permission: "confirm",
|
|
7378
|
+
mutating: true,
|
|
7379
|
+
capabilities: ["fs.read"],
|
|
7380
|
+
timeoutMs: 5e3,
|
|
7381
|
+
inputSchema: {
|
|
7382
|
+
type: "object",
|
|
7383
|
+
properties: {
|
|
7384
|
+
path: {
|
|
7385
|
+
type: "string",
|
|
7386
|
+
description: "Directory to navigate to. Can be relative (to projectRoot) or absolute. If omitted, returns the current working directory without changing it."
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
},
|
|
7390
|
+
async execute(input, ctx, _opts) {
|
|
7391
|
+
if (!input.path) {
|
|
7392
|
+
return {
|
|
7393
|
+
current: ctx.workingDir,
|
|
7394
|
+
message: `Current working directory is ${ctx.workingDir}`
|
|
7395
|
+
};
|
|
7396
|
+
}
|
|
7397
|
+
const previous = ctx.workingDir;
|
|
7398
|
+
let resolved;
|
|
7399
|
+
try {
|
|
7400
|
+
resolved = ctx.setWorkingDir(input.path);
|
|
7401
|
+
} catch (err) {
|
|
7402
|
+
return {
|
|
7403
|
+
current: ctx.workingDir,
|
|
7404
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7405
|
+
};
|
|
7406
|
+
}
|
|
7407
|
+
try {
|
|
7408
|
+
await fs14.access(resolved);
|
|
7409
|
+
} catch {
|
|
7410
|
+
try {
|
|
7411
|
+
ctx.setWorkingDir(previous);
|
|
7412
|
+
} catch {
|
|
7413
|
+
}
|
|
7414
|
+
return {
|
|
7415
|
+
current: ctx.workingDir,
|
|
7416
|
+
error: `Directory does not exist: ${resolved}`
|
|
7417
|
+
};
|
|
7418
|
+
}
|
|
7419
|
+
return {
|
|
7420
|
+
current: resolved,
|
|
7421
|
+
previous,
|
|
7422
|
+
message: `Working directory changed to ${resolved}`
|
|
7423
|
+
};
|
|
7424
|
+
}
|
|
7425
|
+
};
|
|
7426
|
+
function findTaskIndex(tasks, query2) {
|
|
7427
|
+
const asNum = Number.parseInt(query2, 10);
|
|
7428
|
+
if (!Number.isNaN(asNum)) {
|
|
7429
|
+
const idx = asNum - 1;
|
|
7430
|
+
if (tasks[idx]) return idx;
|
|
7431
|
+
}
|
|
7432
|
+
const byId = tasks.findIndex((t) => t.id === query2);
|
|
7433
|
+
if (byId >= 0) return byId;
|
|
7434
|
+
const lower = query2.toLowerCase();
|
|
7435
|
+
return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
|
|
7436
|
+
}
|
|
6610
7437
|
var taskTool = {
|
|
6611
7438
|
name: "task",
|
|
6612
7439
|
category: "Session",
|
|
6613
7440
|
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.",
|
|
6614
|
-
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',
|
|
6615
|
-
permission: "
|
|
6616
|
-
mutating:
|
|
7441
|
+
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',
|
|
7442
|
+
permission: "confirm",
|
|
7443
|
+
mutating: true,
|
|
7444
|
+
capabilities: ["fs.write"],
|
|
6617
7445
|
timeoutMs: 2e3,
|
|
6618
7446
|
inputSchema: {
|
|
6619
7447
|
type: "object",
|
|
6620
7448
|
properties: {
|
|
6621
7449
|
action: {
|
|
6622
7450
|
type: "string",
|
|
6623
|
-
enum: ["replace", "add", "status", "show"],
|
|
6624
|
-
description: "replace = set full list, add = append, status = update task status, show = view only."
|
|
7451
|
+
enum: ["replace", "add", "status", "show", "promote", "planify"],
|
|
7452
|
+
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."
|
|
6625
7453
|
},
|
|
6626
7454
|
tasks: {
|
|
6627
7455
|
type: "array",
|
|
@@ -6665,11 +7493,20 @@ var taskTool = {
|
|
|
6665
7493
|
required: ["title", "type", "priority"],
|
|
6666
7494
|
description: "Single task to append (id/createdAt/updatedAt auto-generated)."
|
|
6667
7495
|
},
|
|
6668
|
-
id: { type: "string", description: "Task id for action=status." },
|
|
7496
|
+
id: { type: "string", description: "Task id for action=status or target for action=promote." },
|
|
6669
7497
|
status: {
|
|
6670
7498
|
type: "string",
|
|
6671
7499
|
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
|
|
6672
7500
|
description: "New status for action=status."
|
|
7501
|
+
},
|
|
7502
|
+
target: {
|
|
7503
|
+
type: "string",
|
|
7504
|
+
description: "Target task identifier (id, 1-based index, or title substring) for action=promote."
|
|
7505
|
+
},
|
|
7506
|
+
subtasks: {
|
|
7507
|
+
type: "array",
|
|
7508
|
+
items: { type: "string" },
|
|
7509
|
+
description: "Optional subtask titles for action=promote. Each becomes a pending todo."
|
|
6673
7510
|
}
|
|
6674
7511
|
},
|
|
6675
7512
|
required: ["action"]
|
|
@@ -6680,65 +7517,196 @@ var taskTool = {
|
|
|
6680
7517
|
return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
|
|
6681
7518
|
}
|
|
6682
7519
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
7520
|
+
let early = null;
|
|
7521
|
+
const promoteMeta = { count: 0, title: "" };
|
|
7522
|
+
const planifyMeta = { title: "", details: "" };
|
|
7523
|
+
let didPlanify = false;
|
|
7524
|
+
const file = await mutateTasks(taskPath, sessionId, async (f) => {
|
|
7525
|
+
switch (input.action) {
|
|
7526
|
+
case "show":
|
|
7527
|
+
break;
|
|
7528
|
+
case "replace": {
|
|
7529
|
+
if (!Array.isArray(input.tasks)) {
|
|
7530
|
+
early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
|
|
7531
|
+
return f;
|
|
7532
|
+
}
|
|
7533
|
+
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
7534
|
+
for (const t of input.tasks) {
|
|
7535
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7536
|
+
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
7537
|
+
if (missing.length > 0) {
|
|
7538
|
+
early = {
|
|
7539
|
+
ok: false,
|
|
7540
|
+
message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
|
|
7541
|
+
count: 0,
|
|
7542
|
+
completed: 0,
|
|
7543
|
+
inProgress: 0
|
|
7544
|
+
};
|
|
7545
|
+
return f;
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
}
|
|
7549
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7550
|
+
f.tasks = input.tasks.map((t) => ({
|
|
7551
|
+
...t,
|
|
7552
|
+
createdAt: t.createdAt || now,
|
|
7553
|
+
updatedAt: now
|
|
7554
|
+
}));
|
|
7555
|
+
break;
|
|
6690
7556
|
}
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
7557
|
+
case "add": {
|
|
7558
|
+
const t = input.task;
|
|
7559
|
+
if (!t || !t.title) {
|
|
7560
|
+
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
7561
|
+
return f;
|
|
7562
|
+
}
|
|
7563
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7564
|
+
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
7565
|
+
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
7566
|
+
if (missing.length > 0) {
|
|
7567
|
+
early = {
|
|
7568
|
+
ok: false,
|
|
7569
|
+
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
7570
|
+
count: 0,
|
|
7571
|
+
completed: 0,
|
|
7572
|
+
inProgress: 0
|
|
7573
|
+
};
|
|
7574
|
+
return f;
|
|
7575
|
+
}
|
|
7576
|
+
}
|
|
7577
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7578
|
+
const newTask = {
|
|
7579
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
7580
|
+
title: t.title,
|
|
7581
|
+
description: t.description,
|
|
7582
|
+
type: t.type || "feature",
|
|
7583
|
+
priority: t.priority || "medium",
|
|
7584
|
+
status: t.status || "pending",
|
|
7585
|
+
dependsOn: t.dependsOn,
|
|
7586
|
+
assignee: t.assignee,
|
|
7587
|
+
estimateHours: t.estimateHours,
|
|
7588
|
+
tags: t.tags,
|
|
7589
|
+
createdAt: now,
|
|
7590
|
+
updatedAt: now
|
|
7591
|
+
};
|
|
7592
|
+
f.tasks.push(newTask);
|
|
7593
|
+
break;
|
|
6704
7594
|
}
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
7595
|
+
case "status": {
|
|
7596
|
+
if (!input.id || !input.status) {
|
|
7597
|
+
early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
7598
|
+
return f;
|
|
7599
|
+
}
|
|
7600
|
+
const task = f.tasks.find((t) => t.id === input.id);
|
|
7601
|
+
if (!task) {
|
|
7602
|
+
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
7603
|
+
return f;
|
|
7604
|
+
}
|
|
7605
|
+
task.status = input.status;
|
|
7606
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7607
|
+
break;
|
|
7608
|
+
}
|
|
7609
|
+
case "promote": {
|
|
7610
|
+
const target = input.target?.trim();
|
|
7611
|
+
if (!target) {
|
|
7612
|
+
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
7613
|
+
return f;
|
|
7614
|
+
}
|
|
7615
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
7616
|
+
if (idx === -1) {
|
|
7617
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7618
|
+
return f;
|
|
7619
|
+
}
|
|
7620
|
+
const match = f.tasks[idx];
|
|
7621
|
+
if (!match) {
|
|
7622
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7623
|
+
return f;
|
|
7624
|
+
}
|
|
7625
|
+
if (match.status !== "completed" && match.status !== "failed") {
|
|
7626
|
+
match.status = "in_progress";
|
|
7627
|
+
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7628
|
+
}
|
|
7629
|
+
const todos = [];
|
|
7630
|
+
const ts2 = Date.now();
|
|
7631
|
+
todos.push({
|
|
7632
|
+
id: `todo_${ts2}_task`,
|
|
7633
|
+
content: match.title,
|
|
7634
|
+
status: "in_progress",
|
|
7635
|
+
activeForm: match.title,
|
|
7636
|
+
promotedFromTask: match.id
|
|
7637
|
+
});
|
|
7638
|
+
if (match.description) {
|
|
7639
|
+
todos.push({
|
|
7640
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
7641
|
+
content: match.description.slice(0, 200),
|
|
7642
|
+
status: "pending",
|
|
7643
|
+
promotedFromTask: match.id
|
|
7644
|
+
});
|
|
7645
|
+
}
|
|
7646
|
+
if (input.subtasks && input.subtasks.length > 0) {
|
|
7647
|
+
for (const st of input.subtasks) {
|
|
7648
|
+
todos.push({
|
|
7649
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
7650
|
+
content: st,
|
|
7651
|
+
status: "pending",
|
|
7652
|
+
promotedFromTask: match.id
|
|
7653
|
+
});
|
|
7654
|
+
}
|
|
7655
|
+
}
|
|
7656
|
+
ctx.state.replaceTodos(todos);
|
|
7657
|
+
promoteMeta.count = todos.length;
|
|
7658
|
+
promoteMeta.title = match.title;
|
|
7659
|
+
break;
|
|
6727
7660
|
}
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
7661
|
+
case "planify": {
|
|
7662
|
+
const target = input.target?.trim();
|
|
7663
|
+
if (!target) {
|
|
7664
|
+
early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
7665
|
+
return f;
|
|
7666
|
+
}
|
|
7667
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
7668
|
+
if (idx === -1) {
|
|
7669
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7670
|
+
return f;
|
|
7671
|
+
}
|
|
7672
|
+
const match = f.tasks[idx];
|
|
7673
|
+
if (!match) {
|
|
7674
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7675
|
+
return f;
|
|
7676
|
+
}
|
|
7677
|
+
planifyMeta.title = match.title;
|
|
7678
|
+
planifyMeta.details = match.description ?? "";
|
|
7679
|
+
didPlanify = true;
|
|
7680
|
+
break;
|
|
6731
7681
|
}
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
break;
|
|
7682
|
+
default:
|
|
7683
|
+
early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
|
|
7684
|
+
return f;
|
|
6736
7685
|
}
|
|
6737
|
-
|
|
6738
|
-
|
|
7686
|
+
return f;
|
|
7687
|
+
});
|
|
7688
|
+
if (early) return early;
|
|
7689
|
+
if (didPlanify) {
|
|
7690
|
+
const { title, details } = planifyMeta;
|
|
7691
|
+
const planPath = ctx.meta["plan.path"];
|
|
7692
|
+
if (typeof planPath === "string" && planPath) {
|
|
7693
|
+
const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
7694
|
+
const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
|
|
7695
|
+
await savePlan(planPath, updated);
|
|
7696
|
+
return {
|
|
7697
|
+
ok: true,
|
|
7698
|
+
message: `planify ok \u2014 added "${title}" to plan.
|
|
7699
|
+
${formatPlan(updated)}`,
|
|
7700
|
+
count: file.tasks.length,
|
|
7701
|
+
completed: computeTaskItemProgress(file.tasks).completed,
|
|
7702
|
+
inProgress: computeTaskItemProgress(file.tasks).inProgress
|
|
7703
|
+
};
|
|
7704
|
+
}
|
|
7705
|
+
return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
|
|
6739
7706
|
}
|
|
6740
7707
|
const p = computeTaskItemProgress(file.tasks);
|
|
6741
|
-
const summary =
|
|
7708
|
+
const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
|
|
7709
|
+
${formatTaskList(file.tasks)}` : file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
|
|
6742
7710
|
return {
|
|
6743
7711
|
ok: true,
|
|
6744
7712
|
message: summary,
|
|
@@ -6896,8 +7864,6 @@ function parseResult(runner, result, duration) {
|
|
|
6896
7864
|
truncated: result.truncated
|
|
6897
7865
|
};
|
|
6898
7866
|
}
|
|
6899
|
-
|
|
6900
|
-
// src/todo.ts
|
|
6901
7867
|
var todoTool = {
|
|
6902
7868
|
name: "todo",
|
|
6903
7869
|
category: "Session",
|
|
@@ -6956,6 +7922,48 @@ var todoTool = {
|
|
|
6956
7922
|
}
|
|
6957
7923
|
}
|
|
6958
7924
|
ctx.state.replaceTodos(items);
|
|
7925
|
+
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
7926
|
+
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
7927
|
+
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
7928
|
+
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
7929
|
+
for (const item of items) {
|
|
7930
|
+
if (item.promotedFromPlan) {
|
|
7931
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
|
|
7932
|
+
}
|
|
7933
|
+
if (item.promotedFromTask) {
|
|
7934
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
for (const planId of completedPlanIds) {
|
|
7938
|
+
if (pendingPlanIds.has(planId)) continue;
|
|
7939
|
+
const planPath = ctx.meta["plan.path"];
|
|
7940
|
+
if (typeof planPath !== "string" || !planPath) continue;
|
|
7941
|
+
try {
|
|
7942
|
+
const plan = await loadPlan(planPath);
|
|
7943
|
+
if (plan) {
|
|
7944
|
+
const updated = setPlanItemStatus(plan, planId, "done");
|
|
7945
|
+
await savePlan(planPath, updated);
|
|
7946
|
+
}
|
|
7947
|
+
} catch {
|
|
7948
|
+
}
|
|
7949
|
+
}
|
|
7950
|
+
for (const taskId of completedTaskIds) {
|
|
7951
|
+
if (pendingTaskIds.has(taskId)) continue;
|
|
7952
|
+
const taskPath = ctx.meta["task.path"];
|
|
7953
|
+
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
7954
|
+
try {
|
|
7955
|
+
const file = await loadTasks(taskPath);
|
|
7956
|
+
if (file) {
|
|
7957
|
+
const task = file.tasks.find((t) => t.id === taskId);
|
|
7958
|
+
if (task && task.status !== "completed") {
|
|
7959
|
+
task.status = "completed";
|
|
7960
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7961
|
+
await saveTasks(taskPath, file);
|
|
7962
|
+
}
|
|
7963
|
+
}
|
|
7964
|
+
} catch {
|
|
7965
|
+
}
|
|
7966
|
+
}
|
|
6959
7967
|
return {
|
|
6960
7968
|
count: items.length,
|
|
6961
7969
|
in_progress: items.filter((t) => t.status === "in_progress").length
|
|
@@ -7370,7 +8378,7 @@ var treeTool = {
|
|
|
7370
8378
|
}
|
|
7371
8379
|
};
|
|
7372
8380
|
async function walkDir(dir, depth, opts) {
|
|
7373
|
-
const entries = await
|
|
8381
|
+
const entries = await fs14.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
7374
8382
|
const filtered = entries.filter((e) => {
|
|
7375
8383
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
7376
8384
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -7520,14 +8528,14 @@ var writeTool = {
|
|
|
7520
8528
|
let existed = false;
|
|
7521
8529
|
let prev = "";
|
|
7522
8530
|
try {
|
|
7523
|
-
const stat11 = await
|
|
8531
|
+
const stat11 = await fs14.stat(absPath);
|
|
7524
8532
|
existed = stat11.isFile();
|
|
7525
8533
|
if (existed) {
|
|
7526
8534
|
if (!ctx.hasRead(absPath)) {
|
|
7527
|
-
prev = await
|
|
8535
|
+
prev = await fs14.readFile(absPath, "utf8");
|
|
7528
8536
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7529
8537
|
} else {
|
|
7530
|
-
prev = await
|
|
8538
|
+
prev = await fs14.readFile(absPath, "utf8");
|
|
7531
8539
|
}
|
|
7532
8540
|
}
|
|
7533
8541
|
} catch (err) {
|
|
@@ -7538,7 +8546,7 @@ var writeTool = {
|
|
|
7538
8546
|
await atomicWrite(absPath, input.content);
|
|
7539
8547
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
7540
8548
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
7541
|
-
const stat10 = await
|
|
8549
|
+
const stat10 = await fs14.stat(absPath);
|
|
7542
8550
|
ctx.recordRead(absPath, stat10.mtimeMs);
|
|
7543
8551
|
ctx.session.recordFileChange({
|
|
7544
8552
|
path: absPath,
|
|
@@ -7591,7 +8599,8 @@ var builtinTools = [
|
|
|
7591
8599
|
toolHelpTool,
|
|
7592
8600
|
codebaseIndexTool,
|
|
7593
8601
|
codebaseSearchTool,
|
|
7594
|
-
codebaseStatsTool
|
|
8602
|
+
codebaseStatsTool,
|
|
8603
|
+
setWorkingDirTool
|
|
7595
8604
|
];
|
|
7596
8605
|
|
|
7597
8606
|
// src/pack.ts
|