pi-ast-sgrep 1.3.2 → 1.4.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 +182 -18
- package/dist/code-mode.d.ts +81 -0
- package/dist/code-mode.js +348 -0
- package/dist/codemode/connector.d.ts +64 -0
- package/dist/codemode/connector.js +73 -0
- package/dist/codemode/dispatch.d.ts +84 -0
- package/dist/codemode/dispatch.js +342 -0
- package/dist/codemode/index.d.ts +19 -0
- package/dist/codemode/index.js +19 -0
- package/dist/codemode/native.d.ts +58 -0
- package/dist/codemode/native.js +110 -0
- package/dist/codemode/sandbox.d.ts +25 -0
- package/dist/codemode/sandbox.js +192 -0
- package/dist/codemode/session-pool.d.ts +39 -0
- package/dist/codemode/session-pool.js +173 -0
- package/dist/codemode/types.d.ts +18 -0
- package/dist/codemode/types.js +21 -0
- package/dist/codemode/worker.d.ts +26 -0
- package/dist/codemode/worker.js +250 -0
- package/dist/index.d.ts +25 -2
- package/dist/index.js +297 -11
- package/dist/runtime.d.ts +15 -2
- package/dist/runtime.js +60 -40
- package/native/.gitignore +3 -0
- package/native/README.md +17 -0
- package/package.json +30 -7
- package/skills/ast-sgrep/SKILL.md +42 -5
- package/skills/ast-sgrep/references/query-guide.md +8 -7
|
@@ -0,0 +1,250 @@
|
|
|
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 { createInterface } from "node:readline";
|
|
9
|
+
import { asEnvelope } from "./dispatch.js";
|
|
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
|
+
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
24
|
+
const failAll = (err) => {
|
|
25
|
+
for (const p of pending.values())
|
|
26
|
+
p.reject(err);
|
|
27
|
+
pending.clear();
|
|
28
|
+
};
|
|
29
|
+
const terminate = (err) => {
|
|
30
|
+
if (closed)
|
|
31
|
+
return;
|
|
32
|
+
closed = true;
|
|
33
|
+
killChild(child);
|
|
34
|
+
failAll(err);
|
|
35
|
+
rl.close();
|
|
36
|
+
};
|
|
37
|
+
rl.on("line", (line) => {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed)
|
|
40
|
+
return;
|
|
41
|
+
let msg;
|
|
42
|
+
try {
|
|
43
|
+
msg = JSON.parse(trimmed);
|
|
44
|
+
}
|
|
45
|
+
catch (cause) {
|
|
46
|
+
failAll(new Error(`codemode-serve bad JSON: ${trimmed.slice(0, 200)}`));
|
|
47
|
+
void cause;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const type = msg.type;
|
|
51
|
+
if (type === "bye")
|
|
52
|
+
return;
|
|
53
|
+
const id = typeof msg.id === "string" ? msg.id : undefined;
|
|
54
|
+
if (!id)
|
|
55
|
+
return;
|
|
56
|
+
const waiter = pending.get(id);
|
|
57
|
+
if (!waiter)
|
|
58
|
+
return;
|
|
59
|
+
pending.delete(id);
|
|
60
|
+
waiter.resolve(msg);
|
|
61
|
+
});
|
|
62
|
+
child.stderr.on("data", (chunk) => {
|
|
63
|
+
stderr += String(chunk);
|
|
64
|
+
if (stderr.length > 8_192)
|
|
65
|
+
stderr = stderr.slice(-8_192);
|
|
66
|
+
});
|
|
67
|
+
child.on("error", (err) => {
|
|
68
|
+
closed = true;
|
|
69
|
+
failAll(err);
|
|
70
|
+
});
|
|
71
|
+
child.on("close", (code, signal) => {
|
|
72
|
+
closed = true;
|
|
73
|
+
if (pending.size > 0) {
|
|
74
|
+
failAll(new Error(`codemode-serve exited code=${code ?? "null"} signal=${signal ?? "null"} stderr=${stderr.slice(0, 512)}`));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const onAbort = () => terminate(new Error("codemode-serve aborted"));
|
|
78
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
79
|
+
const write = (payload) => {
|
|
80
|
+
if (closed || !child.stdin.writable) {
|
|
81
|
+
return Promise.reject(new Error("codemode-serve is closed"));
|
|
82
|
+
}
|
|
83
|
+
const id = typeof payload.id === "string" ? payload.id : String(nextId++);
|
|
84
|
+
payload.id = id;
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
pending.set(id, { resolve, reject });
|
|
87
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
|
|
88
|
+
if (err) {
|
|
89
|
+
pending.delete(id);
|
|
90
|
+
reject(err);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const writeWithControls = (payload, label, signal) => {
|
|
96
|
+
if (signal?.aborted)
|
|
97
|
+
return Promise.reject(new Error(`${label} aborted`));
|
|
98
|
+
const response = write(payload);
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
let timer;
|
|
101
|
+
let settled = false;
|
|
102
|
+
const finish = (action) => {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
if (timer)
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
signal?.removeEventListener("abort", onRequestAbort);
|
|
109
|
+
action();
|
|
110
|
+
};
|
|
111
|
+
const fail = (err) => finish(() => {
|
|
112
|
+
terminate(err);
|
|
113
|
+
reject(err);
|
|
114
|
+
});
|
|
115
|
+
const onRequestAbort = () => fail(new Error(`${label} aborted`));
|
|
116
|
+
signal?.addEventListener("abort", onRequestAbort, { once: true });
|
|
117
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
118
|
+
timer = setTimeout(() => fail(new Error(`${label} timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
|
|
119
|
+
}
|
|
120
|
+
response.then((value) => finish(() => resolve(value)), (cause) => finish(() => reject(cause)));
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
// Probe: empty End would close — instead send a tiny catalog call to verify protocol,
|
|
124
|
+
// or just return and let first real call fail. Prefer lazy: no probe.
|
|
125
|
+
return {
|
|
126
|
+
async call(tool, args, callOptions) {
|
|
127
|
+
const msg = await writeWithControls({ type: "call", tool, args }, "codemode call", callOptions?.signal);
|
|
128
|
+
if (msg.type === "error") {
|
|
129
|
+
throw new Error(typeof msg.error === "string" ? msg.error : "codemode-serve error");
|
|
130
|
+
}
|
|
131
|
+
if (msg.ok === false) {
|
|
132
|
+
throw new Error(typeof msg.error === "string" ? msg.error : `codemode ${tool} failed`);
|
|
133
|
+
}
|
|
134
|
+
return asEnvelope(msg.value, tool);
|
|
135
|
+
},
|
|
136
|
+
async batch(calls, callOptions) {
|
|
137
|
+
const msg = await writeWithControls({ type: "batch", calls }, "codemode batch", callOptions?.signal);
|
|
138
|
+
if (msg.type === "error") {
|
|
139
|
+
throw new Error(typeof msg.error === "string" ? msg.error : "codemode-serve batch error");
|
|
140
|
+
}
|
|
141
|
+
const results = Array.isArray(msg.results)
|
|
142
|
+
? msg.results
|
|
143
|
+
: [];
|
|
144
|
+
const out = { results };
|
|
145
|
+
if (typeof msg.mode === "string")
|
|
146
|
+
out.mode = msg.mode;
|
|
147
|
+
if (typeof msg.wall_ms === "number")
|
|
148
|
+
out.wall_ms = msg.wall_ms;
|
|
149
|
+
if (typeof msg.all_ok === "boolean")
|
|
150
|
+
out.all_ok = msg.all_ok;
|
|
151
|
+
return out;
|
|
152
|
+
},
|
|
153
|
+
async end() {
|
|
154
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
155
|
+
if (closed)
|
|
156
|
+
return;
|
|
157
|
+
try {
|
|
158
|
+
if (child.stdin.writable) {
|
|
159
|
+
child.stdin.write(`${JSON.stringify({ type: "end" })}\n`);
|
|
160
|
+
child.stdin.end();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
killChild(child);
|
|
167
|
+
closed = true;
|
|
168
|
+
rl.close();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** One-shot batch via stdin (avoids tempfile). */
|
|
173
|
+
export async function runBatchViaStdin(options) {
|
|
174
|
+
if (options.signal?.aborted)
|
|
175
|
+
throw new Error("codemode-batch aborted");
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
const child = spawn(options.binary, ["codemode-batch", "--requests", "-", "--json"], {
|
|
178
|
+
cwd: options.cwd,
|
|
179
|
+
env: { ...process.env, ...options.env, NO_COLOR: "1" },
|
|
180
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
181
|
+
});
|
|
182
|
+
let stdout = "";
|
|
183
|
+
let stderr = "";
|
|
184
|
+
const timer = options.timeoutMs && options.timeoutMs > 0
|
|
185
|
+
? setTimeout(() => {
|
|
186
|
+
killChild(child);
|
|
187
|
+
reject(new Error(`codemode-batch timed out after ${options.timeoutMs}ms`));
|
|
188
|
+
}, options.timeoutMs)
|
|
189
|
+
: undefined;
|
|
190
|
+
const onAbort = () => {
|
|
191
|
+
killChild(child);
|
|
192
|
+
reject(new Error("codemode-batch aborted"));
|
|
193
|
+
};
|
|
194
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
195
|
+
child.stdout.on("data", (c) => {
|
|
196
|
+
stdout += String(c);
|
|
197
|
+
});
|
|
198
|
+
child.stderr.on("data", (c) => {
|
|
199
|
+
stderr += String(c);
|
|
200
|
+
});
|
|
201
|
+
child.on("error", (err) => {
|
|
202
|
+
if (timer)
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
205
|
+
reject(err);
|
|
206
|
+
});
|
|
207
|
+
child.on("close", (code) => {
|
|
208
|
+
if (timer)
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
211
|
+
if (code !== 0) {
|
|
212
|
+
reject(new Error(`codemode-batch exited ${code}: ${stderr.slice(0, 512) || stdout.slice(0, 512)}`));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
resolve(JSON.parse(stdout));
|
|
217
|
+
}
|
|
218
|
+
catch (cause) {
|
|
219
|
+
reject(cause);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
child.stdin.write(options.body, (err) => {
|
|
223
|
+
if (err) {
|
|
224
|
+
killChild(child);
|
|
225
|
+
reject(err);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
child.stdin.end();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function killChild(child) {
|
|
233
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
234
|
+
return;
|
|
235
|
+
try {
|
|
236
|
+
child.kill("SIGTERM");
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
try {
|
|
243
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
244
|
+
child.kill("SIGKILL");
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// ignore
|
|
248
|
+
}
|
|
249
|
+
}, 2_000).unref?.();
|
|
250
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { FreshnessCoordinator, type
|
|
3
|
-
type RuntimeLike =
|
|
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
|
+
refreshIntervalMs?: number;
|
|
19
|
+
};
|
|
20
|
+
inspectIndexCompatibility?(context: {
|
|
21
|
+
cwd: string;
|
|
22
|
+
}): Promise<"ready" | "missing" | "incompatible">;
|
|
23
|
+
rebuildIncompatibleIndex?(context: {
|
|
24
|
+
cwd: string;
|
|
25
|
+
}, options?: RunOptions): Promise<MachineEnvelope>;
|
|
26
|
+
};
|
|
4
27
|
type FreshnessLike = Pick<FreshnessCoordinator, "ensureFresh" | "markAffectedPath">;
|
|
5
28
|
export declare function registerAstSgrepTools(pi: ExtensionAPI, runtime?: RuntimeLike, freshness?: FreshnessLike): void;
|
|
6
29
|
export declare function registerAstSgrepCommands(pi: ExtensionAPI, runtime?: RuntimeLike): void;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { createAsgrepConnector, runCodemode, runNativeBatch, runBatchViaStdin, CODEMODE_TYPES_FOR_MODEL, NativeSessionPool, argvFor, asEnvelope, } from "./codemode/index.js";
|
|
2
3
|
import { AstSgrepRuntime, FreshnessCoordinator, RuntimeError } from "./runtime.js";
|
|
3
4
|
const DEFAULT_LIMIT = 8;
|
|
4
5
|
const MAX_LIMIT = 100;
|
|
5
6
|
const MAX_EXCERPT_LINES = 100;
|
|
6
7
|
const MAX_CONTENT_CHARS = 1_200;
|
|
8
|
+
const MAX_CODEMODE_RESULT_CHARS = 8_000;
|
|
7
9
|
const searchParameters = Type.Object({
|
|
8
10
|
query: Type.String({ minLength: 1, maxLength: 4_096, description: "Natural-language query, symbol, or structural pattern" }),
|
|
9
11
|
mode: Type.Optional(Type.Union([
|
|
@@ -25,6 +27,13 @@ const indexParameters = Type.Object({
|
|
|
25
27
|
force: Type.Optional(Type.Boolean({ default: false, description: "Rebuild the index from scratch" })),
|
|
26
28
|
}, { additionalProperties: false });
|
|
27
29
|
const statusParameters = Type.Object({}, { additionalProperties: false });
|
|
30
|
+
const codemodeParameters = Type.Object({
|
|
31
|
+
code: Type.String({
|
|
32
|
+
minLength: 1,
|
|
33
|
+
maxLength: 32_000,
|
|
34
|
+
description: "JavaScript async body that calls asgrep.* methods. Prefer Promise.all for independent lookups. Return only the shaped final value.",
|
|
35
|
+
}),
|
|
36
|
+
}, { additionalProperties: false });
|
|
28
37
|
function bounded(text) {
|
|
29
38
|
return text.length <= MAX_CONTENT_CHARS ? text : `${text.slice(0, MAX_CONTENT_CHARS - 1)}…`;
|
|
30
39
|
}
|
|
@@ -35,16 +44,22 @@ function success(command, response) {
|
|
|
35
44
|
const summary = count === undefined ? `${command} completed` : `${command} completed: ${count} result${count === 1 ? "" : "s"}`;
|
|
36
45
|
return {
|
|
37
46
|
content: [{ type: "text", text: bounded(summary) }],
|
|
38
|
-
|
|
47
|
+
// The tool execute owns its machine command: normalize the envelope's
|
|
48
|
+
// command (native catalog names like index_status/index_repo must surface
|
|
49
|
+
// as the machine commands status/index/reindex).
|
|
50
|
+
details: { ok: true, command, response: { ...response, command } },
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
|
-
function errorDetails(cause) {
|
|
53
|
+
function errorDetails(cause, signal) {
|
|
54
|
+
if (signal?.aborted) {
|
|
55
|
+
return { code: "CANCELLED", message: "cancelled", details: {} };
|
|
56
|
+
}
|
|
42
57
|
return cause instanceof RuntimeError
|
|
43
58
|
? { code: cause.code, message: cause.message, details: cause.details }
|
|
44
59
|
: { code: "UNEXPECTED_ERROR", message: cause instanceof Error ? cause.message : String(cause), details: {} };
|
|
45
60
|
}
|
|
46
|
-
function failure(command, cause) {
|
|
47
|
-
const error = errorDetails(cause);
|
|
61
|
+
function failure(command, cause, signal) {
|
|
62
|
+
const error = errorDetails(cause, signal);
|
|
48
63
|
return {
|
|
49
64
|
content: [{ type: "text", text: bounded(`${command} failed [${error.code}]: ${error.message}`) }],
|
|
50
65
|
details: { ok: false, command, error },
|
|
@@ -85,6 +100,58 @@ async function execute(runtime, command, args, signal, onUpdate, ctx, before) {
|
|
|
85
100
|
export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), freshness = runtime instanceof AstSgrepRuntime
|
|
86
101
|
? new FreshnessCoordinator({ refreshIntervalMs: runtime.config.refreshIntervalMs })
|
|
87
102
|
: new FreshnessCoordinator()) {
|
|
103
|
+
const pool = new NativeSessionPool();
|
|
104
|
+
// Prefer a registration-local pool so tests / multi-agent hosts do not share
|
|
105
|
+
// sticky state. sharedNativePool remains for advanced single-session reuse.
|
|
106
|
+
const ensurePool = () => {
|
|
107
|
+
try {
|
|
108
|
+
const env = runtime.nativeEnv?.() ?? { NO_COLOR: "1" };
|
|
109
|
+
let binary;
|
|
110
|
+
try {
|
|
111
|
+
binary = runtime.resolveBinaryPath?.({ env });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
binary = undefined;
|
|
115
|
+
}
|
|
116
|
+
const opts = { env };
|
|
117
|
+
if (binary)
|
|
118
|
+
opts.binary = binary;
|
|
119
|
+
if (runtime.config?.timeoutMs !== undefined)
|
|
120
|
+
opts.timeoutMs = runtime.config.timeoutMs;
|
|
121
|
+
if (typeof env.ASGREP_NO_EMBED === "string") {
|
|
122
|
+
opts.useEmbed = env.ASGREP_NO_EMBED !== "1" && env.ASGREP_NO_EMBED !== "true";
|
|
123
|
+
}
|
|
124
|
+
if (typeof env.ASGREP_INDEX_PATH === "string")
|
|
125
|
+
opts.indexPath = env.ASGREP_INDEX_PATH;
|
|
126
|
+
pool.configure(opts);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
pool.configure({});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const resolveRoot = async (cwd) => runtime.resolveRoot ? await runtime.resolveRoot({ cwd }) : cwd;
|
|
133
|
+
const nativeCall = async (tool, args, context, options = {}) => {
|
|
134
|
+
ensurePool();
|
|
135
|
+
const root = await resolveRoot(context.cwd);
|
|
136
|
+
const worker = await pool.acquire(root);
|
|
137
|
+
if (worker) {
|
|
138
|
+
return asEnvelope(await worker.call(tool, args, options.signal ? { signal: options.signal } : {}));
|
|
139
|
+
}
|
|
140
|
+
// Last resort: cold CLI argv (tests without NAPI / missing addon).
|
|
141
|
+
return runtime.run(argvFor(tool, args), context, options);
|
|
142
|
+
};
|
|
143
|
+
// Freshness + tools share the same warm in-process Searcher as Code Mode.
|
|
144
|
+
const warmRuntime = {
|
|
145
|
+
run: (args, context, options) => runtime.run(args, context, options),
|
|
146
|
+
resolveRoot: (context) => resolveRoot(context.cwd),
|
|
147
|
+
nativeCall,
|
|
148
|
+
};
|
|
149
|
+
if (runtime.inspectIndexCompatibility) {
|
|
150
|
+
warmRuntime.inspectIndexCompatibility = (context) => runtime.inspectIndexCompatibility(context);
|
|
151
|
+
}
|
|
152
|
+
if (runtime.rebuildIncompatibleIndex) {
|
|
153
|
+
warmRuntime.rebuildIncompatibleIndex = (context, options) => runtime.rebuildIncompatibleIndex(context, options);
|
|
154
|
+
}
|
|
88
155
|
pi.on("tool_result", (event, ctx) => {
|
|
89
156
|
if (event.isError || (event.toolName !== "write" && event.toolName !== "edit"))
|
|
90
157
|
return;
|
|
@@ -92,36 +159,255 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
|
|
|
92
159
|
if (typeof path === "string")
|
|
93
160
|
freshness.markAffectedPath(path, ctx.cwd);
|
|
94
161
|
});
|
|
162
|
+
// Primary surface: Code Mode — in-process NAPI (MCP-class), compose in JS.
|
|
163
|
+
// Sibling to MCP: pick one surface; both link core, never each other.
|
|
164
|
+
pi.registerTool({
|
|
165
|
+
name: "asgrep_codemode",
|
|
166
|
+
label: "ast-sgrep Code Mode",
|
|
167
|
+
description: [
|
|
168
|
+
"Primary ast-sgrep tool. Write JavaScript that calls typed asgrep.* methods.",
|
|
169
|
+
"Compose with await / Promise.all, filter in code, return only the shaped final value.",
|
|
170
|
+
"Runs in-process (native addon) — no CLI spawn; warm Searcher for the Pi session.",
|
|
171
|
+
"",
|
|
172
|
+
CODEMODE_TYPES_FOR_MODEL,
|
|
173
|
+
"",
|
|
174
|
+
"Example:",
|
|
175
|
+
"async () => {",
|
|
176
|
+
" const [seed, status] = await Promise.all([",
|
|
177
|
+
" asgrep.search({ query: 'auth refresh', limit: 5 }),",
|
|
178
|
+
" asgrep.indexStatus(),",
|
|
179
|
+
" ]);",
|
|
180
|
+
" const symbol = seed.hits?.[0]?.symbol;",
|
|
181
|
+
" if (!symbol) return { seed, status };",
|
|
182
|
+
" const graph = await asgrep.chain({ query: symbol, limit: 20 });",
|
|
183
|
+
" return { symbol, nodes: graph.nodes?.slice?.(0, 10) ?? graph, status };",
|
|
184
|
+
"}",
|
|
185
|
+
].join("\n"),
|
|
186
|
+
parameters: codemodeParameters,
|
|
187
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
188
|
+
report(onUpdate, "codemode", "started");
|
|
189
|
+
try {
|
|
190
|
+
const options = signal ? { signal } : {};
|
|
191
|
+
await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
|
|
192
|
+
ensurePool();
|
|
193
|
+
const root = await resolveRoot(ctx.cwd);
|
|
194
|
+
const env = runtime.nativeEnv?.() ?? { NO_COLOR: "1" };
|
|
195
|
+
let binary = null;
|
|
196
|
+
try {
|
|
197
|
+
binary = runtime.resolveBinaryPath?.({ env }) ?? null;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
binary = null;
|
|
201
|
+
}
|
|
202
|
+
// In-process NAPI first; CLI sticky only if addon missing.
|
|
203
|
+
const sticky = await pool.acquire(root);
|
|
204
|
+
const batchHost = {
|
|
205
|
+
run: (args, context, runOptions) => runtime.run(args, context, runOptions ?? {}),
|
|
206
|
+
sticky,
|
|
207
|
+
};
|
|
208
|
+
if (binary) {
|
|
209
|
+
batchHost.runBatch = (calls, context, runOptions) => runNativeBatch((a, c, o) => runtime.run(a, c, o ?? {}), calls, context, runOptions, (body, c, o) => {
|
|
210
|
+
const stdinOpts = {
|
|
211
|
+
binary: binary,
|
|
212
|
+
cwd: c.cwd,
|
|
213
|
+
body,
|
|
214
|
+
env,
|
|
215
|
+
};
|
|
216
|
+
if (o?.signal)
|
|
217
|
+
stdinOpts.signal = o.signal;
|
|
218
|
+
if (runtime.config?.timeoutMs !== undefined)
|
|
219
|
+
stdinOpts.timeoutMs = runtime.config.timeoutMs;
|
|
220
|
+
return runBatchViaStdin(stdinOpts);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const bundle = createAsgrepConnector(batchHost, { cwd: ctx.cwd }, options);
|
|
224
|
+
bundle.resetStats();
|
|
225
|
+
const codemodeOptions = { stats: bundle.stats };
|
|
226
|
+
if (runtime.config?.timeoutMs !== undefined)
|
|
227
|
+
codemodeOptions.timeoutMs = runtime.config.timeoutMs;
|
|
228
|
+
if (signal)
|
|
229
|
+
codemodeOptions.signal = signal;
|
|
230
|
+
const outcome = await runCodemode(params.code, bundle.asgrep, codemodeOptions);
|
|
231
|
+
report(onUpdate, "codemode", "completed");
|
|
232
|
+
if (!outcome.ok) {
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: bounded(`codemode failed: ${outcome.error ?? "unknown error"}`) }],
|
|
235
|
+
details: {
|
|
236
|
+
ok: false,
|
|
237
|
+
command: "codemode",
|
|
238
|
+
error: { code: "CODEMODE_ERROR", message: outcome.error ?? "unknown error", details: { logs: outcome.logs, stats: outcome.stats } },
|
|
239
|
+
code: outcome.code,
|
|
240
|
+
stats: outcome.stats,
|
|
241
|
+
wallMs: outcome.wallMs,
|
|
242
|
+
backend: pool.backend(),
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const rendered = safeRender(outcome.result);
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: bounded(summarizeCodemode(outcome.result, outcome.stats, outcome.wallMs, pool.backend())) }],
|
|
249
|
+
details: {
|
|
250
|
+
ok: true,
|
|
251
|
+
command: "codemode",
|
|
252
|
+
result: outcome.result,
|
|
253
|
+
logs: outcome.logs,
|
|
254
|
+
rendered,
|
|
255
|
+
stats: outcome.stats,
|
|
256
|
+
wallMs: outcome.wallMs,
|
|
257
|
+
backend: pool.backend(),
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch (cause) {
|
|
262
|
+
return failure("codemode", cause);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
// Escape hatches: one-shot tools for simple lookups. Prefer asgrep_codemode.
|
|
267
|
+
// They ride the same session sticky pool when available (no cold spawn).
|
|
95
268
|
pi.registerTool({
|
|
96
269
|
name: "asgrep_search",
|
|
97
270
|
label: "ast-sgrep search",
|
|
98
|
-
description: "
|
|
271
|
+
description: "One-shot search. Prefer asgrep_codemode for anything multi-step, parallel, or filtered.",
|
|
99
272
|
parameters: searchParameters,
|
|
100
273
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
101
274
|
const options = signal ? { signal } : {};
|
|
102
|
-
|
|
275
|
+
report(onUpdate, "search", "started");
|
|
276
|
+
try {
|
|
277
|
+
await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
|
|
278
|
+
ensurePool();
|
|
279
|
+
const root = await resolveRoot(ctx.cwd);
|
|
280
|
+
const sticky = await pool.acquire(root);
|
|
281
|
+
const response = sticky
|
|
282
|
+
? await sticky.call(...searchToolCall(params), options)
|
|
283
|
+
: await runtime.run(searchArgs(params), { cwd: ctx.cwd }, options);
|
|
284
|
+
report(onUpdate, "search", "completed");
|
|
285
|
+
return success("search", response);
|
|
286
|
+
}
|
|
287
|
+
catch (cause) {
|
|
288
|
+
return failure("search", cause);
|
|
289
|
+
}
|
|
103
290
|
},
|
|
104
291
|
});
|
|
105
292
|
pi.registerTool({
|
|
106
293
|
name: "asgrep_index",
|
|
107
294
|
label: "ast-sgrep index",
|
|
108
|
-
description: "Build or rebuild the
|
|
295
|
+
description: "Build or rebuild the index. Prefer asgrep.indexRepo inside asgrep_codemode.",
|
|
109
296
|
parameters: indexParameters,
|
|
110
297
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
111
|
-
const
|
|
112
|
-
|
|
298
|
+
const force = params.force === true;
|
|
299
|
+
const command = force ? "reindex" : "index";
|
|
300
|
+
report(onUpdate, command, "started");
|
|
301
|
+
try {
|
|
302
|
+
ensurePool();
|
|
303
|
+
const root = await resolveRoot(ctx.cwd);
|
|
304
|
+
const sticky = await pool.acquire(root);
|
|
305
|
+
const response = sticky
|
|
306
|
+
? await sticky.call("index_repo", { force }, signal ? { signal } : {})
|
|
307
|
+
: await runtime.run([command, ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
|
|
308
|
+
report(onUpdate, command, "completed");
|
|
309
|
+
return success(command, response);
|
|
310
|
+
}
|
|
311
|
+
catch (cause) {
|
|
312
|
+
return failure(command, cause, signal);
|
|
313
|
+
}
|
|
113
314
|
},
|
|
114
315
|
});
|
|
115
316
|
pi.registerTool({
|
|
116
317
|
name: "asgrep_status",
|
|
117
318
|
label: "ast-sgrep status",
|
|
118
|
-
description: "
|
|
319
|
+
description: "Index/runtime status. Prefer asgrep.indexStatus inside asgrep_codemode.",
|
|
119
320
|
parameters: statusParameters,
|
|
120
321
|
async execute(_toolCallId, _params, signal, onUpdate, ctx) {
|
|
121
|
-
|
|
322
|
+
report(onUpdate, "status", "started");
|
|
323
|
+
try {
|
|
324
|
+
ensurePool();
|
|
325
|
+
const root = await resolveRoot(ctx.cwd);
|
|
326
|
+
const sticky = await pool.acquire(root);
|
|
327
|
+
const response = sticky
|
|
328
|
+
? await sticky.call("index_status", {}, signal ? { signal } : {})
|
|
329
|
+
: await runtime.run(["status", ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
|
|
330
|
+
report(onUpdate, "status", "completed");
|
|
331
|
+
return success("status", response);
|
|
332
|
+
}
|
|
333
|
+
catch (cause) {
|
|
334
|
+
return failure("status", cause);
|
|
335
|
+
}
|
|
122
336
|
},
|
|
123
337
|
});
|
|
124
338
|
}
|
|
339
|
+
/** Map one-shot search params to typed sticky tool+args. */
|
|
340
|
+
function searchToolCall(params) {
|
|
341
|
+
const mode = params.mode ?? "natural";
|
|
342
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
343
|
+
const excerpt_lines = params.excerptLines ?? 0;
|
|
344
|
+
switch (mode) {
|
|
345
|
+
case "semantic":
|
|
346
|
+
return ["semantic", { query: params.query, limit, excerpt_lines, format: "capsule" }];
|
|
347
|
+
case "chain":
|
|
348
|
+
return ["chain", { query: params.query, limit, top_n: 20 }];
|
|
349
|
+
case "defs":
|
|
350
|
+
return ["defs", { symbol: params.query, limit, excerpt_lines }];
|
|
351
|
+
case "callers":
|
|
352
|
+
return ["callers", { symbol: params.query, limit, excerpt_lines }];
|
|
353
|
+
case "imports":
|
|
354
|
+
return ["imports", { module: params.query, limit, excerpt_lines }];
|
|
355
|
+
case "pattern":
|
|
356
|
+
case "word":
|
|
357
|
+
case "literal":
|
|
358
|
+
case "regex":
|
|
359
|
+
return ["search", { query: `${mode}: ${params.query}`, limit, excerpt_lines, format: "capsule" }];
|
|
360
|
+
case "natural":
|
|
361
|
+
return ["search", { query: params.query, limit, excerpt_lines, format: "capsule" }];
|
|
362
|
+
default: {
|
|
363
|
+
const _exhaustive = mode;
|
|
364
|
+
void _exhaustive;
|
|
365
|
+
return ["search", { query: params.query, limit, excerpt_lines, format: "capsule" }];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function safeRender(value) {
|
|
370
|
+
try {
|
|
371
|
+
const text = JSON.stringify(value);
|
|
372
|
+
if (text === undefined)
|
|
373
|
+
return String(value);
|
|
374
|
+
return text.length <= MAX_CODEMODE_RESULT_CHARS ? text : `${text.slice(0, MAX_CODEMODE_RESULT_CHARS - 1)}…`;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return String(value);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function summarizeCodemode(value, stats, wallMs, backend) {
|
|
381
|
+
const parts = ["codemode completed"];
|
|
382
|
+
if (value && typeof value === "object") {
|
|
383
|
+
const record = value;
|
|
384
|
+
if (Array.isArray(record.hits))
|
|
385
|
+
parts[0] = `codemode completed: ${record.hits.length} hit${record.hits.length === 1 ? "" : "s"}`;
|
|
386
|
+
else if (typeof record.hit_count === "number")
|
|
387
|
+
parts[0] = `codemode completed: ${record.hit_count} hit${record.hit_count === 1 ? "" : "s"}`;
|
|
388
|
+
else if (typeof record.node_count === "number")
|
|
389
|
+
parts[0] = `codemode completed: ${record.node_count} node${record.node_count === 1 ? "" : "s"}`;
|
|
390
|
+
}
|
|
391
|
+
if (backend === "napi")
|
|
392
|
+
parts.push("in-process");
|
|
393
|
+
else if (backend === "cli")
|
|
394
|
+
parts.push("cli-sticky");
|
|
395
|
+
if (stats && stats.calls > 0) {
|
|
396
|
+
const via = (stats.stickyCalls ?? 0) > 0
|
|
397
|
+
? `native ${stats.stickyCalls}`
|
|
398
|
+
: stats.batchedCalls > 0
|
|
399
|
+
? `batched ${stats.batchedCalls}`
|
|
400
|
+
: stats.parallelSpawnCalls > 0
|
|
401
|
+
? `parallel-spawn ${stats.parallelSpawnCalls}`
|
|
402
|
+
: `${stats.calls} call${stats.calls === 1 ? "" : "s"}`;
|
|
403
|
+
parts.push(via);
|
|
404
|
+
if (stats.waves > 1)
|
|
405
|
+
parts.push(`${stats.waves} waves`);
|
|
406
|
+
}
|
|
407
|
+
if (wallMs !== undefined)
|
|
408
|
+
parts.push(`${wallMs}ms`);
|
|
409
|
+
return parts.join(" · ");
|
|
410
|
+
}
|
|
125
411
|
const COMMANDS = [
|
|
126
412
|
["asgrep-doctor", "Check the ast-sgrep runtime, native binary, index, and project configuration", "doctor"],
|
|
127
413
|
["asgrep-status", "Show ast-sgrep runtime, index, backend, and capability status", "status"],
|