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
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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";
|
|
4
|
+
import { ASGREP_PROMPT_GUIDELINES, ASGREP_PROMPT_SNIPPET, formatCodemodeCall, formatCodemodeResult, formatIndexCall, formatIndexResult, formatSearchCall, formatSearchResult, formatStatusCall, formatStatusResult, presentText, } from "./present.js";
|
|
3
5
|
const DEFAULT_LIMIT = 8;
|
|
4
6
|
const MAX_LIMIT = 100;
|
|
5
7
|
const MAX_EXCERPT_LINES = 100;
|
|
@@ -25,26 +27,40 @@ 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
|
}
|
|
31
|
-
function success(command, response) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
function success(command, response, extra = {}) {
|
|
41
|
+
const text = command === "status"
|
|
42
|
+
? formatStatusResult(response)
|
|
43
|
+
: command === "index" || command === "reindex"
|
|
44
|
+
? formatIndexResult(command, response)
|
|
45
|
+
: formatSearchResult(response, { command, ...extra });
|
|
36
46
|
return {
|
|
37
|
-
content: [{ type: "text", text: bounded(
|
|
38
|
-
|
|
47
|
+
content: [{ type: "text", text: bounded(text) }],
|
|
48
|
+
// The tool execute owns its machine command: normalize the envelope's
|
|
49
|
+
// command (native catalog names like index_status/index_repo must surface
|
|
50
|
+
// as the machine commands status/index/reindex).
|
|
51
|
+
details: { ok: true, command, response: { ...response, command }, ...extra },
|
|
39
52
|
};
|
|
40
53
|
}
|
|
41
|
-
function errorDetails(cause) {
|
|
54
|
+
function errorDetails(cause, signal) {
|
|
55
|
+
if (signal?.aborted) {
|
|
56
|
+
return { code: "CANCELLED", message: "cancelled", details: {} };
|
|
57
|
+
}
|
|
42
58
|
return cause instanceof RuntimeError
|
|
43
59
|
? { code: cause.code, message: cause.message, details: cause.details }
|
|
44
60
|
: { code: "UNEXPECTED_ERROR", message: cause instanceof Error ? cause.message : String(cause), details: {} };
|
|
45
61
|
}
|
|
46
|
-
function failure(command, cause) {
|
|
47
|
-
const error = errorDetails(cause);
|
|
62
|
+
function failure(command, cause, signal) {
|
|
63
|
+
const error = errorDetails(cause, signal);
|
|
48
64
|
return {
|
|
49
65
|
content: [{ type: "text", text: bounded(`${command} failed [${error.code}]: ${error.message}`) }],
|
|
50
66
|
details: { ok: false, command, error },
|
|
@@ -85,43 +101,399 @@ async function execute(runtime, command, args, signal, onUpdate, ctx, before) {
|
|
|
85
101
|
export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), freshness = runtime instanceof AstSgrepRuntime
|
|
86
102
|
? new FreshnessCoordinator({ refreshIntervalMs: runtime.config.refreshIntervalMs })
|
|
87
103
|
: new FreshnessCoordinator()) {
|
|
104
|
+
const pool = new NativeSessionPool();
|
|
105
|
+
let poolConfigured = false;
|
|
106
|
+
// Prefer a registration-local pool so tests / multi-agent hosts do not share
|
|
107
|
+
// sticky state. sharedNativePool remains for advanced single-session reuse.
|
|
108
|
+
const ensurePool = () => {
|
|
109
|
+
if (poolConfigured)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
const env = runtime.nativeEnv?.() ?? { NO_COLOR: "1" };
|
|
113
|
+
let binary;
|
|
114
|
+
try {
|
|
115
|
+
binary = runtime.resolveBinaryPath?.({ env });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
binary = undefined;
|
|
119
|
+
}
|
|
120
|
+
const opts = { env };
|
|
121
|
+
if (binary)
|
|
122
|
+
opts.binary = binary;
|
|
123
|
+
if (runtime.config?.timeoutMs !== undefined)
|
|
124
|
+
opts.timeoutMs = runtime.config.timeoutMs;
|
|
125
|
+
if (runtime.config?.maxOutputBytes !== undefined)
|
|
126
|
+
opts.maxOutputBytes = runtime.config.maxOutputBytes;
|
|
127
|
+
if (typeof env.ASGREP_NO_EMBED === "string") {
|
|
128
|
+
opts.useEmbed = env.ASGREP_NO_EMBED !== "1" && env.ASGREP_NO_EMBED !== "true";
|
|
129
|
+
}
|
|
130
|
+
if (typeof env.ASGREP_INDEX_PATH === "string")
|
|
131
|
+
opts.indexPath = env.ASGREP_INDEX_PATH;
|
|
132
|
+
pool.configure(opts);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
pool.configure({});
|
|
136
|
+
}
|
|
137
|
+
poolConfigured = true;
|
|
138
|
+
};
|
|
139
|
+
const resolveRoot = async (cwd) => runtime.resolveRoot ? await runtime.resolveRoot({ cwd }) : cwd;
|
|
140
|
+
const probeCli = (options = {}) => {
|
|
141
|
+
// Test fixtures inject `run` without a resolver; production always has resolveBinaryPath.
|
|
142
|
+
if (typeof runtime.resolveBinaryPath !== "function")
|
|
143
|
+
return { kind: "cli" };
|
|
144
|
+
try {
|
|
145
|
+
const base = runtime.nativeEnv?.() ?? {};
|
|
146
|
+
if (options.env) {
|
|
147
|
+
runtime.resolveBinaryPath({ env: { ...base, ...options.env } });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
runtime.resolveBinaryPath({ env: base });
|
|
151
|
+
}
|
|
152
|
+
return { kind: "cli" };
|
|
153
|
+
}
|
|
154
|
+
catch (cause) {
|
|
155
|
+
return {
|
|
156
|
+
kind: "unavailable",
|
|
157
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const requireBackend = (availability, context) => {
|
|
162
|
+
if (availability.kind !== "unavailable")
|
|
163
|
+
return;
|
|
164
|
+
throw new RuntimeError("BACKEND_UNAVAILABLE", "ast-sgrep backend unavailable (no NAPI session and no CLI binary)", {
|
|
165
|
+
backend: "unavailable",
|
|
166
|
+
// Agent-facing mirrors of the closed unavailable variant (not an open product).
|
|
167
|
+
napi: false,
|
|
168
|
+
cli: false,
|
|
169
|
+
cwd: context.cwd,
|
|
170
|
+
hint: "Install @ast-sgrep/<platform> or run npm run build:native in packages/pi/extension",
|
|
171
|
+
...(availability.cause ? { cause: availability.cause } : {}),
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
const runCli = async (args, context, options = {}) => {
|
|
175
|
+
requireBackend(probeCli(options), context);
|
|
176
|
+
return runtime.run(args, context, options);
|
|
177
|
+
};
|
|
178
|
+
const nativeCall = async (tool, args, context, options = {}) => {
|
|
179
|
+
ensurePool();
|
|
180
|
+
const root = await resolveRoot(context.cwd);
|
|
181
|
+
const worker = await pool.acquire(root);
|
|
182
|
+
if (worker) {
|
|
183
|
+
return asEnvelope(await worker.call(tool, args, options.signal ? { signal: options.signal } : {}));
|
|
184
|
+
}
|
|
185
|
+
// Cold CLI only when a real binary resolves -- never remap missing natives to BINARY_RESOLUTION_FAILED.
|
|
186
|
+
return runCli(argvFor(tool, args), context, options);
|
|
187
|
+
};
|
|
188
|
+
// Freshness + tools share the same warm in-process Searcher as Code Mode.
|
|
189
|
+
const warmRuntime = {
|
|
190
|
+
run: (args, context, options) => runtime.run(args, context, options),
|
|
191
|
+
resolveRoot: (context) => resolveRoot(context.cwd),
|
|
192
|
+
nativeCall,
|
|
193
|
+
};
|
|
194
|
+
if (runtime.watchExternalChanges !== undefined) {
|
|
195
|
+
warmRuntime.watchExternalChanges = runtime.watchExternalChanges;
|
|
196
|
+
}
|
|
197
|
+
if (runtime.resolveIndexPath) {
|
|
198
|
+
warmRuntime.resolveIndexPath = (root) => runtime.resolveIndexPath(root);
|
|
199
|
+
}
|
|
200
|
+
if (runtime.inspectIndexCompatibility) {
|
|
201
|
+
warmRuntime.inspectIndexCompatibility = (context) => runtime.inspectIndexCompatibility(context);
|
|
202
|
+
}
|
|
203
|
+
if (runtime.rebuildIncompatibleIndex) {
|
|
204
|
+
warmRuntime.rebuildIncompatibleIndex = async (context, options) => {
|
|
205
|
+
const root = await resolveRoot(context.cwd);
|
|
206
|
+
await pool.invalidate(root);
|
|
207
|
+
return runtime.rebuildIncompatibleIndex(context, options);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
88
210
|
pi.on("tool_result", (event, ctx) => {
|
|
89
|
-
if (event.isError
|
|
211
|
+
if (event.isError)
|
|
212
|
+
return;
|
|
213
|
+
if (event.toolName !== "write" && event.toolName !== "edit")
|
|
90
214
|
return;
|
|
91
215
|
const path = event.input.path;
|
|
92
216
|
if (typeof path === "string")
|
|
93
217
|
freshness.markAffectedPath(path, ctx.cwd);
|
|
94
218
|
});
|
|
219
|
+
pi.on("session_start", (_event, ctx) => {
|
|
220
|
+
// Warm the in-process Searcher at session start so the first asgrep
|
|
221
|
+
// search does not pay NAPI/SQLite open on the user's first lookup.
|
|
222
|
+
void (async () => {
|
|
223
|
+
try {
|
|
224
|
+
ensurePool();
|
|
225
|
+
const root = await resolveRoot(ctx.cwd);
|
|
226
|
+
await pool.acquire(root);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Doctor reports backend errors; a failed warmup must not block the session.
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
});
|
|
233
|
+
pi.on("session_shutdown", () => {
|
|
234
|
+
freshness.shutdown?.();
|
|
235
|
+
void pool.shutdown();
|
|
236
|
+
});
|
|
237
|
+
// Primary surface: Code Mode -- in-process NAPI (MCP-class), compose in JS.
|
|
238
|
+
// Sibling to MCP: pick one surface; both link core, never each other.
|
|
239
|
+
pi.registerTool({
|
|
240
|
+
name: "asgrep",
|
|
241
|
+
label: "asgrep",
|
|
242
|
+
promptSnippet: ASGREP_PROMPT_SNIPPET,
|
|
243
|
+
promptGuidelines: [...ASGREP_PROMPT_GUIDELINES],
|
|
244
|
+
description: [
|
|
245
|
+
"Primary code-search tool for this project. Call it whenever you need to find, trace, or understand code — do not wait for the user to mention asgrep.",
|
|
246
|
+
"Write JavaScript that calls typed asgrep.* methods. Compose with await / Promise.all, filter in code, return only the shaped final value.",
|
|
247
|
+
"Runs in-process (native addon) with a warm Searcher for the Pi session.",
|
|
248
|
+
"",
|
|
249
|
+
CODEMODE_TYPES_FOR_MODEL,
|
|
250
|
+
"",
|
|
251
|
+
"Example:",
|
|
252
|
+
"async () => {",
|
|
253
|
+
" const [seed, status] = await Promise.all([",
|
|
254
|
+
" asgrep.search({ query: 'auth refresh', limit: 5 }),",
|
|
255
|
+
" asgrep.indexStatus(),",
|
|
256
|
+
" ]);",
|
|
257
|
+
" const symbol = seed.hits?.[0]?.symbol;",
|
|
258
|
+
" if (!symbol) return { seed, status };",
|
|
259
|
+
" const graph = await asgrep.chain({ query: symbol, limit: 20 });",
|
|
260
|
+
" return { symbol, nodes: graph.nodes?.slice?.(0, 10) ?? graph, status };",
|
|
261
|
+
"}",
|
|
262
|
+
].join("\n"),
|
|
263
|
+
parameters: codemodeParameters,
|
|
264
|
+
renderCall(args, theme, context) {
|
|
265
|
+
return presentText(formatCodemodeCall(args.code, theme), context.lastComponent);
|
|
266
|
+
},
|
|
267
|
+
renderResult(result, options, theme, context) {
|
|
268
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
269
|
+
const lines = text.split("\n");
|
|
270
|
+
const max = options.expanded ? lines.length : 16;
|
|
271
|
+
const shown = lines.slice(0, max).join("\n");
|
|
272
|
+
const rest = lines.length > max ? `\n… ${lines.length - max} more` : "";
|
|
273
|
+
return presentText(shown + rest, context.lastComponent);
|
|
274
|
+
},
|
|
275
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
276
|
+
report(onUpdate, "codemode", "started");
|
|
277
|
+
try {
|
|
278
|
+
const timeoutMs = runtime.config?.timeoutMs ?? 30_000;
|
|
279
|
+
const deadline = Date.now() + timeoutMs;
|
|
280
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
281
|
+
const operationSignal = signal
|
|
282
|
+
? AbortSignal.any([signal, timeoutSignal])
|
|
283
|
+
: timeoutSignal;
|
|
284
|
+
const options = { signal: operationSignal };
|
|
285
|
+
ensurePool();
|
|
286
|
+
const root = await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
|
|
287
|
+
const env = runtime.nativeEnv?.() ?? { NO_COLOR: "1" };
|
|
288
|
+
let binary = null;
|
|
289
|
+
try {
|
|
290
|
+
binary = runtime.resolveBinaryPath?.({ env }) ?? null;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
binary = null;
|
|
294
|
+
}
|
|
295
|
+
// In-process NAPI first; CLI sticky only if addon missing.
|
|
296
|
+
const sticky = await pool.acquire(root);
|
|
297
|
+
const batchHost = {
|
|
298
|
+
run: (args, context, runOptions) => runtime.run(args, context, runOptions ?? {}),
|
|
299
|
+
sticky,
|
|
300
|
+
};
|
|
301
|
+
if (binary) {
|
|
302
|
+
batchHost.runBatch = (calls, context, runOptions) => runNativeBatch((a, c, o) => runtime.run(a, c, o ?? {}), calls, context, runOptions, (body, c, o) => {
|
|
303
|
+
const stdinOpts = {
|
|
304
|
+
binary: binary,
|
|
305
|
+
cwd: c.cwd,
|
|
306
|
+
body,
|
|
307
|
+
env,
|
|
308
|
+
};
|
|
309
|
+
if (o?.signal)
|
|
310
|
+
stdinOpts.signal = o.signal;
|
|
311
|
+
if (runtime.config?.timeoutMs !== undefined)
|
|
312
|
+
stdinOpts.timeoutMs = runtime.config.timeoutMs;
|
|
313
|
+
if (runtime.config?.maxOutputBytes !== undefined)
|
|
314
|
+
stdinOpts.maxOutputBytes = runtime.config.maxOutputBytes;
|
|
315
|
+
return runBatchViaStdin(stdinOpts);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const bundle = createAsgrepConnector(batchHost, { cwd: ctx.cwd }, options);
|
|
319
|
+
bundle.resetStats();
|
|
320
|
+
const codemodeOptions = { stats: bundle.stats };
|
|
321
|
+
codemodeOptions.timeoutMs = Math.max(1, deadline - Date.now());
|
|
322
|
+
codemodeOptions.signal = operationSignal;
|
|
323
|
+
const outcome = await runCodemode(params.code, bundle.asgrep, codemodeOptions);
|
|
324
|
+
report(onUpdate, "codemode", "completed");
|
|
325
|
+
if (!outcome.ok) {
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: "text", text: bounded(`codemode failed: ${outcome.error}`) }],
|
|
328
|
+
details: {
|
|
329
|
+
ok: false,
|
|
330
|
+
command: "codemode",
|
|
331
|
+
error: { code: "CODEMODE_ERROR", message: outcome.error, details: { logs: outcome.logs, stats: outcome.stats } },
|
|
332
|
+
code: outcome.code,
|
|
333
|
+
stats: outcome.stats,
|
|
334
|
+
wallMs: outcome.wallMs,
|
|
335
|
+
backend: pool.backend(),
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const rendered = formatCodemodeResult(outcome.result, {
|
|
340
|
+
...(outcome.stats ? { stats: outcome.stats } : {}),
|
|
341
|
+
wallMs: outcome.wallMs,
|
|
342
|
+
backend: pool.backend(),
|
|
343
|
+
});
|
|
344
|
+
const activationMs = outcome.wallMs;
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text", text: bounded(rendered) }],
|
|
347
|
+
details: {
|
|
348
|
+
ok: true,
|
|
349
|
+
command: "codemode",
|
|
350
|
+
result: outcome.result,
|
|
351
|
+
logs: outcome.logs,
|
|
352
|
+
rendered,
|
|
353
|
+
stats: outcome.stats,
|
|
354
|
+
wallMs: outcome.wallMs,
|
|
355
|
+
activationMs,
|
|
356
|
+
backend: pool.backend(),
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
catch (cause) {
|
|
361
|
+
return failure("codemode", cause);
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
// Escape hatches: one-shot tools for simple lookups. Prefer asgrep.
|
|
366
|
+
// They ride the same session sticky pool when available (no cold spawn).
|
|
95
367
|
pi.registerTool({
|
|
96
368
|
name: "asgrep_search",
|
|
97
|
-
label: "
|
|
98
|
-
|
|
369
|
+
label: "asgrep search",
|
|
370
|
+
promptSnippet: "One-shot asgrep search (natural, defs, callers, pattern, chain, semantic)",
|
|
371
|
+
description: "One-shot search. Prefer asgrep for anything multi-step, parallel, or filtered. Call this on your own whenever a single lookup is enough.",
|
|
99
372
|
parameters: searchParameters,
|
|
373
|
+
renderCall(args, theme, context) {
|
|
374
|
+
return presentText(formatSearchCall(args, theme), context.lastComponent);
|
|
375
|
+
},
|
|
376
|
+
renderResult(result, _options, _theme, context) {
|
|
377
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
378
|
+
return presentText(text, context.lastComponent);
|
|
379
|
+
},
|
|
100
380
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
101
381
|
const options = signal ? { signal } : {};
|
|
102
|
-
|
|
382
|
+
const started = performance.now();
|
|
383
|
+
report(onUpdate, "search", "started");
|
|
384
|
+
try {
|
|
385
|
+
ensurePool();
|
|
386
|
+
const root = await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
|
|
387
|
+
const sticky = await pool.acquire(root);
|
|
388
|
+
const response = sticky
|
|
389
|
+
? await sticky.call(...searchToolCall(params), options)
|
|
390
|
+
: await runCli(searchArgs(params), { cwd: ctx.cwd }, options);
|
|
391
|
+
report(onUpdate, "search", "completed");
|
|
392
|
+
return success("search", response, {
|
|
393
|
+
query: params.query,
|
|
394
|
+
mode: params.mode ?? "natural",
|
|
395
|
+
activationMs: performance.now() - started,
|
|
396
|
+
backend: pool.backend(),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (cause) {
|
|
400
|
+
return failure("search", cause);
|
|
401
|
+
}
|
|
103
402
|
},
|
|
104
403
|
});
|
|
105
404
|
pi.registerTool({
|
|
106
405
|
name: "asgrep_index",
|
|
107
|
-
label: "
|
|
108
|
-
|
|
406
|
+
label: "asgrep index",
|
|
407
|
+
promptSnippet: "Build or rebuild the asgrep index",
|
|
408
|
+
description: "Build or rebuild the index. Prefer asgrep.indexRepo inside asgrep.",
|
|
109
409
|
parameters: indexParameters,
|
|
410
|
+
renderCall(args, theme, context) {
|
|
411
|
+
return presentText(formatIndexCall(args.force === true, theme), context.lastComponent);
|
|
412
|
+
},
|
|
413
|
+
renderResult(result, _options, _theme, context) {
|
|
414
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
415
|
+
return presentText(text, context.lastComponent);
|
|
416
|
+
},
|
|
110
417
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
111
|
-
const
|
|
112
|
-
|
|
418
|
+
const force = params.force === true;
|
|
419
|
+
const command = force ? "reindex" : "index";
|
|
420
|
+
report(onUpdate, command, "started");
|
|
421
|
+
try {
|
|
422
|
+
ensurePool();
|
|
423
|
+
const root = await resolveRoot(ctx.cwd);
|
|
424
|
+
const sticky = await pool.acquire(root);
|
|
425
|
+
const response = sticky
|
|
426
|
+
? await sticky.call("index_repo", { force }, signal ? { signal } : {})
|
|
427
|
+
: await runCli([command, ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
|
|
428
|
+
report(onUpdate, command, "completed");
|
|
429
|
+
return success(command, response);
|
|
430
|
+
}
|
|
431
|
+
catch (cause) {
|
|
432
|
+
return failure(command, cause, signal);
|
|
433
|
+
}
|
|
113
434
|
},
|
|
114
435
|
});
|
|
115
436
|
pi.registerTool({
|
|
116
437
|
name: "asgrep_status",
|
|
117
|
-
label: "
|
|
118
|
-
|
|
438
|
+
label: "asgrep status",
|
|
439
|
+
promptSnippet: "asgrep index and backend status",
|
|
440
|
+
description: "Index/runtime status. Prefer asgrep.indexStatus inside asgrep.",
|
|
119
441
|
parameters: statusParameters,
|
|
442
|
+
renderCall(_args, theme, context) {
|
|
443
|
+
return presentText(formatStatusCall(theme), context.lastComponent);
|
|
444
|
+
},
|
|
445
|
+
renderResult(result, _options, _theme, context) {
|
|
446
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
447
|
+
return presentText(text, context.lastComponent);
|
|
448
|
+
},
|
|
120
449
|
async execute(_toolCallId, _params, signal, onUpdate, ctx) {
|
|
121
|
-
|
|
450
|
+
report(onUpdate, "status", "started");
|
|
451
|
+
try {
|
|
452
|
+
ensurePool();
|
|
453
|
+
const root = await resolveRoot(ctx.cwd);
|
|
454
|
+
const sticky = await pool.acquire(root);
|
|
455
|
+
const response = sticky
|
|
456
|
+
? await sticky.call("index_status", {}, signal ? { signal } : {})
|
|
457
|
+
: await runCli(["status", ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
|
|
458
|
+
report(onUpdate, "status", "completed");
|
|
459
|
+
return success("status", response);
|
|
460
|
+
}
|
|
461
|
+
catch (cause) {
|
|
462
|
+
return failure("status", cause);
|
|
463
|
+
}
|
|
122
464
|
},
|
|
123
465
|
});
|
|
124
466
|
}
|
|
467
|
+
const SEARCH_CALL_SPEC = {
|
|
468
|
+
semantic: { tool: "semantic" },
|
|
469
|
+
chain: { tool: "chain" },
|
|
470
|
+
defs: { tool: "defs", key: "symbol" },
|
|
471
|
+
callers: { tool: "callers", key: "symbol" },
|
|
472
|
+
imports: { tool: "imports", key: "module" },
|
|
473
|
+
pattern: { tool: "search", prefix: "pattern" },
|
|
474
|
+
word: { tool: "search", prefix: "word" },
|
|
475
|
+
literal: { tool: "search", prefix: "literal" },
|
|
476
|
+
regex: { tool: "search", prefix: "regex" },
|
|
477
|
+
natural: { tool: "search" },
|
|
478
|
+
};
|
|
479
|
+
function searchToolCall(params) {
|
|
480
|
+
const mode = params.mode ?? "natural";
|
|
481
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
482
|
+
const excerpt_lines = params.excerptLines ?? 0;
|
|
483
|
+
const spec = SEARCH_CALL_SPEC[mode];
|
|
484
|
+
if (spec.tool === "semantic") {
|
|
485
|
+
return ["semantic", { query: params.query, limit, excerpt_lines, format: "capsule" }];
|
|
486
|
+
}
|
|
487
|
+
if (spec.tool === "chain") {
|
|
488
|
+
return ["chain", { query: params.query, limit, top_n: 20 }];
|
|
489
|
+
}
|
|
490
|
+
if (spec.tool === "search") {
|
|
491
|
+
const query = spec.prefix ? `${spec.prefix}: ${params.query}` : params.query;
|
|
492
|
+
return ["search", { query, limit, excerpt_lines, format: "capsule" }];
|
|
493
|
+
}
|
|
494
|
+
// defs / callers / imports
|
|
495
|
+
return [spec.tool, { [spec.key]: params.query, limit, excerpt_lines }];
|
|
496
|
+
}
|
|
125
497
|
const COMMANDS = [
|
|
126
498
|
["asgrep-doctor", "Check the ast-sgrep runtime, native binary, index, and project configuration", "doctor"],
|
|
127
499
|
["asgrep-status", "Show ast-sgrep runtime, index, backend, and capability status", "status"],
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Neat asgrep tool chrome for the Pi TUI and the model-visible content. */
|
|
2
|
+
export type PresentTheme = {
|
|
3
|
+
bold(text: string): string;
|
|
4
|
+
fg(role: string, text: string): string;
|
|
5
|
+
};
|
|
6
|
+
export type HitLike = {
|
|
7
|
+
file?: unknown;
|
|
8
|
+
path?: unknown;
|
|
9
|
+
ref?: unknown;
|
|
10
|
+
symbol?: unknown;
|
|
11
|
+
kind?: unknown;
|
|
12
|
+
preview?: unknown;
|
|
13
|
+
start_line?: unknown;
|
|
14
|
+
line?: unknown;
|
|
15
|
+
lines?: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type EnvelopeLike = {
|
|
18
|
+
ok?: unknown;
|
|
19
|
+
hits?: unknown;
|
|
20
|
+
count?: unknown;
|
|
21
|
+
total?: unknown;
|
|
22
|
+
status?: unknown;
|
|
23
|
+
index_status?: unknown;
|
|
24
|
+
counts?: unknown;
|
|
25
|
+
backend?: unknown;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
export declare const ASGREP_PROMPT_SNIPPET = "Search this repo by intent, symbol, callers, defs, pattern, or chain (in-process asgrep; use without being asked)";
|
|
29
|
+
export declare const ASGREP_PROMPT_GUIDELINES: readonly ["For any code lookup (find a function, callers, defs, intent, structural pattern, or imports), call asgrep or asgrep_search immediately. Do not wait for the user to mention ast-sgrep.", "Prefer the asgrep Code Mode tool when you need more than one lookup, filtering, or parallel work. Write JavaScript that calls asgrep.search / semantic / chain / defs / callers / imports / indexStatus / indexRepo and return a small shaped value.", "Use grep only for exact log strings, filenames, or config keys. Use Pi write/edit to change files; asgrep does not mutate source."];
|
|
30
|
+
export declare function formatSearchCall(params: {
|
|
31
|
+
query?: string;
|
|
32
|
+
mode?: string;
|
|
33
|
+
limit?: number;
|
|
34
|
+
excerptLines?: number;
|
|
35
|
+
}, theme?: PresentTheme): string;
|
|
36
|
+
export declare function formatIndexCall(force: boolean, theme?: PresentTheme): string;
|
|
37
|
+
export declare function formatStatusCall(theme?: PresentTheme): string;
|
|
38
|
+
export declare function formatCodemodeCall(code: string, theme?: PresentTheme): string;
|
|
39
|
+
export declare function formatSearchResult(response: EnvelopeLike, meta: {
|
|
40
|
+
command: string;
|
|
41
|
+
query?: string;
|
|
42
|
+
mode?: string;
|
|
43
|
+
activationMs?: number;
|
|
44
|
+
backend?: string;
|
|
45
|
+
}, theme?: PresentTheme): string;
|
|
46
|
+
export declare function formatStatusResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
47
|
+
export declare function formatIndexResult(command: string, response: EnvelopeLike, theme?: PresentTheme): string;
|
|
48
|
+
export declare function formatCodemodeResult(value: unknown, meta?: {
|
|
49
|
+
stats?: {
|
|
50
|
+
calls: number;
|
|
51
|
+
batchedCalls: number;
|
|
52
|
+
parallelSpawnCalls: number;
|
|
53
|
+
stickyCalls?: number;
|
|
54
|
+
waves: number;
|
|
55
|
+
};
|
|
56
|
+
wallMs?: number;
|
|
57
|
+
backend?: string;
|
|
58
|
+
}, theme?: PresentTheme): string;
|
|
59
|
+
/** Minimal pi-tui Text stand-in so we do not take a TUI package dependency. */
|
|
60
|
+
export declare class AsgrepText {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(text?: string);
|
|
63
|
+
setText(text: string): void;
|
|
64
|
+
invalidate(): void;
|
|
65
|
+
render(_width: number): string[];
|
|
66
|
+
}
|
|
67
|
+
export declare function presentText(formatted: string, last: unknown): AsgrepText;
|