pi-ast-sgrep 2.0.2 → 2.2.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/README.md +16 -19
- package/dist/code-mode.d.ts +1 -1
- package/dist/code-mode.js +1 -1
- package/dist/codemode/connector.d.ts +18 -3
- package/dist/codemode/connector.js +85 -31
- package/dist/codemode/dispatch.d.ts +13 -1
- package/dist/codemode/dispatch.js +87 -24
- package/dist/codemode/guest-api.d.ts +16 -0
- package/dist/codemode/guest-api.js +194 -0
- package/dist/codemode/guest-worker.mjs +287 -0
- package/dist/codemode/index.d.ts +4 -3
- package/dist/codemode/index.js +4 -3
- package/dist/codemode/native.d.ts +1 -1
- package/dist/codemode/native.js +1 -1
- package/dist/codemode/runner.d.ts +13 -9
- package/dist/codemode/runner.js +411 -213
- package/dist/codemode/session-pool.d.ts +6 -1
- package/dist/codemode/session-pool.js +125 -32
- package/dist/codemode/types.d.ts +42 -2
- package/dist/codemode/types.js +40 -15
- package/dist/codemode/worker.d.ts +1 -1
- package/dist/codemode/worker.js +25 -2
- package/dist/host/commands.d.ts +6 -0
- package/dist/host/commands.js +49 -0
- package/dist/host/results.d.ts +123 -0
- package/dist/host/results.js +126 -0
- package/dist/host/tools.d.ts +28 -0
- package/dist/host/tools.js +802 -0
- package/dist/index.d.ts +7 -34
- package/dist/index.js +5 -543
- package/dist/runtime/config.d.ts +36 -0
- package/dist/runtime/config.js +98 -0
- package/dist/runtime/freshness.d.ts +43 -0
- package/dist/runtime/freshness.js +446 -0
- package/dist/runtime/index-health.d.ts +16 -0
- package/dist/runtime/index-health.js +111 -0
- package/dist/runtime/runtime.d.ts +48 -0
- package/dist/runtime/runtime.js +265 -0
- package/dist/runtime/sqlite.d.ts +15 -0
- package/dist/runtime/sqlite.js +63 -0
- package/dist/runtime/types.d.ts +55 -0
- package/dist/runtime/types.js +25 -0
- package/dist/ui/card.d.ts +66 -0
- package/dist/ui/card.js +375 -0
- package/dist/ui/present.d.ts +89 -0
- package/dist/ui/present.js +391 -0
- package/package.json +8 -7
- package/dist/codemode/sandbox-worker.d.ts +0 -1
- package/dist/codemode/sandbox-worker.js +0 -204
- package/dist/present.d.ts +0 -70
- package/dist/present.js +0 -260
- package/dist/runtime.d.ts +0 -137
- package/dist/runtime.js +0 -799
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Fallback: sticky `codemode-serve` child only when the `.node` addon is missing
|
|
8
8
|
* (unsupported host / incomplete install). Doctor reports that as degraded.
|
|
9
9
|
*/
|
|
10
|
-
import type { MachineEnvelope } from "../runtime.js";
|
|
10
|
+
import type { MachineEnvelope } from "../runtime/runtime.js";
|
|
11
11
|
import { type StickyWorker } from "./dispatch.js";
|
|
12
12
|
import { type StickyWorkerOptions } from "./worker.js";
|
|
13
13
|
export type SessionPoolOptions = {
|
|
@@ -22,6 +22,9 @@ export type SessionPoolOptions = {
|
|
|
22
22
|
limit?: number;
|
|
23
23
|
};
|
|
24
24
|
export type StickyStarter = (options: StickyWorkerOptions) => Promise<StickyWorker>;
|
|
25
|
+
/** Unique search must not run on the JS thread; cache hits may. */
|
|
26
|
+
export declare function isUncachedSearchError(cause: unknown): boolean;
|
|
27
|
+
export declare function isClosedWorkerError(cause: unknown): boolean;
|
|
25
28
|
export declare class NativeSessionPool {
|
|
26
29
|
#private;
|
|
27
30
|
constructor(startFn?: StickyStarter);
|
|
@@ -29,6 +32,8 @@ export declare class NativeSessionPool {
|
|
|
29
32
|
configured(): boolean;
|
|
30
33
|
/** Active backend after first successful acquire. */
|
|
31
34
|
backend(): "napi" | "cli" | "none";
|
|
35
|
+
/** Why the last start attempt failed — for doctor/status and error fidelity. */
|
|
36
|
+
lastStartError(root: string): string | undefined;
|
|
32
37
|
acquire(root: string): Promise<StickyWorker | null>;
|
|
33
38
|
call(root: string, tool: string, args?: Record<string, unknown>, options?: {
|
|
34
39
|
signal?: AbortSignal;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { asEnvelope } from "./dispatch.js";
|
|
11
11
|
import { loadCodemodeNative } from "./native.js";
|
|
12
|
+
import { defined } from "./types.js";
|
|
12
13
|
import { startStickyWorker } from "./worker.js";
|
|
13
14
|
const abortError = () => Object.assign(new Error("native call aborted"), { name: "AbortError" });
|
|
14
15
|
/** Bounded metadata and symbol lookups that may run on the JS thread. */
|
|
@@ -19,11 +20,43 @@ const FAST_LOOKUP = new Set([
|
|
|
19
20
|
"index_status",
|
|
20
21
|
"catalog_search",
|
|
21
22
|
"catalog_describe",
|
|
23
|
+
"find",
|
|
24
|
+
"read",
|
|
22
25
|
]);
|
|
23
26
|
function isBusyError(cause) {
|
|
24
27
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
25
28
|
return /session is busy/i.test(message);
|
|
26
29
|
}
|
|
30
|
+
/** Unique search must not run on the JS thread; cache hits may. */
|
|
31
|
+
export function isUncachedSearchError(cause) {
|
|
32
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
33
|
+
return /use call\(\) for search/i.test(message);
|
|
34
|
+
}
|
|
35
|
+
/** NAPI can surface SQLite u64 counters as BigInt (writer_generation exceeds
|
|
36
|
+
* 2^53). BigInt breaks JSON.stringify downstream (pi serializes result
|
|
37
|
+
* details) — normalize to Number at the boundary. Precision past 2^53 is
|
|
38
|
+
* display-only here; equality comparisons still hold since both sides
|
|
39
|
+
* convert the same integer identically. */
|
|
40
|
+
function normalizeNativeValue(value) {
|
|
41
|
+
if (typeof value === "bigint")
|
|
42
|
+
return Number(value);
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return value.map(normalizeNativeValue);
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const [key, entry] of Object.entries(value))
|
|
48
|
+
out[key] = normalizeNativeValue(entry);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
export function isClosedWorkerError(cause) {
|
|
54
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
55
|
+
return /codemode-serve is closed|native session is closed/i.test(message);
|
|
56
|
+
}
|
|
57
|
+
function workerIsClosed(worker) {
|
|
58
|
+
return worker.closed?.() === true;
|
|
59
|
+
}
|
|
27
60
|
function inProcessWorker(session) {
|
|
28
61
|
let tail = Promise.resolve();
|
|
29
62
|
let closed = false;
|
|
@@ -53,26 +86,34 @@ function inProcessWorker(session) {
|
|
|
53
86
|
});
|
|
54
87
|
};
|
|
55
88
|
return {
|
|
89
|
+
closed: () => closed,
|
|
56
90
|
call(tool, args, options) {
|
|
57
91
|
if (options?.signal?.aborted)
|
|
58
92
|
return Promise.reject(abortError());
|
|
59
93
|
const sync = session.callNow;
|
|
60
|
-
if (inflight === 0 && !closed && sync && FAST_LOOKUP.has(tool)) {
|
|
94
|
+
if (inflight === 0 && !closed && sync && (FAST_LOOKUP.has(tool) || tool === "search")) {
|
|
61
95
|
try {
|
|
62
|
-
|
|
96
|
+
const value = sync.call(session, tool, args ?? {});
|
|
97
|
+
if (!(tool === "search" && value == null)) {
|
|
98
|
+
return Promise.resolve(asEnvelope(normalizeNativeValue(value), tool));
|
|
99
|
+
}
|
|
63
100
|
}
|
|
64
101
|
catch (cause) {
|
|
65
|
-
if (
|
|
102
|
+
if (tool === "search" && isUncachedSearchError(cause)) {
|
|
103
|
+
// Older native addons threw on unique search.
|
|
104
|
+
}
|
|
105
|
+
else if (!isBusyError(cause)) {
|
|
66
106
|
return Promise.reject(cause);
|
|
107
|
+
}
|
|
67
108
|
}
|
|
68
109
|
}
|
|
69
|
-
return enqueue(async () => asEnvelope(await session.call(tool, args, options?.signal), tool), options?.signal);
|
|
110
|
+
return enqueue(async () => asEnvelope(normalizeNativeValue(await session.call(tool, args, options?.signal)), tool), options?.signal);
|
|
70
111
|
},
|
|
71
112
|
batch(calls, options) {
|
|
72
113
|
return enqueue(async () => {
|
|
73
114
|
const response = await session.batch(calls, options?.signal);
|
|
74
115
|
const result = {
|
|
75
|
-
results: response.results,
|
|
116
|
+
results: normalizeNativeValue(response.results),
|
|
76
117
|
all_ok: response.allOk,
|
|
77
118
|
wall_ms: response.wallMs,
|
|
78
119
|
mode: response.mode,
|
|
@@ -95,6 +136,10 @@ export class NativeSessionPool {
|
|
|
95
136
|
#startFn;
|
|
96
137
|
#backend = "none";
|
|
97
138
|
#shutdownPromise = null;
|
|
139
|
+
/** Real start failure per root — surfaced instead of a stale "closed" error. */
|
|
140
|
+
#startFailures = new Map();
|
|
141
|
+
/** Crash-loop guard: a failing backend gets this long before respawn retries. */
|
|
142
|
+
static #RESTART_BACKOFF_MS = 15_000;
|
|
98
143
|
constructor(startFn = startStickyWorker) {
|
|
99
144
|
this.#startFn = startFn;
|
|
100
145
|
}
|
|
@@ -108,12 +153,26 @@ export class NativeSessionPool {
|
|
|
108
153
|
backend() {
|
|
109
154
|
return this.#backend;
|
|
110
155
|
}
|
|
156
|
+
/** Why the last start attempt failed — for doctor/status and error fidelity. */
|
|
157
|
+
lastStartError(root) {
|
|
158
|
+
return this.#startFailures.get(root)?.error;
|
|
159
|
+
}
|
|
111
160
|
async acquire(root) {
|
|
112
161
|
if (this.#shutdownPromise)
|
|
113
162
|
return null;
|
|
114
163
|
const existing = this.#entries.get(root);
|
|
115
|
-
if (existing)
|
|
116
|
-
|
|
164
|
+
if (existing) {
|
|
165
|
+
if (!workerIsClosed(existing.worker))
|
|
166
|
+
return existing.worker;
|
|
167
|
+
await this.invalidate(root);
|
|
168
|
+
}
|
|
169
|
+
// A backend that just failed is crash-looping: skip the respawn for a
|
|
170
|
+
// bounded window so callers fall back instead of paying a doomed spawn
|
|
171
|
+
// per call. The window expires and recovery still happens automatically.
|
|
172
|
+
const failure = this.#startFailures.get(root);
|
|
173
|
+
if (failure && Date.now() - failure.at < NativeSessionPool.#RESTART_BACKOFF_MS) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
117
176
|
const inFlight = this.#starting.get(root);
|
|
118
177
|
if (inFlight)
|
|
119
178
|
return inFlight;
|
|
@@ -130,10 +189,31 @@ export class NativeSessionPool {
|
|
|
130
189
|
async call(root, tool, args = {}, options) {
|
|
131
190
|
if (options?.signal?.aborted)
|
|
132
191
|
throw abortError();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
192
|
+
try {
|
|
193
|
+
const worker = await this.acquire(root);
|
|
194
|
+
if (!worker) {
|
|
195
|
+
const startError = this.lastStartError(root);
|
|
196
|
+
throw new Error(startError
|
|
197
|
+
? "codemode backend unavailable: " + startError
|
|
198
|
+
: "native Code Mode backend unavailable");
|
|
199
|
+
}
|
|
200
|
+
return await worker.call(tool, args, options);
|
|
201
|
+
}
|
|
202
|
+
catch (cause) {
|
|
203
|
+
if (options?.signal?.aborted || !isClosedWorkerError(cause))
|
|
204
|
+
throw cause;
|
|
205
|
+
await this.invalidate(root);
|
|
206
|
+
const retry = await this.acquire(root);
|
|
207
|
+
if (!retry) {
|
|
208
|
+
// Surface the real restart failure (spawn error / schema refusal), not
|
|
209
|
+
// the stale "closed" message that hides it.
|
|
210
|
+
const startError = this.lastStartError(root);
|
|
211
|
+
if (startError)
|
|
212
|
+
throw new Error("codemode backend unavailable: " + startError);
|
|
213
|
+
throw cause;
|
|
214
|
+
}
|
|
215
|
+
return retry.call(tool, args, options);
|
|
216
|
+
}
|
|
137
217
|
}
|
|
138
218
|
async invalidate(root) {
|
|
139
219
|
this.#generations.set(root, this.#generationFor(root) + 1);
|
|
@@ -141,6 +221,7 @@ export class NativeSessionPool {
|
|
|
141
221
|
this.#starting.delete(root);
|
|
142
222
|
const entry = this.#entries.get(root);
|
|
143
223
|
this.#entries.delete(root);
|
|
224
|
+
this.#startFailures.delete(root);
|
|
144
225
|
if (entry)
|
|
145
226
|
await entry.worker.end().catch(() => undefined);
|
|
146
227
|
if (starting)
|
|
@@ -181,17 +262,24 @@ export class NativeSessionPool {
|
|
|
181
262
|
async #start(root) {
|
|
182
263
|
const gen = this.#generationFor(root);
|
|
183
264
|
const opts = this.#options ?? {};
|
|
265
|
+
const fail = (error) => {
|
|
266
|
+
this.#startFailures.set(root, {
|
|
267
|
+
at: Date.now(),
|
|
268
|
+
error: error instanceof Error ? error.message : String(error),
|
|
269
|
+
});
|
|
270
|
+
return null;
|
|
271
|
+
};
|
|
184
272
|
// 1) In-process NAPI (preferred — zero spawn).
|
|
185
273
|
const binding = loadCodemodeNative();
|
|
274
|
+
let napiError = null;
|
|
186
275
|
if (binding) {
|
|
187
276
|
try {
|
|
188
|
-
const config = {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
config.useEmbed = opts.useEmbed;
|
|
277
|
+
const config = defined({
|
|
278
|
+
root,
|
|
279
|
+
indexPath: opts.indexPath,
|
|
280
|
+
limit: opts.limit,
|
|
281
|
+
useEmbed: opts.useEmbed,
|
|
282
|
+
});
|
|
195
283
|
const session = new binding.Session(config);
|
|
196
284
|
const worker = inProcessWorker(session);
|
|
197
285
|
if (gen !== this.#generationFor(root)) {
|
|
@@ -200,37 +288,42 @@ export class NativeSessionPool {
|
|
|
200
288
|
}
|
|
201
289
|
this.#entries.set(root, { root, worker, generation: gen, backend: "napi" });
|
|
202
290
|
this.#backend = "napi";
|
|
291
|
+
this.#startFailures.delete(root);
|
|
203
292
|
return worker;
|
|
204
293
|
}
|
|
205
|
-
catch {
|
|
206
|
-
// Fall through to CLI sticky.
|
|
294
|
+
catch (cause) {
|
|
295
|
+
// Fall through to CLI sticky, but keep the real error for fidelity.
|
|
296
|
+
napiError = cause;
|
|
207
297
|
}
|
|
208
298
|
}
|
|
209
299
|
// 2) CLI sticky fallback (degraded).
|
|
210
|
-
if (!opts.binary)
|
|
211
|
-
return
|
|
300
|
+
if (!opts.binary) {
|
|
301
|
+
return fail(napiError ?? new Error("native Code Mode backend unavailable (no addon, no binary)"));
|
|
302
|
+
}
|
|
212
303
|
try {
|
|
213
|
-
const stickyOpts = {
|
|
304
|
+
const stickyOpts = defined({
|
|
214
305
|
binary: opts.binary,
|
|
215
306
|
cwd: root,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
stickyOpts.timeoutMs = opts.timeoutMs;
|
|
221
|
-
if (opts.maxOutputBytes !== undefined)
|
|
222
|
-
stickyOpts.maxOutputBytes = opts.maxOutputBytes;
|
|
307
|
+
env: opts.env,
|
|
308
|
+
timeoutMs: opts.timeoutMs,
|
|
309
|
+
maxOutputBytes: opts.maxOutputBytes,
|
|
310
|
+
});
|
|
223
311
|
const worker = await this.#startFn(stickyOpts);
|
|
224
312
|
if (gen !== this.#generationFor(root)) {
|
|
225
313
|
await worker.end().catch(() => undefined);
|
|
226
314
|
return null;
|
|
227
315
|
}
|
|
316
|
+
if (workerIsClosed(worker)) {
|
|
317
|
+
await worker.end().catch(() => undefined);
|
|
318
|
+
return fail(new Error("codemode-serve exited during startup"));
|
|
319
|
+
}
|
|
228
320
|
this.#entries.set(root, { root, worker, generation: gen, backend: "cli" });
|
|
229
321
|
this.#backend = "cli";
|
|
322
|
+
this.#startFailures.delete(root);
|
|
230
323
|
return worker;
|
|
231
324
|
}
|
|
232
|
-
catch {
|
|
233
|
-
return
|
|
325
|
+
catch (cause) {
|
|
326
|
+
return fail(cause);
|
|
234
327
|
}
|
|
235
328
|
}
|
|
236
329
|
}
|
package/dist/codemode/types.d.ts
CHANGED
|
@@ -1,18 +1,58 @@
|
|
|
1
1
|
/** Typed surface the model sees inside a Code Mode program (`asgrep.*`). */
|
|
2
|
+
/** Drop undefined keys — replaces spread-conditional arg-building chains. */
|
|
3
|
+
export declare function defined<T extends Record<string, unknown>>(args: T): Record<string, unknown>;
|
|
2
4
|
export type SearchArgs = {
|
|
3
5
|
query: string;
|
|
4
6
|
limit?: number;
|
|
5
7
|
excerptLines?: number;
|
|
6
8
|
format?: "capsule" | "agent";
|
|
9
|
+
/** Directory or glob; injected as an `in:` query token. */
|
|
10
|
+
in?: string;
|
|
11
|
+
fileFilter?: string;
|
|
12
|
+
file_filter?: string;
|
|
13
|
+
lang?: string;
|
|
14
|
+
};
|
|
15
|
+
export type FindArgs = SearchArgs;
|
|
16
|
+
export type ReadArgs = {
|
|
17
|
+
path?: string;
|
|
18
|
+
start?: number;
|
|
19
|
+
end?: number;
|
|
20
|
+
ref?: string;
|
|
21
|
+
refs?: unknown[];
|
|
22
|
+
contextLines?: number;
|
|
23
|
+
maxChars?: number;
|
|
24
|
+
};
|
|
25
|
+
export type EditArgs = {
|
|
26
|
+
path?: string;
|
|
27
|
+
oldText?: string;
|
|
28
|
+
newText?: string;
|
|
29
|
+
edits?: Array<{
|
|
30
|
+
path?: string;
|
|
31
|
+
oldText: string;
|
|
32
|
+
newText: string;
|
|
33
|
+
}>;
|
|
7
34
|
};
|
|
8
35
|
export type ChainArgs = {
|
|
9
36
|
query: string;
|
|
10
37
|
limit?: number;
|
|
11
38
|
excerptLines?: number;
|
|
12
39
|
};
|
|
40
|
+
/** Host methods the program may invoke. Primary lookup methods first. */
|
|
41
|
+
export declare const CODEMODE_HOST_METHODS: readonly ["search", "find", "read", "edit", "semantic", "chain", "defs", "callers", "imports", "indexStatus", "indexRepo", "doctor", "catalogSearch", "catalogDescribe"];
|
|
42
|
+
export type CodemodeHostMethod = (typeof CODEMODE_HOST_METHODS)[number];
|
|
13
43
|
/**
|
|
14
44
|
* Compact TypeScript declarations for the `asgrep` tool description.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
45
|
+
* Four commands only — every token here is paid on every turn.
|
|
46
|
+
* Return shapes are muscle memory (Blacksmith): field names, never values.
|
|
47
|
+
* defs:/callers:/imports:/pattern:/blast: go through find or search prefixes.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Always-on API cheat sheet for the Code Mode tool description.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately minimal: every token here rides in the system prompt of every
|
|
53
|
+
* request. The full per-method schema is one call away through
|
|
54
|
+
* `asgrep.catalogSearch(query)` / `asgrep.catalogDescribe(name)`, which returns
|
|
55
|
+
* the same shapes from the native catalog, so the model pays for the reference
|
|
56
|
+
* only when it needs it.
|
|
17
57
|
*/
|
|
18
58
|
export declare const CODEMODE_TYPES_FOR_MODEL: string;
|
package/dist/codemode/types.js
CHANGED
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
/** Typed surface the model sees inside a Code Mode program (`asgrep.*`). */
|
|
2
|
+
/** Drop undefined keys — replaces spread-conditional arg-building chains. */
|
|
3
|
+
export function defined(args) {
|
|
4
|
+
const out = {};
|
|
5
|
+
for (const [key, value] of Object.entries(args))
|
|
6
|
+
if (value !== undefined)
|
|
7
|
+
out[key] = value;
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
/** Host methods the program may invoke. Primary lookup methods first. */
|
|
11
|
+
export const CODEMODE_HOST_METHODS = [
|
|
12
|
+
"search",
|
|
13
|
+
"find",
|
|
14
|
+
"read",
|
|
15
|
+
"edit",
|
|
16
|
+
"semantic",
|
|
17
|
+
"chain",
|
|
18
|
+
"defs",
|
|
19
|
+
"callers",
|
|
20
|
+
"imports",
|
|
21
|
+
"indexStatus",
|
|
22
|
+
"indexRepo",
|
|
23
|
+
"doctor",
|
|
24
|
+
"catalogSearch",
|
|
25
|
+
"catalogDescribe",
|
|
26
|
+
];
|
|
2
27
|
/**
|
|
3
28
|
* Compact TypeScript declarations for the `asgrep` tool description.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
29
|
+
* Four commands only — every token here is paid on every turn.
|
|
30
|
+
* Return shapes are muscle memory (Blacksmith): field names, never values.
|
|
31
|
+
* defs:/callers:/imports:/pattern:/blast: go through find or search prefixes.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Always-on API cheat sheet for the Code Mode tool description.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately minimal: every token here rides in the system prompt of every
|
|
37
|
+
* request. The full per-method schema is one call away through
|
|
38
|
+
* `asgrep.catalogSearch(query)` / `asgrep.catalogDescribe(name)`, which returns
|
|
39
|
+
* the same shapes from the native catalog, so the model pays for the reference
|
|
40
|
+
* only when it needs it.
|
|
6
41
|
*/
|
|
7
42
|
export const CODEMODE_TYPES_FOR_MODEL = `
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
chain(input: { query: string; limit?: number }): Promise<unknown>;
|
|
12
|
-
defs(input: { symbol: string; limit?: number }): Promise<unknown>;
|
|
13
|
-
callers(input: { symbol: string; limit?: number }): Promise<unknown>;
|
|
14
|
-
imports(input: { module: string; limit?: number }): Promise<unknown>;
|
|
15
|
-
indexStatus(): Promise<unknown>;
|
|
16
|
-
indexRepo(input?: { force?: boolean }): Promise<unknown>;
|
|
17
|
-
catalogSearch(input: { query: string }): Promise<unknown>;
|
|
18
|
-
catalogDescribe(input: { name: string }): Promise<unknown>;
|
|
19
|
-
};
|
|
20
|
-
/** JS: Promise, JSON, Array, Object, Map, Set, Math. No require/process/fetch/fs. */
|
|
43
|
+
asgrep.search(query|{query,in,lang,limit,excerptLines}) | find(q) | semantic(q) | defs(sym) | callers(sym)
|
|
44
|
+
| imports(mod) | chain(q) | read({path|ref|refs,start,end}) | edit({path,oldText,newText}|{edits}) | indexStatus()
|
|
45
|
+
hits[{file,ref,symbol,kind,preview}] | windows[{path,start,end,text}] | catalogDescribe("name") for schemas
|
|
21
46
|
`.trim();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* One process, one warm Searcher, for the entire Code Mode program — the biggest
|
|
5
5
|
* Amdahl win over per-wave `codemode-batch` spawns.
|
|
6
6
|
*/
|
|
7
|
-
import type { MachineEnvelope } from "../runtime.js";
|
|
7
|
+
import type { MachineEnvelope } from "../runtime/runtime.js";
|
|
8
8
|
import { type StickyWorker } from "./dispatch.js";
|
|
9
9
|
export type StickyWorkerOptions = {
|
|
10
10
|
binary: string;
|
package/dist/codemode/worker.js
CHANGED
|
@@ -16,9 +16,26 @@ export async function startStickyWorker(options) {
|
|
|
16
16
|
env: { ...process.env, ...options.env, NO_COLOR: "1" },
|
|
17
17
|
stdio: ["pipe", "pipe", "pipe"],
|
|
18
18
|
});
|
|
19
|
+
// Fail fast on spawn errors (ENOENT / EACCES): callers get the spawn error
|
|
20
|
+
// now instead of a dead transport that only fails on first use.
|
|
21
|
+
await new Promise((resolve, reject) => {
|
|
22
|
+
const onError = (err) => {
|
|
23
|
+
child.removeListener("spawn", onSpawn);
|
|
24
|
+
reject(err);
|
|
25
|
+
};
|
|
26
|
+
const onSpawn = () => {
|
|
27
|
+
child.removeListener("error", onError);
|
|
28
|
+
resolve();
|
|
29
|
+
};
|
|
30
|
+
child.once("error", onError);
|
|
31
|
+
child.once("spawn", onSpawn);
|
|
32
|
+
});
|
|
19
33
|
const pending = new Map();
|
|
20
34
|
let nextId = 0;
|
|
21
35
|
let closed = false;
|
|
36
|
+
// The real termination cause (exit code + stderr) — callers that hit a dead
|
|
37
|
+
// transport must see WHY it died, not a bare "is closed".
|
|
38
|
+
let deadCause = null;
|
|
22
39
|
let stderr = "";
|
|
23
40
|
let stdout = Buffer.alloc(0);
|
|
24
41
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -32,6 +49,7 @@ export async function startStickyWorker(options) {
|
|
|
32
49
|
if (closed)
|
|
33
50
|
return;
|
|
34
51
|
closed = true;
|
|
52
|
+
deadCause ??= err;
|
|
35
53
|
options.signal?.removeEventListener("abort", onAbort);
|
|
36
54
|
killChild(child);
|
|
37
55
|
failAll(err);
|
|
@@ -112,15 +130,19 @@ export async function startStickyWorker(options) {
|
|
|
112
130
|
child.on("close", (code, signal) => {
|
|
113
131
|
closed = true;
|
|
114
132
|
options.signal?.removeEventListener("abort", onAbort);
|
|
133
|
+
deadCause ??= new Error(`codemode-serve exited code=${code ?? "null"} signal=${signal ?? "null"} stderr=${stderr.slice(0, 512)}`);
|
|
115
134
|
if (pending.size > 0) {
|
|
116
|
-
failAll(
|
|
135
|
+
failAll(deadCause);
|
|
117
136
|
}
|
|
118
137
|
});
|
|
119
138
|
const onAbort = () => terminate(new Error("codemode-serve aborted"));
|
|
120
139
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
140
|
+
const closedError = () => deadCause
|
|
141
|
+
? new Error("codemode-serve is closed (" + deadCause.message + ")")
|
|
142
|
+
: new Error("codemode-serve is closed");
|
|
121
143
|
const write = (payload) => {
|
|
122
144
|
if (closed || !child.stdin.writable) {
|
|
123
|
-
return Promise.reject(
|
|
145
|
+
return Promise.reject(closedError());
|
|
124
146
|
}
|
|
125
147
|
const id = typeof payload.id === "string" ? payload.id : String(nextId++);
|
|
126
148
|
payload.id = id;
|
|
@@ -168,6 +190,7 @@ export async function startStickyWorker(options) {
|
|
|
168
190
|
// Probe: empty End would close — instead send a tiny catalog call to verify protocol,
|
|
169
191
|
// or just return and let first real call fail. Prefer lazy: no probe.
|
|
170
192
|
return {
|
|
193
|
+
closed: () => closed || !child.stdin.writable,
|
|
171
194
|
async call(tool, args, callOptions) {
|
|
172
195
|
const msg = await writeWithControls({ type: "call", tool, args }, "codemode call", callOptions?.signal);
|
|
173
196
|
if (msg.type === "error") {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash commands: /asgrep-doctor /asgrep-status /asgrep-index /asgrep-reindex.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { type RuntimeLike } from "./results.js";
|
|
6
|
+
export declare function registerAstSgrepCommands(pi: ExtensionAPI, runtime?: RuntimeLike): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AstSgrepRuntime } from "../runtime/runtime.js";
|
|
2
|
+
import { bounded, errorDetails } from "./results.js";
|
|
3
|
+
const COMMANDS = [
|
|
4
|
+
["asgrep-doctor", "Check the ast-sgrep runtime, native binary, index, and project configuration", "doctor"],
|
|
5
|
+
["asgrep-status", "Show ast-sgrep runtime, index, backend, and capability status", "status"],
|
|
6
|
+
["asgrep-index", "Build the ast-sgrep index for the current project", "index"],
|
|
7
|
+
["asgrep-reindex", "Rebuild the ast-sgrep index for the current project", "reindex"],
|
|
8
|
+
];
|
|
9
|
+
async function runCommand(runtime, command, ctx, args) {
|
|
10
|
+
if (args.trim() !== "") {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
command,
|
|
14
|
+
error: { code: "INVALID_ARGUMENTS", message: `/${command} does not accept arguments`, details: { args } },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const response = await runtime.run([command.slice("asgrep-".length), ".", "--json"], { cwd: ctx.cwd });
|
|
19
|
+
return { ok: true, command, response };
|
|
20
|
+
}
|
|
21
|
+
catch (cause) {
|
|
22
|
+
return { ok: false, command, error: errorDetails(cause) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function compactCommandResult(result) {
|
|
26
|
+
if (!result.ok)
|
|
27
|
+
return `${result.command} failed [${result.error.code}]: ${result.error.message}`;
|
|
28
|
+
const response = result.response;
|
|
29
|
+
const counts = response.counts && typeof response.counts === "object"
|
|
30
|
+
? Object.entries(response.counts).map(([key, value]) => `${key}=${String(value)}`).join(" ")
|
|
31
|
+
: "";
|
|
32
|
+
const state = typeof response.status === "string" ? response.status
|
|
33
|
+
: typeof response.index_status === "string" ? response.index_status
|
|
34
|
+
: response.ok ? "healthy" : "failed";
|
|
35
|
+
return bounded([`${result.command}: ${state}`, counts].filter(Boolean).join(" · "));
|
|
36
|
+
}
|
|
37
|
+
export function registerAstSgrepCommands(pi, runtime = new AstSgrepRuntime(pi)) {
|
|
38
|
+
for (const [name, description] of COMMANDS) {
|
|
39
|
+
pi.registerCommand(name, {
|
|
40
|
+
description,
|
|
41
|
+
async handler(args, context) {
|
|
42
|
+
const ctx = context;
|
|
43
|
+
const result = await runCommand(runtime, name, ctx, args);
|
|
44
|
+
const output = ctx.hasUI ? compactCommandResult(result) : JSON.stringify(result);
|
|
45
|
+
ctx.ui.notify(output, result.ok ? "info" : "error");
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { type MachineEnvelope, type RunOptions } from "../runtime/types.js";
|
|
2
|
+
import type { FreshnessCoordinator } from "../runtime/freshness.js";
|
|
3
|
+
export declare const MAX_CONTENT_CHARS = 8000;
|
|
4
|
+
export type RuntimeLike = {
|
|
5
|
+
run(args: readonly string[], context: {
|
|
6
|
+
cwd: string;
|
|
7
|
+
}, options?: RunOptions): Promise<MachineEnvelope>;
|
|
8
|
+
resolveRoot?(context: {
|
|
9
|
+
cwd: string;
|
|
10
|
+
}): Promise<string>;
|
|
11
|
+
resolveBinaryPath?(options?: {
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}): string;
|
|
14
|
+
nativeEnv?(options?: {
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
}): NodeJS.ProcessEnv;
|
|
17
|
+
config?: {
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
maxOutputBytes?: number;
|
|
20
|
+
refreshIntervalMs?: number;
|
|
21
|
+
};
|
|
22
|
+
inspectIndexCompatibility?(context: {
|
|
23
|
+
cwd: string;
|
|
24
|
+
}): Promise<"ready" | "missing" | "incompatible">;
|
|
25
|
+
rebuildIncompatibleIndex?(context: {
|
|
26
|
+
cwd: string;
|
|
27
|
+
}, options?: RunOptions): Promise<MachineEnvelope>;
|
|
28
|
+
resolveIndexPath?(root: string): string;
|
|
29
|
+
watchExternalChanges?: boolean;
|
|
30
|
+
};
|
|
31
|
+
export type FreshnessLike = Pick<FreshnessCoordinator, "ensureFresh" | "markAffectedPath"> & {
|
|
32
|
+
markRootDirty?(root: string): void;
|
|
33
|
+
shutdown?(): void;
|
|
34
|
+
};
|
|
35
|
+
export type ToolContext = {
|
|
36
|
+
cwd: string;
|
|
37
|
+
};
|
|
38
|
+
export type CommandContext = ToolContext & {
|
|
39
|
+
hasUI: boolean;
|
|
40
|
+
ui: {
|
|
41
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export type CommandResult = {
|
|
45
|
+
ok: true;
|
|
46
|
+
command: string;
|
|
47
|
+
response: MachineEnvelope;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
command: string;
|
|
51
|
+
error: {
|
|
52
|
+
code: string;
|
|
53
|
+
message: string;
|
|
54
|
+
details: Readonly<Record<string, unknown>>;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export type Update = (result: {
|
|
58
|
+
content: Array<{
|
|
59
|
+
type: "text";
|
|
60
|
+
text: string;
|
|
61
|
+
}>;
|
|
62
|
+
details: Record<string, unknown>;
|
|
63
|
+
}) => void;
|
|
64
|
+
export declare function bounded(text: string): string;
|
|
65
|
+
export declare function success(command: string, response: MachineEnvelope, extra?: {
|
|
66
|
+
query?: string;
|
|
67
|
+
mode?: string;
|
|
68
|
+
activationMs?: number;
|
|
69
|
+
backend?: string;
|
|
70
|
+
freshness?: "stale";
|
|
71
|
+
indexState?: "empty" | "ready";
|
|
72
|
+
excerptLines?: number;
|
|
73
|
+
notes?: string[];
|
|
74
|
+
}): {
|
|
75
|
+
content: {
|
|
76
|
+
type: "text";
|
|
77
|
+
text: string;
|
|
78
|
+
}[];
|
|
79
|
+
details: {
|
|
80
|
+
query?: string;
|
|
81
|
+
mode?: string;
|
|
82
|
+
activationMs?: number;
|
|
83
|
+
backend?: string;
|
|
84
|
+
freshness?: "stale";
|
|
85
|
+
indexState?: "empty" | "ready";
|
|
86
|
+
excerptLines?: number;
|
|
87
|
+
notes?: string[];
|
|
88
|
+
ok: boolean;
|
|
89
|
+
command: string;
|
|
90
|
+
response: {
|
|
91
|
+
command: string;
|
|
92
|
+
tool: "asgrep";
|
|
93
|
+
schema_version: string;
|
|
94
|
+
ok: boolean;
|
|
95
|
+
version?: string;
|
|
96
|
+
machine_schema_version?: string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
export declare function errorDetails(cause: unknown, signal?: AbortSignal): {
|
|
101
|
+
code: string;
|
|
102
|
+
message: string;
|
|
103
|
+
details: Readonly<Record<string, unknown>>;
|
|
104
|
+
};
|
|
105
|
+
export declare function isFreshnessTimeout(cause: unknown, userSignal?: AbortSignal): boolean;
|
|
106
|
+
/** Leading or mid-query `in:path` scope used to bound a fresh-directory index. */
|
|
107
|
+
export declare function extractInPath(query: string): string | undefined;
|
|
108
|
+
export declare function failure(command: string, cause: unknown, signal?: AbortSignal): {
|
|
109
|
+
content: {
|
|
110
|
+
type: "text";
|
|
111
|
+
text: string;
|
|
112
|
+
}[];
|
|
113
|
+
details: {
|
|
114
|
+
ok: boolean;
|
|
115
|
+
command: string;
|
|
116
|
+
error: {
|
|
117
|
+
code: string;
|
|
118
|
+
message: string;
|
|
119
|
+
details: Readonly<Record<string, unknown>>;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
export declare function report(onUpdate: Update | undefined, command: string, phase: "started" | "completed"): void;
|