pi-ast-sgrep 1.3.2 → 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/README.md CHANGED
@@ -1,48 +1,242 @@
1
1
  # pi-ast-sgrep
2
2
 
3
- [![ast-sgrep for Pi](assets/preview.png)](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/pi-package.md)
3
+ Native Code Mode, structural, graph, and semantic code search for [Pi](https://github.com/earendil-works/pi).
4
4
 
5
- Native intent, structural, definition, caller, chain, and semantic code search for Pi.
5
+ [![pi-ast-sgrep: native code search inside Pi](https://cdn.jsdelivr.net/npm/pi-ast-sgrep/assets/preview.png)](https://pi.dev/packages/pi-ast-sgrep?name=pi-ast-sgrep)
6
+
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
+
9
+ **v2.0.0** · 13 languages · local-first semantic · critic + two-channel `AND` · **Code Mode** (on by default, no API key)
10
+
11
+ **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
+
13
+ ### What's new for Pi in 2.0
14
+
15
+ | Change | What you get |
16
+ |--------|----------------|
17
+ | Local-first embeddings | No `ASGREP_EMBED_API_KEY` / Ollama URL. Hashed semantic is default; optional ONNX stays in-process. |
18
+ | Results on the model path | `asgrep_search` and Code Mode serialize hits into `content`. |
19
+ | Two-channel queries | `asgrep.search({ query: 'callers:process_request AND pattern:fn $NAME($$$)' })` joins by span; `AND NOT` subtracts. Plain English `and` stays hybrid. |
20
+ | Critic + follow-ups | Agent envelopes include `why` (`critic:` notes) and causal `follow_up_queries` from the actual top hit. |
21
+ | Native work off the event loop | Index/search run as N-API worker tasks so Pi JS is not blocked on SQLite. |
22
+ | Auto-registered tools | `asgrep` lands without requiring a skill file. |
23
+ | Schema 12 | Older indexes rebuild through the normal compatibility path; `/asgrep-reindex` is the explicit full rebuild. |
24
+
25
+ ## Install
6
26
 
7
27
  ```bash
8
28
  pi install npm:pi-ast-sgrep
9
29
  ```
10
30
 
11
- Requires Node.js `>=22.19.0`, Pi `>=0.80.6 <1`, and a packaged host: macOS arm64/x64, glibc Linux arm64/x64, or Windows x64. The extension, `ast-sgrep` launcher, and selected native package manifests are exact-version matched at `1.3.2`; the embedded CLI compatibility identity is `1.3.2`. Alpine/musl, Windows arm64, and other hosts fail with an actionable unsupported-platform error; there is no source build or runtime download fallback.
31
+ Restart Pi if the current session does not load newly installed package resources. For a project-local installation, add `-l`:
32
+
33
+ ```bash
34
+ pi install -l npm:pi-ast-sgrep
35
+ ```
12
36
 
13
- ## First use
37
+ No Rust toolchain or separate MCP server is required. The npm package selects the native binary and in-process addon for the current supported platform.
14
38
 
15
- The package registers:
39
+ ## What this package adds
16
40
 
17
- - `asgrep_search`, `asgrep_index`, and `asgrep_status` tools;
18
- - `/asgrep-doctor`, `/asgrep-status`, `/asgrep-index`, and `/asgrep-reindex` commands;
19
- - the `ast-sgrep` skill.
41
+ | Resource | Purpose |
42
+ |---|---|
43
+ | `asgrep` | Primary tool. Run a bounded JavaScript program that composes typed `asgrep.*` calls. Auto-registered with Pi (no skill file). |
44
+ | `asgrep_search` | Run one natural, structural, symbol, graph, semantic, word, literal, or regex lookup. |
45
+ | `asgrep_index` | Create, refresh, or explicitly rebuild the current project index. |
46
+ | `asgrep_status` | Inspect the selected root, index, backend, counts, and capabilities. |
20
47
 
21
- Open Pi in a project and search. The first search lazily creates `.asgrep/`. Examples for `asgrep_search`:
48
+ The package also registers `/asgrep-doctor`, `/asgrep-status`, `/asgrep-index`, and `/asgrep-reindex`.
49
+
50
+ ## Start with Code Mode
51
+
52
+ Ask Pi:
53
+
54
+ > Use ast-sgrep Code Mode to find where access tokens are refreshed, trace the top result's callers, and return only the relevant files, symbols, and lines.
55
+
56
+ Pi can make one `asgrep` call like this:
22
57
 
23
58
  ```json
24
- {"query":"auth_refresh","mode":"defs"}
25
- {"query":"auth_refresh","mode":"callers"}
26
- {"query":"where are credentials renewed?","mode":"semantic"}
59
+ {
60
+ "code": "async () => {\n const seed = await asgrep.search({ query: 'where are access tokens refreshed?', limit: 5 });\n const symbol = seed.hits?.[0]?.symbol;\n if (!symbol) return { seed };\n const [defs, callers] = await Promise.all([\n asgrep.defs({ symbol, limit: 5 }),\n asgrep.callers({ symbol, limit: 10 }),\n ]);\n return { symbol, defs: defs.hits, callers: callers.hits };\n}"
61
+ }
27
62
  ```
28
63
 
29
- Run `/asgrep-doctor` to diagnose the runtime, binary, protocol, index, or configuration; run `/asgrep-status` to inspect the current project. The extension refreshes successful Pi write/edit changes before the next search and coalesces concurrent refreshes.
64
+ This workflow narrows the first result, runs independent follow-up searches together, and returns a small shaped value to the model.
65
+
66
+ ### Code Mode API
67
+
68
+ The Code Mode program receives these asynchronous methods on `asgrep`:
69
+
70
+ | Method | Use |
71
+ |---|---|
72
+ | `asgrep.search({ query, limit?, excerptLines? })` | Search by intent, symbol, or a prefixed structural query. |
73
+ | `asgrep.semantic({ query, limit?, excerptLines? })` | Search local semantic embeddings directly. |
74
+ | `asgrep.defs({ symbol, limit? })` | Find definitions for one symbol. |
75
+ | `asgrep.callers({ symbol, limit? })` | Find call sites for one symbol. |
76
+ | `asgrep.imports({ module, limit? })` | Find imports of one module. |
77
+ | `asgrep.chain({ query, limit? })` | Trace related symbols and graph edges. |
78
+ | `asgrep.indexStatus()` | Read index and backend state. |
79
+ | `asgrep.indexRepo({ force? })` | Create, refresh, or rebuild the index. |
80
+ | `asgrep.catalogSearch({ query })` | Discover less common ast-sgrep operations. |
81
+ | `asgrep.catalogDescribe({ name })` | Read the schema for a discovered operation. |
82
+
83
+ Use `Promise.all` for independent calls. Filter, map, sort, and slice intermediate values in JavaScript. Return only the evidence needed for the next reasoning step.
84
+
85
+ Code Mode runs in a disposable worker with a restricted `node:vm` context that exposes only a serialized `asgrep.*` bridge and console. String and WebAssembly code generation are disabled, ambient Node globals such as `process` and `require` are not exposed, and terminating the worker contains synchronous and microtask CPU loops. Node does not consider `vm` an adversarial-code security boundary, however, and the installed Pi package has full OS-user access; do not treat Code Mode as an OS jail. Prefer Code Mode **or** MCP for a client, never both.
86
+
87
+ The bridge rejects oversized call arguments and serialized results, allows at
88
+ most 256 host calls per program, and caps collected console output before it
89
+ reaches the extension host. Raw-memory and WebAssembly globals are unavailable;
90
+ worker heap/stack limits contain the remaining accidental memory growth. Native
91
+ tool values are capped at 1 MiB each and complete batch responses at 4 MiB before
92
+ Node-API converts them into extension-host objects. These bounds do not turn `node:vm` into an OS
93
+ sandbox.
94
+
95
+ ## Direct one-shot search
96
+
97
+ Use `asgrep_search` when one lookup is enough:
98
+
99
+ ```json
100
+ {"query":"auth_refresh","mode":"defs","limit":8}
101
+ {"query":"auth_refresh","mode":"callers","limit":8}
102
+ {"query":"where are credentials renewed?","mode":"semantic","limit":8}
103
+ {"query":"$CLIENT.post($URL)","mode":"pattern","limit":8}
104
+ {"query":"callers:process_request AND pattern:fn $NAME($$$)", "mode":"natural","limit":8}
105
+ {"query":"defs:handle AND NOT callers:test_","mode":"natural","limit":8}
106
+ ```
107
+
108
+ Available modes:
109
+
110
+ | Mode | Best for |
111
+ |---|---|
112
+ | `natural` | Intent or mixed code-language queries when exact spelling is unknown. Also the mode for two-channel `AND` / `AND NOT` query strings. |
113
+ | `pattern` | Syntax-aware ast-sgrep patterns with metavariables. |
114
+ | `defs`, `callers`, `imports` | Symbol and module navigation. |
115
+ | `chain` | Multi-hop relationship tracing. |
116
+ | `semantic` | Meaning-based local vector search. |
117
+ | `word`, `literal`, `regex` | Explicit text-oriented matching. |
118
+
119
+ `limit` accepts 1–100 and defaults to 8. Excerpts are disabled by default; set `excerptLines` only after narrowing the result set.
120
+
121
+ ## Why Code Mode is fast
122
+
123
+ Official platform packages include `ast-sgrep-codemode.node`. The extension loads an in-process native `CodeModeSession` and keeps one warm Searcher per project root for Code Mode, direct tools, and freshness checks. Normal searches do not spawn a CLI process.
124
+
125
+ Native index and search calls run as Promise-returning N-API worker tasks rather than on Node's event-loop thread. Calls for one warm session are serialized before entering libuv so concurrent Pi work does not occupy worker threads waiting on the same SQLite session.
126
+
127
+ Independent calls created in the same JavaScript turn are coalesced into a batch. `Promise.all` can therefore fan out several lookups while the model makes one tool call. If the native addon is unavailable, the bundled CLI service is a degraded fallback; `/asgrep-doctor` reports the active backend.
128
+
129
+ Code Mode and `ast-sgrep-mcp` are separate front ends over the same Rust search core. Pi uses Code Mode directly and does not use an MCP adapter.
130
+
131
+ ## Indexing and freshness
132
+
133
+ Start Pi in the repository you want to search. The first search validates the index and lazily creates `<project-root>/.asgrep/` when needed. Run `/asgrep-index` if you want to build it before searching.
134
+
135
+ After a successful Pi `write` or `edit`, the extension marks the affected path dirty and updates only known changed paths before the next search. It also watches the project for external filesystem changes: known file changes receive the same targeted update, while renames, directory changes, ignore-file edits, watcher errors, and ambiguous events trigger a correctness scan. `.asgrep` writes are excluded so indexing cannot dirty itself. If recursive watching is unavailable, an immediate scan plus the periodic full scan preserve correctness. Concurrent searches for the same root share one in-flight refresh.
136
+
137
+ The periodic interval forces a full incremental reconciliation even when the watcher reports nothing, covering dropped or coalesced filesystem events. Run `/asgrep-index` when you need freshness immediately after a large external operation; use `/asgrep-reindex` only for an incompatible or corrupt index, or when you explicitly need a strict full rebuild.
138
+
139
+ The package never edits `.gitignore`. Add this entry yourself if index data must stay untracked:
140
+
141
+ ```gitignore
142
+ .asgrep/
143
+ ```
144
+
145
+ ## Commands
146
+
147
+ | Command | Action |
148
+ |---|---|
149
+ | `/asgrep-doctor` | Check package versions, native runtime, protocol, index, and project settings. |
150
+ | `/asgrep-status` | Show the current root and index state. |
151
+ | `/asgrep-index` | Create or incrementally refresh the index. |
152
+ | `/asgrep-reindex` | Strictly rebuild the index in one transaction while preserving the prior usable rows on failure. |
153
+
154
+ These commands take no arguments.
155
+
156
+ ## Requirements
157
+
158
+ - Node.js `>=22.19.0`.
159
+ - Pi currently tested with `@earendil-works/pi-coding-agent >=0.80.6 <1`.
160
+ - macOS arm64 or x64, glibc Linux arm64 or x64, or Windows x64.
161
+
162
+ Alpine/musl Linux, Windows arm64, and other hosts are not packaged. The package does not compile Rust, search `PATH`, or download executables at runtime. On an unsupported host, run `/asgrep-doctor` for the exact platform error.
163
+
164
+ The extension, `ast-sgrep` launcher, platform package, native addon, and embedded CLI are exact-version matched. Update or reinstall the complete package if doctor reports a version or protocol mismatch.
165
+
166
+ ## Typed Code Mode API
167
+
168
+ Import the search-only programmatic surface from `pi-ast-sgrep/code-mode` and execute related lookups in one typed plan:
169
+
170
+ ```ts
171
+ import { AstSgrepRuntime } from "pi-ast-sgrep/runtime";
172
+ import { createSgrepCodeMode } from "pi-ast-sgrep/code-mode";
173
+
174
+ const mode = createSgrepCodeMode(new AstSgrepRuntime(pi), { cwd: process.cwd() });
175
+ const result = await mode.execute(async (sgrep) => {
176
+ const [text, ast, semantic] = await Promise.all([
177
+ sgrep.keywordSearch("refresh token"),
178
+ sgrep.astSearch("function_declaration"),
179
+ sgrep.semanticSearch("credential renewal"),
180
+ ]);
181
+ const bodies = await sgrep.codeRead(text.hits.slice(0, 3), { contextLines: 2 });
182
+ return { text, ast, semantic, bodies };
183
+ });
184
+ ```
185
+
186
+ - `keywordSearch` runs lexical retrieval only.
187
+ - `astSearch` runs `pattern:` structural search only.
188
+ - `semanticSearch` runs embedding retrieval only.
189
+ - `codeRead` streams bounded `file#Lx-Ly` refs inside the project, including adjacent context, symlink containment, strict UTF-8 validation, cancellation, and an aggregate output budget.
190
+ - `find`, `astFind`, `semantic`, and `read` remain typed aliases for the four methods above.
191
+
192
+ The agent chooses the retrieval granularity; these methods never auto-fuse channels. One-shot CLI search retains fusion for human/direct engine use. The API exposes no rewrite or mutation operation. Structural rewrites remain delegated to ast-grep. Search responses retain signal, contributor, score, and margin provenance.
30
193
 
31
194
  ## Local by default
32
195
 
33
- Local semantic indexing/search works offline with no credential, telemetry, first-use model download, executable download, PATH lookup, or MCP adapter. Optional external embedding providers are opt-in and may receive the source text and queries needed to create embeddings.
196
+ The default semantic backend works offline. It needs no credential, sends no telemetry, and downloads no model on first use. Search data stays under the project's `.asgrep/` directory.
197
+
198
+ In-process neural embeddings are optional (`--features neural-embed`). They never send source text to a remote embedding API. Hashed local search remains the default.
199
+
200
+ Pi packages are trusted code. Installation grants this JavaScript extension and its native code the permissions of the OS user running Pi. Project-root confinement is a package policy, not an operating-system security boundary.
201
+
202
+ ## Configuration
203
+
204
+ Defaults are a 30-second operation timeout, 4 MiB output limit, and 30-second freshness interval. Supported environment settings are:
34
205
 
35
- Indexing writes database, embedding, metadata, and lock/rebuild files under the project's `.asgrep/`. The package never edits `.gitignore`; add `.asgrep/` yourself if you do not want it committed. Pi packages run with the OS user's full access and are not sandboxed.
206
+ | Setting | Purpose |
207
+ |---|---|
208
+ | `ASGREP_ROOT` | Select the project root. |
209
+ | `ASGREP_TIMEOUT_MS` | Set the native operation timeout. |
210
+ | `ASGREP_MAX_OUTPUT_BYTES` | Bound native output. |
211
+ | `ASGREP_REFRESH_INTERVAL_MS` | Set the idle freshness-check interval. |
212
+ | `ASGREP_BIN` | Override the packaged binary for development. |
36
213
 
37
- ## Update or remove
214
+ Explicit project configuration can opt into `allowOutsideProject`; global settings and environment variables cannot relax the default project boundary. See the [complete package guide](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/pi-package.md) for schema and precedence details.
215
+
216
+ ## Update, rollback, or remove
38
217
 
39
218
  ```bash
40
219
  pi update npm:pi-ast-sgrep
41
220
  pi remove npm:pi-ast-sgrep
42
221
  ```
43
222
 
44
- Removal preserves every project's `.asgrep` data for reinstall or rollback. Delete that directory separately and explicitly only when you no longer need it. Compatible updates reuse validated data; incompatible formats rebuild atomically and preserve recoverable prior data on failure. Roll back by removing the package and installing `npm:pi-ast-sgrep@<previous-version>`, then run `/asgrep-doctor`.
223
+ Removal preserves each project's `.asgrep/` data for reinstall or rollback. Delete that directory separately only when you no longer need the index.
224
+
225
+ To roll back, install one prior version as a matched unit:
226
+
227
+ ```bash
228
+ pi remove npm:pi-ast-sgrep
229
+ pi install npm:pi-ast-sgrep@<previous-version>
230
+ ```
231
+
232
+ Then run `/asgrep-doctor`. Compatible updates reuse validated data. Incompatible formats rebuild transactionally in place and preserve recoverable prior rows when a rebuild fails.
233
+
234
+ ## More documentation
45
235
 
46
- Read the [complete install, configuration, security, recovery, and uninstall guide](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/pi-package.md). Release provenance and package order are documented in [RELEASING.md](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/RELEASING.md).
236
+ - [Complete Pi package guide](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/pi-package.md)
237
+ - [Code Mode architecture and performance](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/codemode.md)
238
+ - [Query grammar](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/QUERY_GRAMMAR.md) (prefixes and two-channel `AND`)
239
+ - [Fusion ranking and critic](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/fusion-ranking.md)
240
+ - [Release provenance](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/RELEASING.md)
47
241
 
48
242
  MIT
@@ -0,0 +1,89 @@
1
+ import { type AstSgrepRuntime, type MachineEnvelope, type RunOptions, type RuntimeContext } from "./runtime.js";
2
+ export type SgrepKind = "asgrep" | "def" | "caller" | "graph" | "anchor" | "import" | "pattern" | "embed";
3
+ export type SgrepSignal = "exact" | "structural" | "semantic";
4
+ export type SgrepRef = `${string}#L${number}-L${number}`;
5
+ /**
6
+ * Trusted search hit. Location is solely `ref` (parsed once at the CLI/JSON boundary).
7
+ * Wire may still dual-encode file/lines; those are not live fields on this type.
8
+ */
9
+ export interface SgrepHit {
10
+ kind: SgrepKind;
11
+ signal: SgrepSignal;
12
+ contributors: SgrepKind[];
13
+ score: number;
14
+ margin: number;
15
+ ref: SgrepRef;
16
+ preview: string;
17
+ symbol?: string | null;
18
+ caller?: string | null;
19
+ callee?: string | null;
20
+ language?: string | null;
21
+ excerpt?: string;
22
+ }
23
+ export interface SgrepSearchResponse extends MachineEnvelope {
24
+ hits: SgrepHit[];
25
+ query?: string;
26
+ hit_count?: number;
27
+ }
28
+ export interface SgrepSearchOptions extends RunOptions {
29
+ limit?: number;
30
+ excerptLines?: number;
31
+ }
32
+ export interface SgrepReadOptions {
33
+ contextLines?: number;
34
+ /** Aggregate character budget across all refs. */
35
+ maxChars?: number;
36
+ signal?: AbortSignal;
37
+ }
38
+ /**
39
+ * Trusted read window. Location is solely `ref` (actual lines returned; may expand the
40
+ * request via contextLines). Derive file/lines with `parseSgrepRef` -- no live twins.
41
+ */
42
+ export interface SgrepReadResult {
43
+ ref: SgrepRef;
44
+ content: string;
45
+ truncated: boolean;
46
+ /** Present when truncated: 1-indexed line to resume from (on the last shown line). */
47
+ resumeOffset?: number;
48
+ /** Named recovery hint for the model (empty/past-EOF/truncation). */
49
+ note?: string;
50
+ }
51
+ export interface SgrepApi {
52
+ keywordSearch(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
53
+ astSearch(pattern: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
54
+ semanticSearch(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
55
+ codeRead(ids: SgrepRef | Pick<SgrepHit, "ref"> | readonly (SgrepRef | Pick<SgrepHit, "ref">)[], options?: SgrepReadOptions): Promise<SgrepReadResult[]>;
56
+ /** Alias for keywordSearch. */
57
+ find(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
58
+ /** Alias for astSearch. */
59
+ astFind(pattern: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
60
+ /** Alias for semanticSearch. */
61
+ semantic(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
62
+ /** Alias for codeRead. */
63
+ read(ids: SgrepRef | Pick<SgrepHit, "ref"> | readonly (SgrepRef | Pick<SgrepHit, "ref">)[], options?: SgrepReadOptions): Promise<SgrepReadResult[]>;
64
+ }
65
+ export type SgrepPlan<T> = (sgrep: Readonly<SgrepApi>) => T | Promise<T>;
66
+ type RuntimeLike = Pick<AstSgrepRuntime, "run" | "resolveRoot">;
67
+ /** Derive file/lines from a branded ref (sole location encoding on SgrepHit). */
68
+ export declare function parseSgrepRef(ref: SgrepRef): {
69
+ file: string;
70
+ start: number;
71
+ end: number;
72
+ };
73
+ export declare class SgrepCodeMode implements SgrepApi {
74
+ #private;
75
+ private readonly runtime;
76
+ private readonly context;
77
+ constructor(runtime: RuntimeLike, context: RuntimeContext);
78
+ execute<T>(plan: SgrepPlan<T>): Promise<T>;
79
+ keywordSearch(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
80
+ astSearch(pattern: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
81
+ semanticSearch(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
82
+ find(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
83
+ astFind(pattern: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
84
+ semantic(query: string, options?: SgrepSearchOptions): Promise<SgrepSearchResponse>;
85
+ codeRead(ids: SgrepRef | Pick<SgrepHit, "ref"> | readonly (SgrepRef | Pick<SgrepHit, "ref">)[], options?: SgrepReadOptions): Promise<SgrepReadResult[]>;
86
+ read(ids: SgrepRef | Pick<SgrepHit, "ref"> | readonly (SgrepRef | Pick<SgrepHit, "ref">)[], options?: SgrepReadOptions): Promise<SgrepReadResult[]>;
87
+ }
88
+ export declare function createSgrepCodeMode(runtime: RuntimeLike, context: RuntimeContext): SgrepCodeMode;
89
+ export {};