pi-ast-sgrep 2.5.2 → 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";
@@ -281,7 +281,7 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
281
281
  const requireBackend = (availability, context) => {
282
282
  if (availability.kind !== "unavailable")
283
283
  return;
284
- 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.`, {
285
285
  backend: "unavailable",
286
286
  // Agent-facing mirrors of the closed unavailable variant (not an open product).
287
287
  napi: false,
@@ -502,6 +502,11 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
502
502
  catch {
503
503
  // Doctor reports backend errors; a failed warmup must not block the session.
504
504
  }
505
+ finally {
506
+ const warning = runtime.binaryWarning?.();
507
+ if (warning && ctx.hasUI)
508
+ ctx.ui.notify(warning, "warning");
509
+ }
505
510
  })();
506
511
  });
507
512
  pi.on("session_shutdown", () => {
@@ -716,14 +721,13 @@ export function registerAstSgrepTools(pi, runtime = new AstSgrepRuntime(pi), fre
716
721
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
717
722
  report(onUpdate, "read", "started");
718
723
  try {
719
- ensurePool();
724
+ // Reading source must remain available when the binary or index is broken.
720
725
  const options = signal ? { signal } : {};
721
- const { root, scope, freshness: fresh } = await freshRoot(ctx.cwd, signal);
722
- const sticky = await pool.acquire(root);
723
- 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 } : {}) });
724
728
  const response = await bundle.asgrep.read(params);
725
729
  report(onUpdate, "read", "completed");
726
- return success("read", response, { backend: pool.backend(), ...(fresh ? { freshness: fresh } : {}) });
730
+ return success("read", response, { backend: "filesystem" });
727
731
  }
728
732
  catch (cause) {
729
733
  return failure("read", cause, signal);
@@ -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 = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ast-sgrep",
3
- "version": "2.5.2",
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": {