pi-ast-sgrep 1.3.2 → 2.0.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 +212 -18
- package/dist/code-mode.d.ts +89 -0
- package/dist/code-mode.js +432 -0
- package/dist/codemode/connector.d.ts +92 -0
- package/dist/codemode/connector.js +79 -0
- package/dist/codemode/dispatch.d.ts +83 -0
- package/dist/codemode/dispatch.js +320 -0
- package/dist/codemode/index.d.ts +18 -0
- package/dist/codemode/index.js +18 -0
- package/dist/codemode/native.d.ts +55 -0
- package/dist/codemode/native.js +119 -0
- package/dist/codemode/runner.d.ts +37 -0
- package/dist/codemode/runner.js +242 -0
- package/dist/codemode/sandbox-worker.d.ts +1 -0
- package/dist/codemode/sandbox-worker.js +204 -0
- package/dist/codemode/session-pool.d.ts +40 -0
- package/dist/codemode/session-pool.js +238 -0
- package/dist/codemode/types.d.ts +18 -0
- package/dist/codemode/types.js +21 -0
- package/dist/codemode/worker.d.ts +29 -0
- package/dist/codemode/worker.js +307 -0
- package/dist/index.d.ts +31 -3
- package/dist/index.js +393 -21
- package/dist/present.d.ts +67 -0
- package/dist/present.js +166 -0
- package/dist/runtime.d.ts +29 -2
- package/dist/runtime.js +436 -148
- package/native/.gitignore +3 -0
- package/native/README.md +17 -0
- package/package.json +26 -11
- package/skills/ast-sgrep/SKILL.md +0 -36
- package/skills/ast-sgrep/references/query-guide.md +0 -21
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped native Code Mode sessions.
|
|
3
|
+
*
|
|
4
|
+
* Primary path: in-process NAPI (`CodeModeSession` inside Node) — same model as
|
|
5
|
+
* MCP linking core. Zero CLI spawn.
|
|
6
|
+
*
|
|
7
|
+
* Fallback: sticky `codemode-serve` child only when the `.node` addon is missing
|
|
8
|
+
* (unsupported host / incomplete install). Doctor reports that as degraded.
|
|
9
|
+
*/
|
|
10
|
+
import { asEnvelope } from "./dispatch.js";
|
|
11
|
+
import { loadCodemodeNative } from "./native.js";
|
|
12
|
+
import { startStickyWorker } from "./worker.js";
|
|
13
|
+
const abortError = () => Object.assign(new Error("native call aborted"), { name: "AbortError" });
|
|
14
|
+
/** Bounded metadata and symbol lookups that may run on the JS thread. */
|
|
15
|
+
const FAST_LOOKUP = new Set([
|
|
16
|
+
"defs",
|
|
17
|
+
"callers",
|
|
18
|
+
"imports",
|
|
19
|
+
"index_status",
|
|
20
|
+
"catalog_search",
|
|
21
|
+
"catalog_describe",
|
|
22
|
+
]);
|
|
23
|
+
function isBusyError(cause) {
|
|
24
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
25
|
+
return /session is busy/i.test(message);
|
|
26
|
+
}
|
|
27
|
+
function inProcessWorker(session) {
|
|
28
|
+
let tail = Promise.resolve();
|
|
29
|
+
let closed = false;
|
|
30
|
+
let inflight = 0;
|
|
31
|
+
const enqueue = (operation, signal) => {
|
|
32
|
+
if (closed)
|
|
33
|
+
return Promise.reject(new Error("native session is closed"));
|
|
34
|
+
if (signal?.aborted)
|
|
35
|
+
return Promise.reject(abortError());
|
|
36
|
+
inflight += 1;
|
|
37
|
+
const slot = tail.then(() => {
|
|
38
|
+
if (signal?.aborted)
|
|
39
|
+
throw abortError();
|
|
40
|
+
return operation();
|
|
41
|
+
});
|
|
42
|
+
tail = slot.then(() => {
|
|
43
|
+
inflight -= 1;
|
|
44
|
+
}, () => {
|
|
45
|
+
inflight -= 1;
|
|
46
|
+
});
|
|
47
|
+
if (!signal)
|
|
48
|
+
return slot;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const abort = () => reject(abortError());
|
|
51
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
52
|
+
slot.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
call(tool, args, options) {
|
|
57
|
+
if (options?.signal?.aborted)
|
|
58
|
+
return Promise.reject(abortError());
|
|
59
|
+
const sync = session.callNow;
|
|
60
|
+
if (inflight === 0 && !closed && sync && FAST_LOOKUP.has(tool)) {
|
|
61
|
+
try {
|
|
62
|
+
return Promise.resolve(asEnvelope(sync.call(session, tool, args ?? {}), tool));
|
|
63
|
+
}
|
|
64
|
+
catch (cause) {
|
|
65
|
+
if (!isBusyError(cause))
|
|
66
|
+
return Promise.reject(cause);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return enqueue(async () => asEnvelope(await session.call(tool, args, options?.signal), tool), options?.signal);
|
|
70
|
+
},
|
|
71
|
+
batch(calls, options) {
|
|
72
|
+
return enqueue(async () => {
|
|
73
|
+
const response = await session.batch(calls, options?.signal);
|
|
74
|
+
const result = {
|
|
75
|
+
results: response.results,
|
|
76
|
+
all_ok: response.allOk,
|
|
77
|
+
wall_ms: response.wallMs,
|
|
78
|
+
mode: response.mode,
|
|
79
|
+
};
|
|
80
|
+
return result;
|
|
81
|
+
}, options?.signal);
|
|
82
|
+
},
|
|
83
|
+
async end() {
|
|
84
|
+
closed = true;
|
|
85
|
+
await tail;
|
|
86
|
+
// The NAPI session is released when this worker closure is dropped.
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export class NativeSessionPool {
|
|
91
|
+
#entries = new Map();
|
|
92
|
+
#starting = new Map();
|
|
93
|
+
#options = null;
|
|
94
|
+
#generations = new Map();
|
|
95
|
+
#startFn;
|
|
96
|
+
#backend = "none";
|
|
97
|
+
#shutdownPromise = null;
|
|
98
|
+
constructor(startFn = startStickyWorker) {
|
|
99
|
+
this.#startFn = startFn;
|
|
100
|
+
}
|
|
101
|
+
configure(options) {
|
|
102
|
+
this.#options = options;
|
|
103
|
+
}
|
|
104
|
+
configured() {
|
|
105
|
+
return this.#options !== null || loadCodemodeNative() !== null;
|
|
106
|
+
}
|
|
107
|
+
/** Active backend after first successful acquire. */
|
|
108
|
+
backend() {
|
|
109
|
+
return this.#backend;
|
|
110
|
+
}
|
|
111
|
+
async acquire(root) {
|
|
112
|
+
if (this.#shutdownPromise)
|
|
113
|
+
return null;
|
|
114
|
+
const existing = this.#entries.get(root);
|
|
115
|
+
if (existing)
|
|
116
|
+
return existing.worker;
|
|
117
|
+
const inFlight = this.#starting.get(root);
|
|
118
|
+
if (inFlight)
|
|
119
|
+
return inFlight;
|
|
120
|
+
const start = this.#start(root);
|
|
121
|
+
this.#starting.set(root, start);
|
|
122
|
+
try {
|
|
123
|
+
return await start;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (this.#starting.get(root) === start)
|
|
127
|
+
this.#starting.delete(root);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async call(root, tool, args = {}, options) {
|
|
131
|
+
if (options?.signal?.aborted)
|
|
132
|
+
throw abortError();
|
|
133
|
+
const worker = await this.acquire(root);
|
|
134
|
+
if (!worker)
|
|
135
|
+
throw new Error("native Code Mode backend unavailable");
|
|
136
|
+
return worker.call(tool, args, options);
|
|
137
|
+
}
|
|
138
|
+
async invalidate(root) {
|
|
139
|
+
this.#generations.set(root, this.#generationFor(root) + 1);
|
|
140
|
+
const starting = this.#starting.get(root);
|
|
141
|
+
this.#starting.delete(root);
|
|
142
|
+
const entry = this.#entries.get(root);
|
|
143
|
+
this.#entries.delete(root);
|
|
144
|
+
if (entry)
|
|
145
|
+
await entry.worker.end().catch(() => undefined);
|
|
146
|
+
if (starting)
|
|
147
|
+
await starting.catch(() => null);
|
|
148
|
+
if (this.#entries.size === 0)
|
|
149
|
+
this.#backend = "none";
|
|
150
|
+
}
|
|
151
|
+
async shutdown() {
|
|
152
|
+
if (this.#shutdownPromise)
|
|
153
|
+
return this.#shutdownPromise;
|
|
154
|
+
const shutdown = this.#shutdownAll();
|
|
155
|
+
this.#shutdownPromise = shutdown;
|
|
156
|
+
try {
|
|
157
|
+
await shutdown;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
if (this.#shutdownPromise === shutdown)
|
|
161
|
+
this.#shutdownPromise = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async #shutdownAll() {
|
|
165
|
+
const roots = new Set([...this.#entries.keys(), ...this.#starting.keys()]);
|
|
166
|
+
for (const root of roots)
|
|
167
|
+
this.#generations.set(root, this.#generationFor(root) + 1);
|
|
168
|
+
const entries = [...this.#entries.values()];
|
|
169
|
+
const starting = [...this.#starting.values()];
|
|
170
|
+
this.#entries.clear();
|
|
171
|
+
this.#starting.clear();
|
|
172
|
+
this.#backend = "none";
|
|
173
|
+
await Promise.all([
|
|
174
|
+
...entries.map((e) => e.worker.end().catch(() => undefined)),
|
|
175
|
+
...starting.map((start) => start.catch(() => null)),
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
#generationFor(root) {
|
|
179
|
+
return this.#generations.get(root) ?? 0;
|
|
180
|
+
}
|
|
181
|
+
async #start(root) {
|
|
182
|
+
const gen = this.#generationFor(root);
|
|
183
|
+
const opts = this.#options ?? {};
|
|
184
|
+
// 1) In-process NAPI (preferred — zero spawn).
|
|
185
|
+
const binding = loadCodemodeNative();
|
|
186
|
+
if (binding) {
|
|
187
|
+
try {
|
|
188
|
+
const config = { root };
|
|
189
|
+
if (opts.indexPath)
|
|
190
|
+
config.indexPath = opts.indexPath;
|
|
191
|
+
if (opts.limit !== undefined)
|
|
192
|
+
config.limit = opts.limit;
|
|
193
|
+
if (opts.useEmbed !== undefined)
|
|
194
|
+
config.useEmbed = opts.useEmbed;
|
|
195
|
+
const session = new binding.Session(config);
|
|
196
|
+
const worker = inProcessWorker(session);
|
|
197
|
+
if (gen !== this.#generationFor(root)) {
|
|
198
|
+
await worker.end().catch(() => undefined);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
this.#entries.set(root, { root, worker, generation: gen, backend: "napi" });
|
|
202
|
+
this.#backend = "napi";
|
|
203
|
+
return worker;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Fall through to CLI sticky.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// 2) CLI sticky fallback (degraded).
|
|
210
|
+
if (!opts.binary)
|
|
211
|
+
return null;
|
|
212
|
+
try {
|
|
213
|
+
const stickyOpts = {
|
|
214
|
+
binary: opts.binary,
|
|
215
|
+
cwd: root,
|
|
216
|
+
};
|
|
217
|
+
if (opts.env)
|
|
218
|
+
stickyOpts.env = opts.env;
|
|
219
|
+
if (opts.timeoutMs !== undefined)
|
|
220
|
+
stickyOpts.timeoutMs = opts.timeoutMs;
|
|
221
|
+
if (opts.maxOutputBytes !== undefined)
|
|
222
|
+
stickyOpts.maxOutputBytes = opts.maxOutputBytes;
|
|
223
|
+
const worker = await this.#startFn(stickyOpts);
|
|
224
|
+
if (gen !== this.#generationFor(root)) {
|
|
225
|
+
await worker.end().catch(() => undefined);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
this.#entries.set(root, { root, worker, generation: gen, backend: "cli" });
|
|
229
|
+
this.#backend = "cli";
|
|
230
|
+
return worker;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Singleton for advanced hosts; tools registration uses a local pool. */
|
|
238
|
+
export const sharedNativePool = new NativeSessionPool();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Typed surface the model sees inside a Code Mode program (`asgrep.*`). */
|
|
2
|
+
export type SearchArgs = {
|
|
3
|
+
query: string;
|
|
4
|
+
limit?: number;
|
|
5
|
+
excerptLines?: number;
|
|
6
|
+
format?: "capsule" | "agent";
|
|
7
|
+
};
|
|
8
|
+
export type ChainArgs = {
|
|
9
|
+
query: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
excerptLines?: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Compact TypeScript declarations for the `asgrep` tool description.
|
|
15
|
+
* Keep short — every token here is paid on every turn (schema landfill lesson
|
|
16
|
+
* from pi-codex-conversion: compose inside Code Mode, don't dump 17 schemas).
|
|
17
|
+
*/
|
|
18
|
+
export declare const CODEMODE_TYPES_FOR_MODEL: string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Typed surface the model sees inside a Code Mode program (`asgrep.*`). */
|
|
2
|
+
/**
|
|
3
|
+
* Compact TypeScript declarations for the `asgrep` tool description.
|
|
4
|
+
* Keep short — every token here is paid on every turn (schema landfill lesson
|
|
5
|
+
* from pi-codex-conversion: compose inside Code Mode, don't dump 17 schemas).
|
|
6
|
+
*/
|
|
7
|
+
export const CODEMODE_TYPES_FOR_MODEL = `
|
|
8
|
+
declare const asgrep: {
|
|
9
|
+
search(input: { query: string; limit?: number; excerptLines?: number }): Promise<unknown>;
|
|
10
|
+
semantic(input: { query: string; limit?: number; excerptLines?: number }): Promise<unknown>;
|
|
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. */
|
|
21
|
+
`.trim();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticky NDJSON Code Mode worker (`asgrep codemode-serve`).
|
|
3
|
+
*
|
|
4
|
+
* One process, one warm Searcher, for the entire Code Mode program — the biggest
|
|
5
|
+
* Amdahl win over per-wave `codemode-batch` spawns.
|
|
6
|
+
*/
|
|
7
|
+
import type { MachineEnvelope } from "../runtime.js";
|
|
8
|
+
import { type StickyWorker } from "./dispatch.js";
|
|
9
|
+
export type StickyWorkerOptions = {
|
|
10
|
+
binary: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** Kill worker when one request exceeds this duration (ms). */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Maximum bytes accepted in one NDJSON response. */
|
|
17
|
+
maxOutputBytes?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function startStickyWorker(options: StickyWorkerOptions): Promise<StickyWorker>;
|
|
20
|
+
/** One-shot batch via stdin (avoids tempfile). */
|
|
21
|
+
export declare function runBatchViaStdin(options: {
|
|
22
|
+
binary: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
body: string;
|
|
25
|
+
env?: NodeJS.ProcessEnv;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
maxOutputBytes?: number;
|
|
29
|
+
}): Promise<MachineEnvelope>;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticky NDJSON Code Mode worker (`asgrep codemode-serve`).
|
|
3
|
+
*
|
|
4
|
+
* One process, one warm Searcher, for the entire Code Mode program — the biggest
|
|
5
|
+
* Amdahl win over per-wave `codemode-batch` spawns.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { asEnvelope } from "./dispatch.js";
|
|
9
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
export async function startStickyWorker(options) {
|
|
11
|
+
if (options.signal?.aborted) {
|
|
12
|
+
throw new Error("codemode-serve aborted before start");
|
|
13
|
+
}
|
|
14
|
+
const child = spawn(options.binary, ["--root", options.cwd, "codemode-serve"], {
|
|
15
|
+
cwd: options.cwd,
|
|
16
|
+
env: { ...process.env, ...options.env, NO_COLOR: "1" },
|
|
17
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
18
|
+
});
|
|
19
|
+
const pending = new Map();
|
|
20
|
+
let nextId = 0;
|
|
21
|
+
let closed = false;
|
|
22
|
+
let stderr = "";
|
|
23
|
+
let stdout = Buffer.alloc(0);
|
|
24
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
26
|
+
const failAll = (err) => {
|
|
27
|
+
for (const p of pending.values())
|
|
28
|
+
p.reject(err);
|
|
29
|
+
pending.clear();
|
|
30
|
+
};
|
|
31
|
+
const terminate = (err) => {
|
|
32
|
+
if (closed)
|
|
33
|
+
return;
|
|
34
|
+
closed = true;
|
|
35
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
36
|
+
killChild(child);
|
|
37
|
+
failAll(err);
|
|
38
|
+
child.stdout.destroy();
|
|
39
|
+
};
|
|
40
|
+
child.stdin.on("error", terminate);
|
|
41
|
+
const handleLine = (line) => {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed)
|
|
44
|
+
return;
|
|
45
|
+
let msg;
|
|
46
|
+
try {
|
|
47
|
+
msg = JSON.parse(trimmed);
|
|
48
|
+
}
|
|
49
|
+
catch (cause) {
|
|
50
|
+
terminate(new Error(`codemode-serve bad JSON: ${trimmed.slice(0, 200)}`));
|
|
51
|
+
void cause;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const type = msg.type;
|
|
55
|
+
if (type === "bye")
|
|
56
|
+
return;
|
|
57
|
+
const id = typeof msg.id === "string" ? msg.id : undefined;
|
|
58
|
+
if (!id)
|
|
59
|
+
return;
|
|
60
|
+
const waiter = pending.get(id);
|
|
61
|
+
if (!waiter)
|
|
62
|
+
return;
|
|
63
|
+
pending.delete(id);
|
|
64
|
+
waiter.resolve(msg);
|
|
65
|
+
};
|
|
66
|
+
child.stdout.on("data", (chunk) => {
|
|
67
|
+
if (closed)
|
|
68
|
+
return;
|
|
69
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
70
|
+
let offset = 0;
|
|
71
|
+
let newline;
|
|
72
|
+
while ((newline = bytes.indexOf(0x0a, offset)) >= 0) {
|
|
73
|
+
const segment = bytes.subarray(offset, newline);
|
|
74
|
+
const lineBytes = stdout.length + segment.length + 1;
|
|
75
|
+
if (lineBytes > maxOutputBytes) {
|
|
76
|
+
terminate(new Error(`codemode-serve output exceeded ${maxOutputBytes} bytes`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const line = stdout.length === 0
|
|
80
|
+
? segment
|
|
81
|
+
: Buffer.concat([stdout, segment], stdout.length + segment.length);
|
|
82
|
+
stdout = Buffer.alloc(0);
|
|
83
|
+
try {
|
|
84
|
+
handleLine(decoder.decode(line));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
terminate(new Error("codemode-serve output is not valid UTF-8"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (closed)
|
|
91
|
+
return;
|
|
92
|
+
offset = newline + 1;
|
|
93
|
+
}
|
|
94
|
+
const tail = bytes.subarray(offset);
|
|
95
|
+
if (stdout.length + tail.length > maxOutputBytes) {
|
|
96
|
+
terminate(new Error(`codemode-serve output exceeded ${maxOutputBytes} bytes`));
|
|
97
|
+
}
|
|
98
|
+
else if (tail.length > 0) {
|
|
99
|
+
stdout = stdout.length === 0
|
|
100
|
+
? Buffer.from(tail)
|
|
101
|
+
: Buffer.concat([stdout, tail], stdout.length + tail.length);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
child.stderr.on("data", (chunk) => {
|
|
105
|
+
stderr += String(chunk);
|
|
106
|
+
if (stderr.length > 8_192)
|
|
107
|
+
stderr = stderr.slice(-8_192);
|
|
108
|
+
});
|
|
109
|
+
child.on("error", (err) => {
|
|
110
|
+
terminate(err);
|
|
111
|
+
});
|
|
112
|
+
child.on("close", (code, signal) => {
|
|
113
|
+
closed = true;
|
|
114
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
115
|
+
if (pending.size > 0) {
|
|
116
|
+
failAll(new Error(`codemode-serve exited code=${code ?? "null"} signal=${signal ?? "null"} stderr=${stderr.slice(0, 512)}`));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const onAbort = () => terminate(new Error("codemode-serve aborted"));
|
|
120
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
121
|
+
const write = (payload) => {
|
|
122
|
+
if (closed || !child.stdin.writable) {
|
|
123
|
+
return Promise.reject(new Error("codemode-serve is closed"));
|
|
124
|
+
}
|
|
125
|
+
const id = typeof payload.id === "string" ? payload.id : String(nextId++);
|
|
126
|
+
payload.id = id;
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
pending.set(id, { resolve, reject });
|
|
129
|
+
try {
|
|
130
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
|
|
131
|
+
if (err)
|
|
132
|
+
terminate(err);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
terminate(cause instanceof Error ? cause : new Error(String(cause)));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const writeWithControls = (payload, label, signal) => {
|
|
141
|
+
if (signal?.aborted)
|
|
142
|
+
return Promise.reject(new Error(`${label} aborted`));
|
|
143
|
+
const response = write(payload);
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let timer;
|
|
146
|
+
let settled = false;
|
|
147
|
+
const finish = (action) => {
|
|
148
|
+
if (settled)
|
|
149
|
+
return;
|
|
150
|
+
settled = true;
|
|
151
|
+
if (timer)
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
signal?.removeEventListener("abort", onRequestAbort);
|
|
154
|
+
action();
|
|
155
|
+
};
|
|
156
|
+
const fail = (err) => finish(() => {
|
|
157
|
+
terminate(err);
|
|
158
|
+
reject(err);
|
|
159
|
+
});
|
|
160
|
+
const onRequestAbort = () => fail(new Error(`${label} aborted`));
|
|
161
|
+
signal?.addEventListener("abort", onRequestAbort, { once: true });
|
|
162
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
163
|
+
timer = setTimeout(() => fail(new Error(`${label} timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
|
|
164
|
+
}
|
|
165
|
+
response.then((value) => finish(() => resolve(value)), (cause) => finish(() => reject(cause)));
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
// Probe: empty End would close — instead send a tiny catalog call to verify protocol,
|
|
169
|
+
// or just return and let first real call fail. Prefer lazy: no probe.
|
|
170
|
+
return {
|
|
171
|
+
async call(tool, args, callOptions) {
|
|
172
|
+
const msg = await writeWithControls({ type: "call", tool, args }, "codemode call", callOptions?.signal);
|
|
173
|
+
if (msg.type === "error") {
|
|
174
|
+
throw new Error(typeof msg.error === "string" ? msg.error : "codemode-serve error");
|
|
175
|
+
}
|
|
176
|
+
if (msg.ok === false) {
|
|
177
|
+
throw new Error(typeof msg.error === "string" ? msg.error : `codemode ${tool} failed`);
|
|
178
|
+
}
|
|
179
|
+
return asEnvelope(msg.value, tool);
|
|
180
|
+
},
|
|
181
|
+
async batch(calls, callOptions) {
|
|
182
|
+
const msg = await writeWithControls({ type: "batch", calls }, "codemode batch", callOptions?.signal);
|
|
183
|
+
if (msg.type === "error") {
|
|
184
|
+
throw new Error(typeof msg.error === "string" ? msg.error : "codemode-serve batch error");
|
|
185
|
+
}
|
|
186
|
+
const results = Array.isArray(msg.results)
|
|
187
|
+
? msg.results
|
|
188
|
+
: [];
|
|
189
|
+
const out = { results };
|
|
190
|
+
if (typeof msg.mode === "string")
|
|
191
|
+
out.mode = msg.mode;
|
|
192
|
+
if (typeof msg.wall_ms === "number")
|
|
193
|
+
out.wall_ms = msg.wall_ms;
|
|
194
|
+
if (typeof msg.all_ok === "boolean")
|
|
195
|
+
out.all_ok = msg.all_ok;
|
|
196
|
+
return out;
|
|
197
|
+
},
|
|
198
|
+
async end() {
|
|
199
|
+
if (closed)
|
|
200
|
+
return;
|
|
201
|
+
terminate(new Error("codemode-serve ended"));
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** One-shot batch via stdin (avoids tempfile). */
|
|
206
|
+
export async function runBatchViaStdin(options) {
|
|
207
|
+
if (options.signal?.aborted)
|
|
208
|
+
throw new Error("codemode-batch aborted");
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const child = spawn(options.binary, ["codemode-batch", "--requests", "-", "--json"], {
|
|
211
|
+
cwd: options.cwd,
|
|
212
|
+
env: { ...process.env, ...options.env, NO_COLOR: "1" },
|
|
213
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
214
|
+
});
|
|
215
|
+
let stdout = "";
|
|
216
|
+
let stderr = "";
|
|
217
|
+
let outputBytes = 0;
|
|
218
|
+
let settled = false;
|
|
219
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
220
|
+
const cleanup = () => {
|
|
221
|
+
if (timer)
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
224
|
+
};
|
|
225
|
+
const finish = (action) => {
|
|
226
|
+
if (settled)
|
|
227
|
+
return;
|
|
228
|
+
settled = true;
|
|
229
|
+
cleanup();
|
|
230
|
+
action();
|
|
231
|
+
};
|
|
232
|
+
const fail = (error) => finish(() => {
|
|
233
|
+
killChild(child);
|
|
234
|
+
reject(error);
|
|
235
|
+
});
|
|
236
|
+
child.stdin.on("error", fail);
|
|
237
|
+
const timer = options.timeoutMs && options.timeoutMs > 0
|
|
238
|
+
? setTimeout(() => {
|
|
239
|
+
fail(new Error(`codemode-batch timed out after ${options.timeoutMs}ms`));
|
|
240
|
+
}, options.timeoutMs)
|
|
241
|
+
: undefined;
|
|
242
|
+
const onAbort = () => {
|
|
243
|
+
fail(new Error("codemode-batch aborted"));
|
|
244
|
+
};
|
|
245
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
246
|
+
child.stdout.on("data", (c) => {
|
|
247
|
+
outputBytes += Buffer.byteLength(c);
|
|
248
|
+
if (outputBytes > maxOutputBytes) {
|
|
249
|
+
fail(new Error(`codemode-batch output exceeded ${maxOutputBytes} bytes`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
stdout += String(c);
|
|
253
|
+
});
|
|
254
|
+
child.stderr.on("data", (c) => {
|
|
255
|
+
outputBytes += Buffer.byteLength(c);
|
|
256
|
+
if (outputBytes > maxOutputBytes) {
|
|
257
|
+
fail(new Error(`codemode-batch output exceeded ${maxOutputBytes} bytes`));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
stderr += String(c);
|
|
261
|
+
});
|
|
262
|
+
child.on("error", (err) => {
|
|
263
|
+
fail(err);
|
|
264
|
+
});
|
|
265
|
+
child.on("close", (code) => {
|
|
266
|
+
if (settled)
|
|
267
|
+
return;
|
|
268
|
+
if (code !== 0) {
|
|
269
|
+
fail(new Error(`codemode-batch exited ${code}: ${stderr.slice(0, 512) || stdout.slice(0, 512)}`));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const value = JSON.parse(stdout);
|
|
274
|
+
finish(() => resolve(value));
|
|
275
|
+
}
|
|
276
|
+
catch (cause) {
|
|
277
|
+
fail(cause instanceof Error ? cause : new Error(String(cause)));
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
child.stdin.write(options.body, (err) => {
|
|
281
|
+
if (err) {
|
|
282
|
+
fail(err);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
child.stdin.end();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function killChild(child) {
|
|
290
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
291
|
+
return;
|
|
292
|
+
try {
|
|
293
|
+
child.kill("SIGTERM");
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
try {
|
|
300
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
301
|
+
child.kill("SIGKILL");
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// ignore
|
|
305
|
+
}
|
|
306
|
+
}, 2_000).unref?.();
|
|
307
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { FreshnessCoordinator, type
|
|
3
|
-
type RuntimeLike =
|
|
4
|
-
|
|
2
|
+
import { FreshnessCoordinator, type MachineEnvelope, type RunOptions } from "./runtime.js";
|
|
3
|
+
type RuntimeLike = {
|
|
4
|
+
run(args: readonly string[], context: {
|
|
5
|
+
cwd: string;
|
|
6
|
+
}, options?: RunOptions): Promise<MachineEnvelope>;
|
|
7
|
+
resolveRoot?(context: {
|
|
8
|
+
cwd: string;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
resolveBinaryPath?(options?: {
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
}): string;
|
|
13
|
+
nativeEnv?(options?: {
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
}): NodeJS.ProcessEnv;
|
|
16
|
+
config?: {
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
maxOutputBytes?: number;
|
|
19
|
+
refreshIntervalMs?: number;
|
|
20
|
+
};
|
|
21
|
+
inspectIndexCompatibility?(context: {
|
|
22
|
+
cwd: string;
|
|
23
|
+
}): Promise<"ready" | "missing" | "incompatible">;
|
|
24
|
+
rebuildIncompatibleIndex?(context: {
|
|
25
|
+
cwd: string;
|
|
26
|
+
}, options?: RunOptions): Promise<MachineEnvelope>;
|
|
27
|
+
resolveIndexPath?(root: string): string;
|
|
28
|
+
watchExternalChanges?: boolean;
|
|
29
|
+
};
|
|
30
|
+
type FreshnessLike = Pick<FreshnessCoordinator, "ensureFresh" | "markAffectedPath"> & {
|
|
31
|
+
shutdown?(): void;
|
|
32
|
+
};
|
|
5
33
|
export declare function registerAstSgrepTools(pi: ExtensionAPI, runtime?: RuntimeLike, freshness?: FreshnessLike): void;
|
|
6
34
|
export declare function registerAstSgrepCommands(pi: ExtensionAPI, runtime?: RuntimeLike): void;
|
|
7
35
|
export default function astSgrepExtension(pi: ExtensionAPI): void;
|