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.
Files changed (53) hide show
  1. package/README.md +16 -19
  2. package/dist/code-mode.d.ts +1 -1
  3. package/dist/code-mode.js +1 -1
  4. package/dist/codemode/connector.d.ts +18 -3
  5. package/dist/codemode/connector.js +85 -31
  6. package/dist/codemode/dispatch.d.ts +13 -1
  7. package/dist/codemode/dispatch.js +87 -24
  8. package/dist/codemode/guest-api.d.ts +16 -0
  9. package/dist/codemode/guest-api.js +194 -0
  10. package/dist/codemode/guest-worker.mjs +287 -0
  11. package/dist/codemode/index.d.ts +4 -3
  12. package/dist/codemode/index.js +4 -3
  13. package/dist/codemode/native.d.ts +1 -1
  14. package/dist/codemode/native.js +1 -1
  15. package/dist/codemode/runner.d.ts +13 -9
  16. package/dist/codemode/runner.js +411 -213
  17. package/dist/codemode/session-pool.d.ts +6 -1
  18. package/dist/codemode/session-pool.js +125 -32
  19. package/dist/codemode/types.d.ts +42 -2
  20. package/dist/codemode/types.js +40 -15
  21. package/dist/codemode/worker.d.ts +1 -1
  22. package/dist/codemode/worker.js +25 -2
  23. package/dist/host/commands.d.ts +6 -0
  24. package/dist/host/commands.js +49 -0
  25. package/dist/host/results.d.ts +123 -0
  26. package/dist/host/results.js +126 -0
  27. package/dist/host/tools.d.ts +28 -0
  28. package/dist/host/tools.js +802 -0
  29. package/dist/index.d.ts +7 -34
  30. package/dist/index.js +5 -543
  31. package/dist/runtime/config.d.ts +36 -0
  32. package/dist/runtime/config.js +98 -0
  33. package/dist/runtime/freshness.d.ts +43 -0
  34. package/dist/runtime/freshness.js +446 -0
  35. package/dist/runtime/index-health.d.ts +16 -0
  36. package/dist/runtime/index-health.js +111 -0
  37. package/dist/runtime/runtime.d.ts +48 -0
  38. package/dist/runtime/runtime.js +265 -0
  39. package/dist/runtime/sqlite.d.ts +15 -0
  40. package/dist/runtime/sqlite.js +63 -0
  41. package/dist/runtime/types.d.ts +55 -0
  42. package/dist/runtime/types.js +25 -0
  43. package/dist/ui/card.d.ts +66 -0
  44. package/dist/ui/card.js +375 -0
  45. package/dist/ui/present.d.ts +89 -0
  46. package/dist/ui/present.js +391 -0
  47. package/package.json +8 -7
  48. package/dist/codemode/sandbox-worker.d.ts +0 -1
  49. package/dist/codemode/sandbox-worker.js +0 -204
  50. package/dist/present.d.ts +0 -70
  51. package/dist/present.js +0 -260
  52. package/dist/runtime.d.ts +0 -137
  53. package/dist/runtime.js +0 -799
package/dist/index.d.ts CHANGED
@@ -1,36 +1,9 @@
1
+ /**
2
+ * pi-ast-sgrep extension entry: constructs the runtime + freshness
3
+ * coordinator and registers tools + commands. Implementation lives in
4
+ * tools.ts / commands.ts; result plumbing in envelope.ts.
5
+ */
1
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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
- };
33
- export declare function registerAstSgrepTools(pi: ExtensionAPI, runtime?: RuntimeLike, freshness?: FreshnessLike): void;
34
- export declare function registerAstSgrepCommands(pi: ExtensionAPI, runtime?: RuntimeLike): void;
7
+ export { registerAstSgrepTools } from "./host/tools.js";
8
+ export { registerAstSgrepCommands } from "./host/commands.js";
35
9
  export default function astSgrepExtension(pi: ExtensionAPI): void;
36
- export {};
package/dist/index.js CHANGED
@@ -1,546 +1,8 @@
1
- import { Type } from "typebox";
2
- import { createAsgrepConnector, runCodemode, runNativeBatch, runBatchViaStdin, CODEMODE_TYPES_FOR_MODEL, NativeSessionPool, argvFor, asEnvelope, } from "./codemode/index.js";
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";
5
- const DEFAULT_LIMIT = 8;
6
- const MAX_LIMIT = 100;
7
- const MAX_EXCERPT_LINES = 100;
8
- const MAX_CONTENT_CHARS = 1_200;
9
- const searchParameters = Type.Object({
10
- query: Type.String({ minLength: 1, maxLength: 4_096, description: "Natural-language query, symbol, or structural pattern" }),
11
- mode: Type.Optional(Type.Union([
12
- Type.Literal("natural"),
13
- Type.Literal("pattern"),
14
- Type.Literal("defs"),
15
- Type.Literal("callers"),
16
- Type.Literal("chain"),
17
- Type.Literal("semantic"),
18
- Type.Literal("word"),
19
- Type.Literal("literal"),
20
- Type.Literal("regex"),
21
- Type.Literal("imports"),
22
- ], { default: "natural", description: "Search strategy (CLI-aligned modes)" })),
23
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_LIMIT, default: DEFAULT_LIMIT })),
24
- excerptLines: Type.Optional(Type.Integer({ minimum: 0, maximum: MAX_EXCERPT_LINES, default: 0, description: "Opt in to excerpt body lines" })),
25
- }, { additionalProperties: false });
26
- const indexParameters = Type.Object({
27
- force: Type.Optional(Type.Boolean({ default: false, description: "Rebuild the index from scratch" })),
28
- }, { additionalProperties: false });
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 });
37
- function bounded(text) {
38
- return text.length <= MAX_CONTENT_CHARS ? text : `${text.slice(0, MAX_CONTENT_CHARS - 1)}…`;
39
- }
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 });
46
- return {
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 },
52
- };
53
- }
54
- function errorDetails(cause, signal) {
55
- if (signal?.aborted) {
56
- return { code: "CANCELLED", message: "cancelled", details: {} };
57
- }
58
- return cause instanceof RuntimeError
59
- ? { code: cause.code, message: cause.message, details: cause.details }
60
- : { code: "UNEXPECTED_ERROR", message: cause instanceof Error ? cause.message : String(cause), details: {} };
61
- }
62
- function failure(command, cause, signal) {
63
- const error = errorDetails(cause, signal);
64
- return {
65
- content: [{ type: "text", text: bounded(`${command} failed [${error.code}]: ${error.message}`) }],
66
- details: { ok: false, command, error },
67
- };
68
- }
69
- function report(onUpdate, command, phase) {
70
- onUpdate?.({
71
- content: [{ type: "text", text: `${command} ${phase}` }],
72
- details: { command, phase },
73
- });
74
- }
75
- function queryForMode(query, mode) {
76
- if (mode === "pattern" || mode === "defs" || mode === "callers" || mode === "word" || mode === "literal" || mode === "regex" || mode === "imports") {
77
- return `${mode}: ${query}`;
78
- }
79
- return query;
80
- }
81
- function searchArgs(params) {
82
- const mode = params.mode ?? "natural";
83
- const query = queryForMode(params.query, mode);
84
- const output = ["--json", "--format", "agent-capsule", "--limit", String(params.limit ?? DEFAULT_LIMIT), "--excerpt-lines", String(params.excerptLines ?? 0)];
85
- return mode === "chain" || mode === "semantic"
86
- ? [mode, query, ".", ...output]
87
- : [...output, query, "."];
88
- }
89
- async function execute(runtime, command, args, signal, onUpdate, ctx, before) {
90
- report(onUpdate, command, "started");
91
- try {
92
- await before?.();
93
- const response = await runtime.run(args, { cwd: ctx.cwd }, signal ? { signal } : {});
94
- report(onUpdate, command, "completed");
95
- return success(command, response);
96
- }
97
- catch (cause) {
98
- return failure(command, cause);
99
- }
100
- }
101
- export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), freshness = runtime instanceof AstSgrepRuntime
102
- ? new FreshnessCoordinator({ refreshIntervalMs: runtime.config.refreshIntervalMs })
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
- }
210
- pi.on("tool_result", (event, ctx) => {
211
- if (event.isError)
212
- return;
213
- if (event.toolName !== "write" && event.toolName !== "edit")
214
- return;
215
- const path = event.input.path;
216
- if (typeof path === "string")
217
- freshness.markAffectedPath(path, ctx.cwd);
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).
367
- pi.registerTool({
368
- name: "asgrep_search",
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.",
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
- },
380
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
381
- const options = signal ? { signal } : {};
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
- }
402
- },
403
- });
404
- pi.registerTool({
405
- name: "asgrep_index",
406
- label: "asgrep index",
407
- promptSnippet: "Build or rebuild the asgrep index",
408
- description: "Build or rebuild the index. Prefer asgrep.indexRepo inside asgrep.",
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
- },
417
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
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
- }
434
- },
435
- });
436
- pi.registerTool({
437
- name: "asgrep_status",
438
- label: "asgrep status",
439
- promptSnippet: "asgrep index and backend status",
440
- description: "Index/runtime status. Prefer asgrep.indexStatus inside asgrep.",
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
- },
449
- async execute(_toolCallId, _params, signal, onUpdate, ctx) {
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
- }
464
- },
465
- });
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
- }
497
- const COMMANDS = [
498
- ["asgrep-doctor", "Check the ast-sgrep runtime, native binary, index, and project configuration", "doctor"],
499
- ["asgrep-status", "Show ast-sgrep runtime, index, backend, and capability status", "status"],
500
- ["asgrep-index", "Build the ast-sgrep index for the current project", "index"],
501
- ["asgrep-reindex", "Rebuild the ast-sgrep index for the current project", "reindex"],
502
- ];
503
- async function runCommand(runtime, command, ctx, args) {
504
- if (args.trim() !== "") {
505
- return {
506
- ok: false,
507
- command,
508
- error: { code: "INVALID_ARGUMENTS", message: `/${command} does not accept arguments`, details: { args } },
509
- };
510
- }
511
- try {
512
- const response = await runtime.run([command.slice("asgrep-".length), ".", "--json"], { cwd: ctx.cwd });
513
- return { ok: true, command, response };
514
- }
515
- catch (cause) {
516
- return { ok: false, command, error: errorDetails(cause) };
517
- }
518
- }
519
- function compactCommandResult(result) {
520
- if (!result.ok)
521
- return `${result.command} failed [${result.error.code}]: ${result.error.message}`;
522
- const response = result.response;
523
- const counts = response.counts && typeof response.counts === "object"
524
- ? Object.entries(response.counts).map(([key, value]) => `${key}=${String(value)}`).join(" ")
525
- : "";
526
- const state = typeof response.status === "string" ? response.status
527
- : typeof response.index_status === "string" ? response.index_status
528
- : response.ok ? "healthy" : "failed";
529
- return bounded([`${result.command}: ${state}`, counts].filter(Boolean).join(" · "));
530
- }
531
- export function registerAstSgrepCommands(pi, runtime = new AstSgrepRuntime(pi)) {
532
- for (const [name, description] of COMMANDS) {
533
- pi.registerCommand(name, {
534
- description,
535
- async handler(args, context) {
536
- const ctx = context;
537
- const result = await runCommand(runtime, name, ctx, args);
538
- const output = ctx.hasUI ? compactCommandResult(result) : JSON.stringify(result);
539
- ctx.ui.notify(output, result.ok ? "info" : "error");
540
- },
541
- });
542
- }
543
- }
1
+ import { AstSgrepRuntime, FreshnessCoordinator } from "./runtime/runtime.js";
2
+ import { registerAstSgrepTools } from "./host/tools.js";
3
+ import { registerAstSgrepCommands } from "./host/commands.js";
4
+ export { registerAstSgrepTools } from "./host/tools.js";
5
+ export { registerAstSgrepCommands } from "./host/commands.js";
544
6
  export default function astSgrepExtension(pi) {
545
7
  const runtime = new AstSgrepRuntime(pi);
546
8
  const freshness = new FreshnessCoordinator({ refreshIntervalMs: runtime.config.refreshIntervalMs });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Runtime configuration: schema, legacy migration, env overlay, precedence.
3
+ * Leaf module (imports types only).
4
+ */
5
+ import { CONFIG_SCHEMA_VERSION } from "./types.js";
6
+ export interface RuntimeConfig {
7
+ schemaVersion?: typeof CONFIG_SCHEMA_VERSION;
8
+ binaryPath?: string;
9
+ root?: string;
10
+ allowOutsideProject?: boolean;
11
+ timeoutMs?: number;
12
+ maxOutputBytes?: number;
13
+ refreshIntervalMs?: number;
14
+ env?: Readonly<Record<string, string>>;
15
+ }
16
+ export interface LegacyRuntimeConfig extends Omit<RuntimeConfig, "schemaVersion" | "timeoutMs" | "maxOutputBytes" | "refreshIntervalMs"> {
17
+ schemaVersion?: 0;
18
+ timeout?: number;
19
+ maxOutput?: number;
20
+ refreshInterval?: number;
21
+ }
22
+ export type RuntimeConfigInput = RuntimeConfig | LegacyRuntimeConfig;
23
+ export interface ConfigSources {
24
+ explicitProjectConfig?: RuntimeConfigInput;
25
+ projectSettings?: RuntimeConfigInput;
26
+ globalSettings?: RuntimeConfigInput;
27
+ environment?: NodeJS.ProcessEnv;
28
+ defaults?: RuntimeConfigInput;
29
+ }
30
+ export declare function finitePositive(value: unknown, fallback: number, name: string): number;
31
+ /** Convert schema 0/unversioned settings without mutating the rollback source. */
32
+ export declare function migrateConfig(input?: RuntimeConfigInput): RuntimeConfig;
33
+ /** Serialize current settings for a schema-0 rollback without mutating the current value. */
34
+ export declare function rollbackConfig(input: RuntimeConfig): LegacyRuntimeConfig;
35
+ /** Merge each setting independently, from the documented lowest to highest priority. */
36
+ export declare function resolveConfig(sources?: ConfigSources): Required<Pick<RuntimeConfig, "timeoutMs" | "maxOutputBytes">> & RuntimeConfig;