pi-ast-sgrep 2.2.1 → 2.5.3

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 CHANGED
@@ -6,7 +6,9 @@ Native Code Mode, structural, graph, and semantic code search for [Pi](https://g
6
6
 
7
7
  `pi-ast-sgrep` gives Pi a warm, project-aware search engine for understanding code. It finds behavior by intent, resolves definitions and callers, traces relationships, matches syntax-aware patterns, joins two indexed channels, and searches local semantic embeddings. The primary `asgrep` tool lets Pi compose several searches in one JavaScript program instead of spending one model round trip per lookup.
8
8
 
9
- **v2.0.0** · 13 languages · local-first semantic · critic + two-channel `AND` · **Code Mode** (on by default, no API key)
9
+ **v2.5.3** (native engine 2.5.2) · 15 languages · local-first semantic · critic + two-channel `AND` · **Code Mode** (on by default, no API key)
10
+
11
+ **2.5.3 recovery fix:** Missing `ASGREP_BIN`/`AST_SGREP_BINARY` developer builds now fall back to the verified bundled binary with a warning. Explicit `binaryPath` settings and invalid binaries remain strict. `asgrep_read` works without a search backend or index, `/asgrep-doctor` reports component versions, and the launcher dependency now requires at least 2.5.2. Update with `pi update npm:pi-ast-sgrep`, then reload Pi.
10
12
 
11
13
  **Upgrading to 2.0:** this is a breaking semver release because the cloud/Ollama embedding backends were removed. Update the Pi package normally; local hashed semantic search remains the default, optional neural embeddings remain in-process, and indexes that still store `embed_backend=cloud|ollama` fail closed until `/asgrep-reindex`. One-shot tools and Code Mode now put bounded hits in `content` so the model sees them, not only display-only `details`.
12
14
 
@@ -104,4 +104,5 @@ export declare function createAsgrepConnector(host: BatchCapableHost, context: {
104
104
  }, options?: {
105
105
  signal?: AbortSignal;
106
106
  scope?: string;
107
+ localReads?: boolean;
107
108
  }): ConnectorBundle;
@@ -80,6 +80,8 @@ export function createAsgrepConnector(host, context, options = {}) {
80
80
  };
81
81
  const call = async (tool, args, signal) => {
82
82
  const opts = callOptions(signal);
83
+ if (tool === "read" && options.localReads)
84
+ return fallbackCall(tool, args, opts.signal);
83
85
  if (NATIVE_FALLBACK_TOOLS.has(tool) && nativeMissing.has(tool))
84
86
  return fallbackCall(tool, args, opts.signal);
85
87
  try {
@@ -14,17 +14,25 @@ async function runCommand(runtime, command, ctx, args) {
14
14
  error: { code: "INVALID_ARGUMENTS", message: `/${command} does not accept arguments`, details: { args } },
15
15
  };
16
16
  }
17
+ let diagnostics;
17
18
  try {
19
+ if (command === "asgrep-doctor")
20
+ diagnostics = await runtime.diagnostics?.({ cwd: ctx.cwd });
18
21
  const response = await runtime.run([command.slice("asgrep-".length), ".", "--json"], { cwd: ctx.cwd });
19
- return { ok: true, command, response };
22
+ return { ok: true, command, response, ...(diagnostics ? { diagnostics } : {}) };
20
23
  }
21
24
  catch (cause) {
22
- return { ok: false, command, error: errorDetails(cause) };
25
+ return { ok: false, command, error: errorDetails(cause), ...(diagnostics ? { diagnostics } : {}) };
23
26
  }
24
27
  }
25
28
  function compactCommandResult(result) {
29
+ const diagnostics = result.diagnostics;
30
+ const versions = diagnostics
31
+ ? `extension=${diagnostics.extension ?? "unknown"} launcher=${diagnostics.launcher ?? "unknown"} native=${diagnostics.native ?? "unavailable"}`
32
+ : "";
33
+ const warning = typeof diagnostics?.warning === "string" ? diagnostics.warning : "";
26
34
  if (!result.ok)
27
- return `${result.command} failed [${result.error.code}]: ${result.error.message}`;
35
+ return bounded([`${result.command} failed [${result.error.code}]: ${result.error.message}`, versions, warning].filter(Boolean).join(" · "));
28
36
  const response = result.response;
29
37
  const counts = response.counts && typeof response.counts === "object"
30
38
  ? Object.entries(response.counts).map(([key, value]) => `${key}=${String(value)}`).join(" ")
@@ -32,7 +40,7 @@ function compactCommandResult(result) {
32
40
  const state = typeof response.status === "string" ? response.status
33
41
  : typeof response.index_status === "string" ? response.index_status
34
42
  : response.ok ? "healthy" : "failed";
35
- return bounded([`${result.command}: ${state}`, counts].filter(Boolean).join(" · "));
43
+ return bounded([`${result.command}: ${state}`, counts, versions, warning].filter(Boolean).join(" · "));
36
44
  }
37
45
  export function registerAstSgrepCommands(pi, runtime = new AstSgrepRuntime(pi)) {
38
46
  for (const [name, description] of COMMANDS) {
@@ -42,7 +50,7 @@ export function registerAstSgrepCommands(pi, runtime = new AstSgrepRuntime(pi))
42
50
  const ctx = context;
43
51
  const result = await runCommand(runtime, name, ctx, args);
44
52
  const output = ctx.hasUI ? compactCommandResult(result) : JSON.stringify(result);
45
- ctx.ui.notify(output, result.ok ? "info" : "error");
53
+ ctx.ui.notify(output, result.ok ? result.diagnostics?.warning ? "warning" : "info" : "error");
46
54
  },
47
55
  });
48
56
  }
@@ -14,6 +14,10 @@ export type RuntimeLike = {
14
14
  nativeEnv?(options?: {
15
15
  env?: NodeJS.ProcessEnv;
16
16
  }): NodeJS.ProcessEnv;
17
+ binaryWarning?(): string | undefined;
18
+ diagnostics?(context: {
19
+ cwd: string;
20
+ }): Promise<Record<string, unknown>>;
17
21
  config?: {
18
22
  timeoutMs?: number;
19
23
  maxOutputBytes?: number;
@@ -42,6 +46,8 @@ export type CommandContext = ToolContext & {
42
46
  };
43
47
  };
44
48
  export type CommandResult = {
49
+ diagnostics?: Record<string, unknown>;
50
+ } & ({
45
51
  ok: true;
46
52
  command: string;
47
53
  response: MachineEnvelope;
@@ -53,7 +59,7 @@ export type CommandResult = {
53
59
  message: string;
54
60
  details: Readonly<Record<string, unknown>>;
55
61
  };
56
- };
62
+ });
57
63
  export type Update = (result: {
58
64
  content: Array<{
59
65
  type: "text";
@@ -13,7 +13,7 @@ export declare const DEFAULT_LIMIT = 8;
13
13
  * session, or a host that drops the built-ins still gets them — but they are
14
14
  * left out of the active set when the host already provides read+edit. Pi only
15
15
  * sends ACTIVE tools (schema, snippet, guidelines) to the model, so this is the
16
- * difference between ~296 tokens per request and nothing.
16
+ * difference between ~271 tokens per request and nothing.
17
17
  *
18
18
  * ASGREP_KEEP_FILE_TOOLS=1 pins them active regardless.
19
19
  */
@@ -4,22 +4,21 @@ import { Type } from "typebox";
4
4
  import { createAsgrepConnector, runCodemode, runNativeBatch, runBatchViaStdin, CODEMODE_TYPES_FOR_MODEL, NativeSessionPool, argvFor, asEnvelope, applyQueryScope, warmCodemodeSandbox, resetCodemodeSandboxForTests, isClosedWorkerError, } from "../codemode/index.js";
5
5
  import { AstSgrepRuntime, FreshnessCoordinator, RuntimeError } from "../runtime/runtime.js";
6
6
  import { RESOLVED_ROOT } from "../runtime/types.js";
7
- import { ASGREP_PROMPT_GUIDELINES, ASGREP_PROMPT_SNIPPET, formatCodemodeResult, } from "../ui/present.js";
7
+ import { ASGREP_PROMPT_GUIDELINES, ASGREP_PROMPT_GUIDELINES_HOST_FILES, ASGREP_PROMPT_SNIPPET, formatCodemodeResult, } from "../ui/present.js";
8
8
  import { EMPTY_CALL, renderAsgrepResult } from "../ui/card.js";
9
9
  import { bounded, errorDetails, failure, isFreshnessTimeout, extractInPath, report, success, } from "./results.js";
10
10
  export const DEFAULT_LIMIT = 8;
11
11
  const MAX_LIMIT = 100;
12
12
  const MAX_EXCERPT_LINES = 100;
13
13
  const searchParameters = Type.Object({
14
- query: Type.String({ maxLength: 4_096, description: "Query, symbol, or pattern" }),
14
+ query: Type.String({ maxLength: 4_096 }),
15
15
  mode: Type.Optional(Type.Unsafe({
16
16
  type: "string",
17
17
  enum: ["natural", "pattern", "defs", "callers", "chain", "semantic", "word", "literal", "regex", "imports"],
18
18
  default: "natural",
19
- description: "Search strategy",
20
19
  })),
21
20
  limit: Type.Optional(Type.Integer({ default: DEFAULT_LIMIT })),
22
- excerptLines: Type.Optional(Type.Integer({ default: 0, description: "Inline N excerpt lines per hit" })),
21
+ excerptLines: Type.Optional(Type.Integer({ default: 0 })),
23
22
  in: Type.Optional(Type.String({ maxLength: 512, description: "Bound to a directory or glob" })),
24
23
  lang: Type.Optional(Type.String({ maxLength: 32, description: "Language filter (rs, ts, py)" })),
25
24
  }, { additionalProperties: false });
@@ -27,9 +26,9 @@ const indexParameters = Type.Object({
27
26
  force: Type.Optional(Type.Boolean({ default: false, description: "Rebuild the index from scratch" })),
28
27
  }, { additionalProperties: false });
29
28
  const editParameters = Type.Object({
30
- path: Type.Optional(Type.String({ maxLength: 512, description: "File to edit" })),
29
+ path: Type.Optional(Type.String({ maxLength: 512 })),
31
30
  oldText: Type.Optional(Type.String({ description: "Exact text to replace (must match once)" })),
32
- newText: Type.Optional(Type.String({ description: "Replacement" })),
31
+ newText: Type.Optional(Type.String()),
33
32
  edits: Type.Optional(Type.Array(Type.Object({
34
33
  path: Type.Optional(Type.String({ minLength: 1, maxLength: 512 })),
35
34
  oldText: Type.String({ minLength: 1 }),
@@ -37,7 +36,7 @@ const editParameters = Type.Object({
37
36
  }), { maxItems: 64, description: "Multi-edit entries; top-level path is the default" })),
38
37
  }, { additionalProperties: false });
39
38
  const readParameters = Type.Object({
40
- path: Type.Optional(Type.String({ maxLength: 512, description: "File to read" })),
39
+ path: Type.Optional(Type.String({ maxLength: 512 })),
41
40
  ref: Type.Optional(Type.String({ description: "Hit ref path#L12-L40" })),
42
41
  refs: Type.Optional(Type.Array(Type.String(), { maxItems: 24, description: "Several refs in one call" })),
43
42
  start: Type.Optional(Type.Integer()),
@@ -49,7 +48,7 @@ const codemodeParameters = Type.Object({
49
48
  code: Type.String({
50
49
  minLength: 1,
51
50
  maxLength: 32_000,
52
- description: "JavaScript: async () => { ... } or a bare body with return. The returned value is the tool result.",
51
+ description: "JavaScript: async () => { ... } or a bare body with return.",
53
52
  }),
54
53
  timeoutMs: Type.Optional(Type.Integer({ description: "Timeout ms (default 30000)" })),
55
54
  }, { additionalProperties: false });
@@ -84,7 +83,7 @@ function withSearchLang(argv, lang) {
84
83
  * session, or a host that drops the built-ins still gets them — but they are
85
84
  * left out of the active set when the host already provides read+edit. Pi only
86
85
  * sends ACTIVE tools (schema, snippet, guidelines) to the model, so this is the
87
- * difference between ~296 tokens per request and nothing.
86
+ * difference between ~271 tokens per request and nothing.
88
87
  *
89
88
  * ASGREP_KEEP_FILE_TOOLS=1 pins them active regardless.
90
89
  */
@@ -282,7 +281,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
282
281
  const requireBackend = (availability, context) => {
283
282
  if (availability.kind !== "unavailable")
284
283
  return;
285
- throw new RuntimeError("BACKEND_UNAVAILABLE", "ast-sgrep backend unavailable (no NAPI session and no CLI binary)", {
284
+ throw new RuntimeError("BACKEND_UNAVAILABLE", `ast-sgrep backend unavailable (no NAPI session and no CLI binary)${availability.cause ? `: ${availability.cause}` : ""}. Run /asgrep-doctor for recovery details.`, {
286
285
  backend: "unavailable",
287
286
  // Agent-facing mirrors of the closed unavailable variant (not an open product).
288
287
  napi: false,
@@ -503,6 +502,11 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
503
502
  catch {
504
503
  // Doctor reports backend errors; a failed warmup must not block the session.
505
504
  }
505
+ finally {
506
+ const warning = runtime.binaryWarning?.();
507
+ if (warning && ctx.hasUI)
508
+ ctx.ui.notify(warning, "warning");
509
+ }
506
510
  })();
507
511
  });
508
512
  pi.on("session_shutdown", () => {
@@ -518,10 +522,10 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
518
522
  name: "asgrep",
519
523
  label: "asgrep",
520
524
  promptSnippet: ASGREP_PROMPT_SNIPPET,
521
- promptGuidelines: [...ASGREP_PROMPT_GUIDELINES],
525
+ promptGuidelines: [...(hostProvidesFileTools(pi) ? ASGREP_PROMPT_GUIDELINES_HOST_FILES : ASGREP_PROMPT_GUIDELINES)],
522
526
  description: [
523
- "Code search (in-process, warm Searcher): use it for any code lookup instead of grep.",
524
- "Write JavaScript; the returned value is your result.",
527
+ "Code search: use it for any code lookup instead of grep.",
528
+ "The returned value is your result.",
525
529
  CODEMODE_TYPES_FOR_MODEL,
526
530
  "Example: async () => (await asgrep.search(\"auth\", { limit: 5 })).hits",
527
531
  ].join("\n"),
@@ -608,7 +612,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
608
612
  pi.registerTool({
609
613
  name: "asgrep_search",
610
614
  label: "asgrep search",
611
- promptSnippet: "One-shot asgrep search",
615
+ promptSnippet: "One-shot search",
612
616
  description: "One-shot search. Use asgrep (Code Mode) for anything multi-step, parallel, or filtered.",
613
617
  parameters: searchParameters,
614
618
  renderShell: "self",
@@ -675,7 +679,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
675
679
  name: "asgrep_edit",
676
680
  label: "asgrep edit",
677
681
  promptSnippet: "Exact-string edit",
678
- description: "Edit by exact-string replace; oldText must match once. edits[] applies many atomically.",
682
+ description: "Edit by exact-string replace. edits[] applies many atomically.",
679
683
  parameters: editParameters,
680
684
  renderShell: "self",
681
685
  renderCall() {
@@ -717,14 +721,13 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
717
721
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
718
722
  report(onUpdate, "read", "started");
719
723
  try {
720
- ensurePool();
724
+ // Reading source must remain available when the binary or index is broken.
721
725
  const options = signal ? { signal } : {};
722
- const { root, scope, freshness: fresh } = await freshRoot(ctx.cwd, signal);
723
- const sticky = await pool.acquire(root);
724
- const bundle = createAsgrepConnector({ run: (a, c, o) => runtime.run(a, c, o), sticky }, rootedAt(root), { ...options, ...(scope ? { scope } : {}) });
726
+ const { root, scope } = await anchorRoot(ctx.cwd);
727
+ const bundle = createAsgrepConnector({ run: (a, c, o) => runtime.run(a, c, o) }, rootedAt(root), { ...options, localReads: true, ...(scope ? { scope } : {}) });
725
728
  const response = await bundle.asgrep.read(params);
726
729
  report(onUpdate, "read", "completed");
727
- return success("read", response, { backend: pool.backend(), ...(fresh ? { freshness: fresh } : {}) });
730
+ return success("read", response, { backend: "filesystem" });
728
731
  }
729
732
  catch (cause) {
730
733
  return failure("read", cause, signal);
@@ -735,7 +738,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
735
738
  name: "asgrep_index",
736
739
  label: "asgrep index",
737
740
  promptSnippet: "Build or rebuild the index",
738
- description: "Build or rebuild the index (embeddings included).",
741
+ description: "Build or rebuild the index.",
739
742
  parameters: indexParameters,
740
743
  renderShell: "self",
741
744
  renderCall() {
@@ -40,6 +40,9 @@ export declare class AstSgrepRuntime {
40
40
  resolveBinaryPath(options?: {
41
41
  env?: NodeJS.ProcessEnv;
42
42
  }): string;
43
+ binaryWarning(): string | undefined;
44
+ /** Report installed layers even if the project's index cannot be opened. */
45
+ diagnostics(context: RuntimeContext): Promise<Record<string, unknown>>;
43
46
  /** Merged process env for native Code Mode workers. */
44
47
  nativeEnv(options?: {
45
48
  env?: NodeJS.ProcessEnv;
@@ -1,6 +1,7 @@
1
1
  import { realpath } from "node:fs/promises";
2
- import { constants, accessSync } from "node:fs";
3
- import { isAbsolute, resolve } from "node:path";
2
+ import { constants, accessSync, readFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { createRequire } from "node:module";
4
5
  import { resolveBinary } from "ast-sgrep";
5
6
  import { CONFIG_SCHEMA_VERSION, DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_REFRESH_INTERVAL_MS, DEFAULT_TIMEOUT_MS, INDEX_FORMAT_VERSION, MACHINE_SCHEMA_VERSION, RUNTIME_VERSION, RuntimeError, RESOLVED_ROOT, } from "./types.js";
6
7
  import { finitePositive, migrateConfig, resolveConfig, rollbackConfig, } from "./config.js";
@@ -25,18 +26,37 @@ export async function resolveRuntimeRoot(projectCwd, requestedRoot, allowOutside
25
26
  }
26
27
  return candidate;
27
28
  }
28
- function getBinary(config, env, resolver) {
29
+ function getBinary(config, env, resolver, onRecovery) {
29
30
  let binary;
31
+ let warning;
30
32
  try {
31
33
  const options = config.binaryPath ? { binaryPath: config.binaryPath, env } : { env };
32
34
  binary = resolver(options);
33
35
  }
34
36
  catch (cause) {
35
37
  const message = cause instanceof Error ? cause.message : String(cause);
36
- if (config.binaryPath) {
37
- throw new RuntimeError("BINARY_NOT_FOUND", `Configured ast-sgrep binary is unavailable: ${config.binaryPath}`, { binaryPath: config.binaryPath, cause: message });
38
+ const failure = cause;
39
+ // Missing environment-only dev builds may recover, never explicit settings,
40
+ // permissions, empty artifacts, checksum failures, PATH, or downloads.
41
+ if (onRecovery && (env.ASGREP_BIN || env.AST_SGREP_BINARY) && failure?.code === "ASGREP_EXECUTABLE_MISSING" &&
42
+ (failure.cause?.code === "ENOENT" || failure.cause?.code === "ENOTDIR")) {
43
+ const variable = env.ASGREP_BIN ? "ASGREP_BIN" : "AST_SGREP_BINARY";
44
+ try {
45
+ binary = resolver({ env: { ...env, ASGREP_BIN: undefined, AST_SGREP_BINARY: undefined } });
46
+ }
47
+ catch (fallbackCause) {
48
+ const fallback = fallbackCause instanceof Error ? fallbackCause.message : String(fallbackCause);
49
+ throw new RuntimeError("BINARY_RESOLUTION_FAILED", `Missing ${variable} override (${config.binaryPath ?? env[variable]}); bundled binary unavailable: ${fallback}. Run /asgrep-doctor and reinstall pi-ast-sgrep with optional dependencies enabled.`, { cause: message, fallbackCause: fallback });
50
+ }
51
+ warning = `Ignored missing ${variable}=${config.binaryPath ?? env[variable]}; using bundled binary ${binary}. Remove the stale export from your shell configuration.`;
52
+ }
53
+ else {
54
+ const hint = "Correct binaryPath or unset ASGREP_BIN/AST_SGREP_BINARY; run /asgrep-doctor.";
55
+ if (config.binaryPath) {
56
+ throw new RuntimeError("BINARY_NOT_FOUND", `Configured ast-sgrep binary is unavailable: ${config.binaryPath}. ${message}. ${hint}`, { binaryPath: config.binaryPath, cause: message, hint });
57
+ }
58
+ throw new RuntimeError("BINARY_RESOLUTION_FAILED", `Unable to resolve an ast-sgrep binary for this platform: ${message}. Run /asgrep-doctor.`, { cause: message });
38
59
  }
39
- throw new RuntimeError("BINARY_RESOLUTION_FAILED", "Unable to resolve an ast-sgrep binary for this platform", { cause: message });
40
60
  }
41
61
  try {
42
62
  accessSync(binary, constants.X_OK);
@@ -44,6 +64,8 @@ function getBinary(config, env, resolver) {
44
64
  catch (cause) {
45
65
  throw new RuntimeError("BINARY_NOT_EXECUTABLE", `ast-sgrep binary is not executable: ${binary}`, { binaryPath: binary, cause: cause instanceof Error ? cause.message : String(cause) });
46
66
  }
67
+ if (warning)
68
+ onRecovery?.(warning);
47
69
  return binary;
48
70
  }
49
71
  function byteLength(value) { return Buffer.byteLength(value, "utf8"); }
@@ -153,11 +175,15 @@ export class AstSgrepRuntime {
153
175
  config;
154
176
  #resolver;
155
177
  #environment;
178
+ #recoverMissingOverride;
179
+ #binaryWarning;
156
180
  constructor(pi, sources = {}, dependencies = {}) {
157
181
  this.pi = pi;
158
182
  this.#environment = sources.environment ?? process.env;
159
183
  this.config = resolveConfig({ ...sources, environment: this.#environment });
160
184
  this.#resolver = dependencies.resolveBinary ?? resolveBinary;
185
+ this.#recoverMissingOverride = ![sources.explicitProjectConfig, sources.projectSettings, sources.globalSettings, sources.defaults]
186
+ .some(source => source?.binaryPath !== undefined);
161
187
  }
162
188
  async resolveRoot(context) {
163
189
  return context[RESOLVED_ROOT]
@@ -249,7 +275,7 @@ export class AstSgrepRuntime {
249
275
  const root = await this.resolveRoot(context);
250
276
  const timeout = finitePositive(options.timeoutMs, this.config.timeoutMs, "timeoutMs");
251
277
  const env = this.#mergedEnv(options.env);
252
- const binary = getBinary(this.config, env, this.#resolver);
278
+ const binary = this.resolveBinaryPath({ env });
253
279
  try {
254
280
  const execOptions = { cwd: root, env, timeout };
255
281
  if (options.signal)
@@ -267,7 +293,35 @@ export class AstSgrepRuntime {
267
293
  }
268
294
  /** Absolute path to the native binary (for sticky serve / stdin batch spawn). */
269
295
  resolveBinaryPath(options = {}) {
270
- return getBinary(this.config, this.#mergedEnv(options.env), this.#resolver);
296
+ this.#binaryWarning = undefined;
297
+ return getBinary(this.config, this.#mergedEnv(options.env), this.#resolver, this.#recoverMissingOverride ? warning => { this.#binaryWarning = warning; } : undefined);
298
+ }
299
+ binaryWarning() { return this.#binaryWarning; }
300
+ /** Report installed layers even if the project's index cannot be opened. */
301
+ async diagnostics(context) {
302
+ const versionAt = (path) => {
303
+ try {
304
+ return JSON.parse(readFileSync(path, "utf8")).version ?? "unknown";
305
+ }
306
+ catch {
307
+ return "unknown";
308
+ }
309
+ };
310
+ const result = {
311
+ extension: versionAt(new URL("../../package.json", import.meta.url)),
312
+ };
313
+ try {
314
+ const launcher = createRequire(import.meta.url).resolve("ast-sgrep");
315
+ result.launcher = versionAt(join(dirname(launcher), "..", "package.json"));
316
+ result.binaryPath = this.resolveBinaryPath();
317
+ result.native = (await this.checkCompatibility(context)).version;
318
+ }
319
+ catch (cause) {
320
+ result.error = cause instanceof Error ? cause.message : String(cause);
321
+ }
322
+ if (this.#binaryWarning)
323
+ result.warning = this.#binaryWarning;
324
+ return result;
271
325
  }
272
326
  /** Merged process env for native Code Mode workers. */
273
327
  nativeEnv(options = {}) {
@@ -27,8 +27,16 @@ export type EnvelopeLike = {
27
27
  backend?: unknown;
28
28
  [key: string]: unknown;
29
29
  };
30
- export declare const ASGREP_PROMPT_SNIPPET = "Code search by intent, symbol, defs, callers, pattern (asgrep; use without being asked)";
30
+ export declare const ASGREP_PROMPT_SNIPPET = "Code search by intent, symbol, defs, callers, pattern (use without being asked)";
31
31
  export declare const ASGREP_PROMPT_GUIDELINES: readonly ["Any code lookup: call asgrep first; read code with asgrep_read, change it with asgrep_edit.", "Compose in Code Mode (search/defs/read, Promise.all, small shaped value); grep only exact strings/filenames. Bound with in:\"path\"; on 0 hits use suggested_next."];
32
+ /**
33
+ * Guideline variant for hosts that already ship read/edit built in: our
34
+ * one-shot file tools are inactive there, so naming them would point the
35
+ * model at tools it cannot see. The host's own builtins describe
36
+ * themselves; this keeps only the asgrep-first routing plus the shared
37
+ * composition guideline.
38
+ */
39
+ export declare const ASGREP_PROMPT_GUIDELINES_HOST_FILES: readonly ["Any code lookup: call asgrep first.", "Compose in Code Mode (search/defs/read, Promise.all, small shaped value); grep only exact strings/filenames. Bound with in:\"path\"; on 0 hits use suggested_next."];
32
40
  export declare function paint(theme: PresentTheme | undefined, role: string, text: string, bold?: boolean): string;
33
41
  export declare function hitLocation(hit: HitLike): string;
34
42
  export declare function hitLabel(hit: HitLike): string;
@@ -1,9 +1,20 @@
1
1
  /** Neat asgrep tool chrome for the Pi TUI and the model-visible content. */
2
- export const ASGREP_PROMPT_SNIPPET = "Code search by intent, symbol, defs, callers, pattern (asgrep; use without being asked)";
2
+ export const ASGREP_PROMPT_SNIPPET = "Code search by intent, symbol, defs, callers, pattern (use without being asked)";
3
3
  export const ASGREP_PROMPT_GUIDELINES = [
4
4
  "Any code lookup: call asgrep first; read code with asgrep_read, change it with asgrep_edit.",
5
5
  "Compose in Code Mode (search/defs/read, Promise.all, small shaped value); grep only exact strings/filenames. Bound with in:\"path\"; on 0 hits use suggested_next.",
6
6
  ];
7
+ /**
8
+ * Guideline variant for hosts that already ship read/edit built in: our
9
+ * one-shot file tools are inactive there, so naming them would point the
10
+ * model at tools it cannot see. The host's own builtins describe
11
+ * themselves; this keeps only the asgrep-first routing plus the shared
12
+ * composition guideline.
13
+ */
14
+ export const ASGREP_PROMPT_GUIDELINES_HOST_FILES = [
15
+ "Any code lookup: call asgrep first.",
16
+ ASGREP_PROMPT_GUIDELINES[1],
17
+ ];
7
18
  export function paint(theme, role, text, bold = false) {
8
19
  const body = bold && theme ? theme.bold(text) : text;
9
20
  return theme ? theme.fg(role, body) : body;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ast-sgrep",
3
- "version": "2.2.1",
3
+ "version": "2.5.3",
4
4
  "description": "Native Code Mode, structural, graph, and semantic code search for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -63,7 +63,7 @@
63
63
  "node": ">=22.19.0"
64
64
  },
65
65
  "dependencies": {
66
- "ast-sgrep": ">=2.0.0 <3",
66
+ "ast-sgrep": ">=2.5.2 <3",
67
67
  "typebox": "^1.0.0"
68
68
  },
69
69
  "peerDependencies": {