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 +3 -1
- package/dist/codemode/connector.d.ts +1 -0
- package/dist/codemode/connector.js +2 -0
- package/dist/host/commands.js +13 -5
- package/dist/host/results.d.ts +7 -1
- package/dist/host/tools.d.ts +1 -1
- package/dist/host/tools.js +24 -21
- package/dist/runtime/runtime.d.ts +3 -0
- package/dist/runtime/runtime.js +62 -8
- package/dist/ui/present.d.ts +9 -1
- package/dist/ui/present.js +12 -1
- package/package.json +2 -2
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.
|
|
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
|
|
|
@@ -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 {
|
package/dist/host/commands.js
CHANGED
|
@@ -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
|
}
|
package/dist/host/results.d.ts
CHANGED
|
@@ -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";
|
package/dist/host/tools.d.ts
CHANGED
|
@@ -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 ~
|
|
16
|
+
* difference between ~271 tokens per request and nothing.
|
|
17
17
|
*
|
|
18
18
|
* ASGREP_KEEP_FILE_TOOLS=1 pins them active regardless.
|
|
19
19
|
*/
|
package/dist/host/tools.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
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 ~
|
|
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",
|
|
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
|
|
524
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
724
|
+
// Reading source must remain available when the binary or index is broken.
|
|
721
725
|
const options = signal ? { signal } : {};
|
|
722
|
-
const { root, scope
|
|
723
|
-
const
|
|
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:
|
|
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
|
|
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;
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
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 =
|
|
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
|
-
|
|
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/dist/ui/present.d.ts
CHANGED
|
@@ -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 (
|
|
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;
|
package/dist/ui/present.js
CHANGED
|
@@ -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 (
|
|
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.
|
|
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.
|
|
66
|
+
"ast-sgrep": ">=2.5.2 <3",
|
|
67
67
|
"typebox": "^1.0.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|