pi-ast-sgrep 1.4.0 → 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/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Type } from "typebox";
2
2
  import { createAsgrepConnector, runCodemode, runNativeBatch, runBatchViaStdin, CODEMODE_TYPES_FOR_MODEL, NativeSessionPool, argvFor, asEnvelope, } from "./codemode/index.js";
3
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";
4
5
  const DEFAULT_LIMIT = 8;
5
6
  const MAX_LIMIT = 100;
6
7
  const MAX_EXCERPT_LINES = 100;
7
8
  const MAX_CONTENT_CHARS = 1_200;
8
- const MAX_CODEMODE_RESULT_CHARS = 8_000;
9
9
  const searchParameters = Type.Object({
10
10
  query: Type.String({ minLength: 1, maxLength: 4_096, description: "Natural-language query, symbol, or structural pattern" }),
11
11
  mode: Type.Optional(Type.Union([
@@ -37,17 +37,18 @@ const codemodeParameters = Type.Object({
37
37
  function bounded(text) {
38
38
  return text.length <= MAX_CONTENT_CHARS ? text : `${text.slice(0, MAX_CONTENT_CHARS - 1)}…`;
39
39
  }
40
- function success(command, response) {
41
- const count = Array.isArray(response.hits) ? response.hits.length :
42
- typeof response.count === "number" ? response.count :
43
- typeof response.total === "number" ? response.total : undefined;
44
- const summary = count === undefined ? `${command} completed` : `${command} completed: ${count} result${count === 1 ? "" : "s"}`;
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 });
45
46
  return {
46
- content: [{ type: "text", text: bounded(summary) }],
47
+ content: [{ type: "text", text: bounded(text) }],
47
48
  // The tool execute owns its machine command: normalize the envelope's
48
49
  // command (native catalog names like index_status/index_repo must surface
49
50
  // as the machine commands status/index/reindex).
50
- details: { ok: true, command, response: { ...response, command } },
51
+ details: { ok: true, command, response: { ...response, command }, ...extra },
51
52
  };
52
53
  }
53
54
  function errorDetails(cause, signal) {
@@ -101,9 +102,12 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
101
102
  ? new FreshnessCoordinator({ refreshIntervalMs: runtime.config.refreshIntervalMs })
102
103
  : new FreshnessCoordinator()) {
103
104
  const pool = new NativeSessionPool();
105
+ let poolConfigured = false;
104
106
  // Prefer a registration-local pool so tests / multi-agent hosts do not share
105
107
  // sticky state. sharedNativePool remains for advanced single-session reuse.
106
108
  const ensurePool = () => {
109
+ if (poolConfigured)
110
+ return;
107
111
  try {
108
112
  const env = runtime.nativeEnv?.() ?? { NO_COLOR: "1" };
109
113
  let binary;
@@ -118,6 +122,8 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
118
122
  opts.binary = binary;
119
123
  if (runtime.config?.timeoutMs !== undefined)
120
124
  opts.timeoutMs = runtime.config.timeoutMs;
125
+ if (runtime.config?.maxOutputBytes !== undefined)
126
+ opts.maxOutputBytes = runtime.config.maxOutputBytes;
121
127
  if (typeof env.ASGREP_NO_EMBED === "string") {
122
128
  opts.useEmbed = env.ASGREP_NO_EMBED !== "1" && env.ASGREP_NO_EMBED !== "true";
123
129
  }
@@ -128,8 +134,47 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
128
134
  catch {
129
135
  pool.configure({});
130
136
  }
137
+ poolConfigured = true;
131
138
  };
132
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
+ };
133
178
  const nativeCall = async (tool, args, context, options = {}) => {
134
179
  ensurePool();
135
180
  const root = await resolveRoot(context.cwd);
@@ -137,8 +182,8 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
137
182
  if (worker) {
138
183
  return asEnvelope(await worker.call(tool, args, options.signal ? { signal: options.signal } : {}));
139
184
  }
140
- // Last resort: cold CLI argv (tests without NAPI / missing addon).
141
- return runtime.run(argvFor(tool, args), context, options);
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);
142
187
  };
143
188
  // Freshness + tools share the same warm in-process Searcher as Code Mode.
144
189
  const warmRuntime = {
@@ -146,28 +191,60 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
146
191
  resolveRoot: (context) => resolveRoot(context.cwd),
147
192
  nativeCall,
148
193
  };
194
+ if (runtime.watchExternalChanges !== undefined) {
195
+ warmRuntime.watchExternalChanges = runtime.watchExternalChanges;
196
+ }
197
+ if (runtime.resolveIndexPath) {
198
+ warmRuntime.resolveIndexPath = (root) => runtime.resolveIndexPath(root);
199
+ }
149
200
  if (runtime.inspectIndexCompatibility) {
150
201
  warmRuntime.inspectIndexCompatibility = (context) => runtime.inspectIndexCompatibility(context);
151
202
  }
152
203
  if (runtime.rebuildIncompatibleIndex) {
153
- warmRuntime.rebuildIncompatibleIndex = (context, options) => runtime.rebuildIncompatibleIndex(context, options);
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
+ };
154
209
  }
155
210
  pi.on("tool_result", (event, ctx) => {
156
- if (event.isError || (event.toolName !== "write" && event.toolName !== "edit"))
211
+ if (event.isError)
212
+ return;
213
+ if (event.toolName !== "write" && event.toolName !== "edit")
157
214
  return;
158
215
  const path = event.input.path;
159
216
  if (typeof path === "string")
160
217
  freshness.markAffectedPath(path, ctx.cwd);
161
218
  });
162
- // Primary surface: Code Mode — in-process NAPI (MCP-class), compose in JS.
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.
163
238
  // Sibling to MCP: pick one surface; both link core, never each other.
164
239
  pi.registerTool({
165
- name: "asgrep_codemode",
166
- label: "ast-sgrep Code Mode",
240
+ name: "asgrep",
241
+ label: "asgrep",
242
+ promptSnippet: ASGREP_PROMPT_SNIPPET,
243
+ promptGuidelines: [...ASGREP_PROMPT_GUIDELINES],
167
244
  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.",
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.",
171
248
  "",
172
249
  CODEMODE_TYPES_FOR_MODEL,
173
250
  "",
@@ -184,13 +261,29 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
184
261
  "}",
185
262
  ].join("\n"),
186
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
+ },
187
275
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
188
276
  report(onUpdate, "codemode", "started");
189
277
  try {
190
- const options = signal ? { signal } : {};
191
- await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
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 };
192
285
  ensurePool();
193
- const root = await resolveRoot(ctx.cwd);
286
+ const root = await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
194
287
  const env = runtime.nativeEnv?.() ?? { NO_COLOR: "1" };
195
288
  let binary = null;
196
289
  try {
@@ -217,25 +310,25 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
217
310
  stdinOpts.signal = o.signal;
218
311
  if (runtime.config?.timeoutMs !== undefined)
219
312
  stdinOpts.timeoutMs = runtime.config.timeoutMs;
313
+ if (runtime.config?.maxOutputBytes !== undefined)
314
+ stdinOpts.maxOutputBytes = runtime.config.maxOutputBytes;
220
315
  return runBatchViaStdin(stdinOpts);
221
316
  });
222
317
  }
223
318
  const bundle = createAsgrepConnector(batchHost, { cwd: ctx.cwd }, options);
224
319
  bundle.resetStats();
225
320
  const codemodeOptions = { stats: bundle.stats };
226
- if (runtime.config?.timeoutMs !== undefined)
227
- codemodeOptions.timeoutMs = runtime.config.timeoutMs;
228
- if (signal)
229
- codemodeOptions.signal = signal;
321
+ codemodeOptions.timeoutMs = Math.max(1, deadline - Date.now());
322
+ codemodeOptions.signal = operationSignal;
230
323
  const outcome = await runCodemode(params.code, bundle.asgrep, codemodeOptions);
231
324
  report(onUpdate, "codemode", "completed");
232
325
  if (!outcome.ok) {
233
326
  return {
234
- content: [{ type: "text", text: bounded(`codemode failed: ${outcome.error ?? "unknown error"}`) }],
327
+ content: [{ type: "text", text: bounded(`codemode failed: ${outcome.error}`) }],
235
328
  details: {
236
329
  ok: false,
237
330
  command: "codemode",
238
- error: { code: "CODEMODE_ERROR", message: outcome.error ?? "unknown error", details: { logs: outcome.logs, stats: outcome.stats } },
331
+ error: { code: "CODEMODE_ERROR", message: outcome.error, details: { logs: outcome.logs, stats: outcome.stats } },
239
332
  code: outcome.code,
240
333
  stats: outcome.stats,
241
334
  wallMs: outcome.wallMs,
@@ -243,9 +336,14 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
243
336
  },
244
337
  };
245
338
  }
246
- const rendered = safeRender(outcome.result);
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;
247
345
  return {
248
- content: [{ type: "text", text: bounded(summarizeCodemode(outcome.result, outcome.stats, outcome.wallMs, pool.backend())) }],
346
+ content: [{ type: "text", text: bounded(rendered) }],
249
347
  details: {
250
348
  ok: true,
251
349
  command: "codemode",
@@ -254,6 +352,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
254
352
  rendered,
255
353
  stats: outcome.stats,
256
354
  wallMs: outcome.wallMs,
355
+ activationMs,
257
356
  backend: pool.backend(),
258
357
  },
259
358
  };
@@ -263,26 +362,39 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
263
362
  }
264
363
  },
265
364
  });
266
- // Escape hatches: one-shot tools for simple lookups. Prefer asgrep_codemode.
365
+ // Escape hatches: one-shot tools for simple lookups. Prefer asgrep.
267
366
  // They ride the same session sticky pool when available (no cold spawn).
268
367
  pi.registerTool({
269
368
  name: "asgrep_search",
270
- label: "ast-sgrep search",
271
- description: "One-shot search. Prefer asgrep_codemode for anything multi-step, parallel, or filtered.",
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.",
272
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
+ },
273
380
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
274
381
  const options = signal ? { signal } : {};
382
+ const started = performance.now();
275
383
  report(onUpdate, "search", "started");
276
384
  try {
277
- await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
278
385
  ensurePool();
279
- const root = await resolveRoot(ctx.cwd);
386
+ const root = await freshness.ensureFresh(warmRuntime, { cwd: ctx.cwd }, options);
280
387
  const sticky = await pool.acquire(root);
281
388
  const response = sticky
282
389
  ? await sticky.call(...searchToolCall(params), options)
283
- : await runtime.run(searchArgs(params), { cwd: ctx.cwd }, options);
390
+ : await runCli(searchArgs(params), { cwd: ctx.cwd }, options);
284
391
  report(onUpdate, "search", "completed");
285
- return success("search", response);
392
+ return success("search", response, {
393
+ query: params.query,
394
+ mode: params.mode ?? "natural",
395
+ activationMs: performance.now() - started,
396
+ backend: pool.backend(),
397
+ });
286
398
  }
287
399
  catch (cause) {
288
400
  return failure("search", cause);
@@ -291,9 +403,17 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
291
403
  });
292
404
  pi.registerTool({
293
405
  name: "asgrep_index",
294
- label: "ast-sgrep index",
295
- description: "Build or rebuild the index. Prefer asgrep.indexRepo inside asgrep_codemode.",
406
+ label: "asgrep index",
407
+ promptSnippet: "Build or rebuild the asgrep index",
408
+ description: "Build or rebuild the index. Prefer asgrep.indexRepo inside asgrep.",
296
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
+ },
297
417
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
298
418
  const force = params.force === true;
299
419
  const command = force ? "reindex" : "index";
@@ -304,7 +424,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
304
424
  const sticky = await pool.acquire(root);
305
425
  const response = sticky
306
426
  ? await sticky.call("index_repo", { force }, signal ? { signal } : {})
307
- : await runtime.run([command, ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
427
+ : await runCli([command, ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
308
428
  report(onUpdate, command, "completed");
309
429
  return success(command, response);
310
430
  }
@@ -315,9 +435,17 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
315
435
  });
316
436
  pi.registerTool({
317
437
  name: "asgrep_status",
318
- label: "ast-sgrep status",
319
- description: "Index/runtime status. Prefer asgrep.indexStatus inside asgrep_codemode.",
438
+ label: "asgrep status",
439
+ promptSnippet: "asgrep index and backend status",
440
+ description: "Index/runtime status. Prefer asgrep.indexStatus inside asgrep.",
320
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
+ },
321
449
  async execute(_toolCallId, _params, signal, onUpdate, ctx) {
322
450
  report(onUpdate, "status", "started");
323
451
  try {
@@ -326,7 +454,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
326
454
  const sticky = await pool.acquire(root);
327
455
  const response = sticky
328
456
  ? await sticky.call("index_status", {}, signal ? { signal } : {})
329
- : await runtime.run(["status", ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
457
+ : await runCli(["status", ".", "--json"], { cwd: ctx.cwd }, signal ? { signal } : {});
330
458
  report(onUpdate, "status", "completed");
331
459
  return success("status", response);
332
460
  }
@@ -336,77 +464,35 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
336
464
  },
337
465
  });
338
466
  }
339
- /** Map one-shot search params to typed sticky tool+args. */
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
+ };
340
479
  function searchToolCall(params) {
341
480
  const mode = params.mode ?? "natural";
342
481
  const limit = params.limit ?? DEFAULT_LIMIT;
343
482
  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
- }
483
+ const spec = SEARCH_CALL_SPEC[mode];
484
+ if (spec.tool === "semantic") {
485
+ return ["semantic", { query: params.query, limit, excerpt_lines, format: "capsule" }];
367
486
  }
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"}`;
487
+ if (spec.tool === "chain") {
488
+ return ["chain", { query: params.query, limit, top_n: 20 }];
390
489
  }
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`);
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" }];
406
493
  }
407
- if (wallMs !== undefined)
408
- parts.push(`${wallMs}ms`);
409
- return parts.join(" · ");
494
+ // defs / callers / imports
495
+ return [spec.tool, { [spec.key]: params.query, limit, excerpt_lines }];
410
496
  }
411
497
  const COMMANDS = [
412
498
  ["asgrep-doctor", "Check the ast-sgrep runtime, native binary, index, and project configuration", "doctor"],
@@ -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;