pi-ast-sgrep 1.3.2 → 1.4.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.
@@ -0,0 +1,64 @@
1
+ import type { MachineEnvelope } from "../runtime.js";
2
+ import type { ChainArgs, SearchArgs } from "./types.js";
3
+ import { type BatchCapableHost, type DispatchStats } from "./dispatch.js";
4
+ export type ConnectorHost = {
5
+ /** Typed tool call (preferred — no argv archaeology). */
6
+ call?(tool: string, args: Record<string, unknown>, context: {
7
+ cwd: string;
8
+ }, options?: {
9
+ signal?: AbortSignal;
10
+ }): Promise<MachineEnvelope>;
11
+ /** Legacy CLI argv (spawn fallback / direct tools). */
12
+ run(args: readonly string[], context: {
13
+ cwd: string;
14
+ }, options?: {
15
+ signal?: AbortSignal;
16
+ }): Promise<MachineEnvelope>;
17
+ };
18
+ export type AsgrepConnector = {
19
+ search(input: SearchArgs): Promise<MachineEnvelope>;
20
+ semantic(input: SearchArgs): Promise<MachineEnvelope>;
21
+ chain(input: ChainArgs): Promise<MachineEnvelope>;
22
+ defs(input: {
23
+ symbol: string;
24
+ limit?: number;
25
+ excerptLines?: number;
26
+ }): Promise<MachineEnvelope>;
27
+ callers(input: {
28
+ symbol: string;
29
+ limit?: number;
30
+ excerptLines?: number;
31
+ }): Promise<MachineEnvelope>;
32
+ imports(input: {
33
+ module: string;
34
+ limit?: number;
35
+ excerptLines?: number;
36
+ }): Promise<MachineEnvelope>;
37
+ indexStatus(): Promise<MachineEnvelope>;
38
+ indexRepo(input?: {
39
+ force?: boolean;
40
+ }): Promise<MachineEnvelope>;
41
+ /** Progressive discovery (like deferred tools) — list/filter available asgrep tools. */
42
+ catalogSearch(input: {
43
+ query: string;
44
+ }): Promise<MachineEnvelope>;
45
+ catalogDescribe(input: {
46
+ name: string;
47
+ }): Promise<MachineEnvelope>;
48
+ };
49
+ export type ConnectorBundle = {
50
+ asgrep: AsgrepConnector;
51
+ stats: () => DispatchStats;
52
+ resetStats: () => void;
53
+ };
54
+ /**
55
+ * Host-side connector: typed methods the sandbox calls.
56
+ *
57
+ * Same-tick calls (Promise.all) are coalesced by CodemodeDispatcher so N
58
+ * lookups share sticky serve / one warm batch process when available.
59
+ */
60
+ export declare function createAsgrepConnector(host: BatchCapableHost, context: {
61
+ cwd: string;
62
+ }, options?: {
63
+ signal?: AbortSignal;
64
+ }): ConnectorBundle;
@@ -0,0 +1,73 @@
1
+ import { createCodemodeDispatcher, } from "./dispatch.js";
2
+ const DEFAULT_LIMIT = 8;
3
+ function clampLimit(limit) {
4
+ if (limit === undefined)
5
+ return DEFAULT_LIMIT;
6
+ return Math.min(100, Math.max(1, Math.trunc(limit)));
7
+ }
8
+ function clampExcerpt(excerptLines) {
9
+ if (excerptLines === undefined)
10
+ return 0;
11
+ return Math.min(100, Math.max(0, Math.trunc(excerptLines)));
12
+ }
13
+ /**
14
+ * Host-side connector: typed methods the sandbox calls.
15
+ *
16
+ * Same-tick calls (Promise.all) are coalesced by CodemodeDispatcher so N
17
+ * lookups share sticky serve / one warm batch process when available.
18
+ */
19
+ export function createAsgrepConnector(host, context, options = {}) {
20
+ const dispatcher = createCodemodeDispatcher(host);
21
+ const runOptions = options.signal ? { signal: options.signal } : {};
22
+ const call = (tool, args) => {
23
+ if (dispatcher.host.call) {
24
+ return dispatcher.host.call(tool, args, context, runOptions);
25
+ }
26
+ // Should not happen — dispatcher always exposes call.
27
+ return dispatcher.host.run([], context, runOptions);
28
+ };
29
+ // Bound function properties (not methods) so vm call sites cannot lose `this`.
30
+ const asgrep = {
31
+ search: (input) => call("search", {
32
+ query: input.query,
33
+ limit: clampLimit(input.limit),
34
+ excerpt_lines: clampExcerpt(input.excerptLines),
35
+ format: input.format === "agent" ? "agent" : "capsule",
36
+ }),
37
+ semantic: (input) => call("semantic", {
38
+ query: input.query,
39
+ limit: clampLimit(input.limit),
40
+ excerpt_lines: clampExcerpt(input.excerptLines),
41
+ format: input.format === "agent" ? "agent" : "capsule",
42
+ }),
43
+ chain: (input) => call("chain", {
44
+ query: input.query,
45
+ limit: clampLimit(input.limit),
46
+ top_n: 20,
47
+ }),
48
+ defs: (input) => call("defs", {
49
+ symbol: input.symbol,
50
+ limit: clampLimit(input.limit),
51
+ excerpt_lines: clampExcerpt(input.excerptLines),
52
+ }),
53
+ callers: (input) => call("callers", {
54
+ symbol: input.symbol,
55
+ limit: clampLimit(input.limit),
56
+ excerpt_lines: clampExcerpt(input.excerptLines),
57
+ }),
58
+ imports: (input) => call("imports", {
59
+ module: input.module,
60
+ limit: clampLimit(input.limit),
61
+ excerpt_lines: clampExcerpt(input.excerptLines),
62
+ }),
63
+ indexStatus: () => call("index_status", {}),
64
+ indexRepo: (input = {}) => call("index_repo", { force: input.force === true }),
65
+ catalogSearch: (input) => call("catalog_search", { query: input.query }),
66
+ catalogDescribe: (input) => call("catalog_describe", { name: input.name }),
67
+ };
68
+ return {
69
+ asgrep,
70
+ stats: dispatcher.stats,
71
+ resetStats: dispatcher.resetStats,
72
+ };
73
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Same-tick call coalescing + typed batch / sticky-serve dispatch.
3
+ *
4
+ * Amdahl: serial cost is process spawn + SQLite open. Sticky serve kills spawn
5
+ * for the whole Code Mode program; batch coalescing kills it per Promise.all wave.
6
+ */
7
+ import type { MachineEnvelope } from "../runtime.js";
8
+ import type { ConnectorHost } from "./connector.js";
9
+ export type CodemodeToolCall = {
10
+ tool: string;
11
+ args: Record<string, unknown>;
12
+ };
13
+ export type DispatchStats = {
14
+ waves: number;
15
+ calls: number;
16
+ batchedCalls: number;
17
+ parallelSpawnCalls: number;
18
+ stickyCalls: number;
19
+ wallMs: number;
20
+ };
21
+ export type BatchResult = {
22
+ results: Array<{
23
+ id: string;
24
+ ok: boolean;
25
+ value?: unknown;
26
+ error?: string;
27
+ }>;
28
+ mode?: string;
29
+ wall_ms?: number;
30
+ all_ok?: boolean;
31
+ };
32
+ export type StickyWorker = {
33
+ call(tool: string, args: Record<string, unknown>, options?: {
34
+ signal?: AbortSignal;
35
+ }): Promise<MachineEnvelope>;
36
+ batch(calls: Array<{
37
+ id: string;
38
+ tool: string;
39
+ args: Record<string, unknown>;
40
+ }>, options?: {
41
+ signal?: AbortSignal;
42
+ }): Promise<BatchResult>;
43
+ end(): Promise<void>;
44
+ };
45
+ export type BatchCapableHost = ConnectorHost & {
46
+ /** One-shot warm batch (codemode-batch). */
47
+ runBatch?(calls: Array<{
48
+ id: string;
49
+ tool: string;
50
+ args: Record<string, unknown>;
51
+ }>, context: {
52
+ cwd: string;
53
+ }, options?: {
54
+ signal?: AbortSignal;
55
+ }): Promise<BatchResult>;
56
+ /** Sticky NDJSON worker for the whole Code Mode program (preferred). */
57
+ sticky?: StickyWorker | null;
58
+ };
59
+ /**
60
+ * Wraps a host so Promise.all([asgrep.search, asgrep.defs, …]) collapses into
61
+ * one microtask wave. Prefers sticky serve → one-shot batch → overlapped spawn.
62
+ */
63
+ export declare function createCodemodeDispatcher(host: BatchCapableHost): {
64
+ host: ConnectorHost;
65
+ stats: () => DispatchStats;
66
+ resetStats: () => void;
67
+ };
68
+ /** Build CLI argv for spawn fallback (typed path preferred). */
69
+ export declare function argvFor(tool: string, args: Record<string, unknown>): string[];
70
+ export declare function asEnvelope(value: unknown, command?: string): MachineEnvelope;
71
+ /** One-shot batch via stdin (no tempfile) when spawn-with-stdin is available. */
72
+ export declare function runNativeBatch(run: ConnectorHost["run"], calls: Array<{
73
+ id: string;
74
+ tool: string;
75
+ args: Record<string, unknown>;
76
+ }>, context: {
77
+ cwd: string;
78
+ }, options?: {
79
+ signal?: AbortSignal;
80
+ }, writeBatch?: (body: string, context: {
81
+ cwd: string;
82
+ }, options?: {
83
+ signal?: AbortSignal;
84
+ }) => Promise<MachineEnvelope>): Promise<BatchResult>;
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Same-tick call coalescing + typed batch / sticky-serve dispatch.
3
+ *
4
+ * Amdahl: serial cost is process spawn + SQLite open. Sticky serve kills spawn
5
+ * for the whole Code Mode program; batch coalescing kills it per Promise.all wave.
6
+ */
7
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ const MAX_WAVE = 32;
11
+ /**
12
+ * Wraps a host so Promise.all([asgrep.search, asgrep.defs, …]) collapses into
13
+ * one microtask wave. Prefers sticky serve → one-shot batch → overlapped spawn.
14
+ */
15
+ export function createCodemodeDispatcher(host) {
16
+ let pending = [];
17
+ let scheduled = false;
18
+ let stats = emptyStats();
19
+ const flush = async () => {
20
+ const wave = pending;
21
+ pending = [];
22
+ scheduled = false;
23
+ if (wave.length === 0)
24
+ return;
25
+ stats.waves += 1;
26
+ stats.calls += wave.length;
27
+ const waveStarted = Date.now();
28
+ try {
29
+ if (wave.length === 1) {
30
+ await settleOne(host, wave[0], stats);
31
+ return;
32
+ }
33
+ // Chunk oversized waves (batch max = 32).
34
+ for (let offset = 0; offset < wave.length; offset += MAX_WAVE) {
35
+ const chunk = wave.slice(offset, offset + MAX_WAVE);
36
+ await settleWave(host, chunk, stats);
37
+ }
38
+ }
39
+ finally {
40
+ stats.wallMs += Date.now() - waveStarted;
41
+ }
42
+ };
43
+ const enqueue = (item) => new Promise((resolve, reject) => {
44
+ item.resolve = resolve;
45
+ item.reject = reject;
46
+ pending.push(item);
47
+ if (!scheduled) {
48
+ scheduled = true;
49
+ queueMicrotask(() => {
50
+ void flush();
51
+ });
52
+ }
53
+ });
54
+ const dispatchHost = {
55
+ call(tool, args, context, options) {
56
+ const item = { tool, args, context, resolve: () => undefined, reject: () => undefined };
57
+ if (options)
58
+ item.options = options;
59
+ return enqueue(item);
60
+ },
61
+ run(args, context, options) {
62
+ // Legacy argv: still coalesce, but tool inference is best-effort only.
63
+ const tool = toolFromArgs(args);
64
+ const callArgs = argsObjectFromArgv(args);
65
+ const item = {
66
+ tool,
67
+ args: callArgs,
68
+ argv: args,
69
+ context,
70
+ resolve: () => undefined,
71
+ reject: () => undefined,
72
+ };
73
+ if (options)
74
+ item.options = options;
75
+ return enqueue(item);
76
+ },
77
+ };
78
+ return {
79
+ host: dispatchHost,
80
+ stats: () => ({ ...stats }),
81
+ resetStats: () => {
82
+ stats = emptyStats();
83
+ },
84
+ };
85
+ }
86
+ async function settleOne(host, item, stats) {
87
+ try {
88
+ if (host.sticky) {
89
+ stats.stickyCalls += 1;
90
+ item.resolve(await host.sticky.call(item.tool, item.args, item.options));
91
+ return;
92
+ }
93
+ // N=1 without sticky: direct CLI (batch-of-1 is tempfile + protocol for no gain).
94
+ const args = item.argv ?? argvFor(item.tool, item.args);
95
+ item.resolve(await host.run(args, item.context, item.options));
96
+ }
97
+ catch (err) {
98
+ item.reject(err);
99
+ }
100
+ }
101
+ async function settleWave(host, wave, stats) {
102
+ if (host.sticky) {
103
+ try {
104
+ const calls = wave.map((item, index) => ({
105
+ id: String(index),
106
+ tool: item.tool,
107
+ args: item.args,
108
+ }));
109
+ const batch = await host.sticky.batch(calls, wave[0]?.options);
110
+ stats.stickyCalls += wave.length;
111
+ settleFromBatch(wave, batch);
112
+ return;
113
+ }
114
+ catch (cause) {
115
+ // Sticky died mid-program — fall through to one-shot / spawn.
116
+ void cause;
117
+ }
118
+ }
119
+ if (host.runBatch) {
120
+ try {
121
+ const calls = wave.map((item, index) => ({
122
+ id: String(index),
123
+ tool: item.tool,
124
+ args: item.args,
125
+ }));
126
+ const batch = await host.runBatch(calls, wave[0].context, wave[0].options);
127
+ stats.batchedCalls += wave.length;
128
+ settleFromBatch(wave, batch);
129
+ return;
130
+ }
131
+ catch (cause) {
132
+ // Transport failure only — do NOT re-run when per-call ok:false.
133
+ void cause;
134
+ }
135
+ }
136
+ stats.parallelSpawnCalls += wave.length;
137
+ await Promise.all(wave.map(async (item) => {
138
+ try {
139
+ const args = item.argv ?? argvFor(item.tool, item.args);
140
+ item.resolve(await host.run(args, item.context, item.options));
141
+ }
142
+ catch (err) {
143
+ item.reject(err);
144
+ }
145
+ }));
146
+ }
147
+ function settleFromBatch(wave, batch) {
148
+ const byId = new Map(batch.results.map((r) => [r.id, r]));
149
+ for (let i = 0; i < wave.length; i++) {
150
+ const item = wave[i];
151
+ const result = byId.get(String(i));
152
+ if (!result) {
153
+ item.reject(new Error(`codemode-batch missing result id=${i}`));
154
+ continue;
155
+ }
156
+ if (!result.ok) {
157
+ item.reject(new Error(result.error ?? `codemode call ${i} failed`));
158
+ continue;
159
+ }
160
+ item.resolve(asEnvelope(result.value, item.tool));
161
+ }
162
+ }
163
+ function emptyStats() {
164
+ return {
165
+ waves: 0,
166
+ calls: 0,
167
+ batchedCalls: 0,
168
+ parallelSpawnCalls: 0,
169
+ stickyCalls: 0,
170
+ wallMs: 0,
171
+ };
172
+ }
173
+ /** Build CLI argv for spawn fallback (typed path preferred). */
174
+ export function argvFor(tool, args) {
175
+ const limit = num(args.limit, 8);
176
+ const excerpt = num(args.excerpt_lines ?? args.excerptLines, 0);
177
+ const capsule = ["--json", "--format", "agent-capsule", "--limit", String(limit), "--excerpt-lines", String(excerpt)];
178
+ switch (tool) {
179
+ case "search":
180
+ return [...capsule, String(args.query ?? ""), "."];
181
+ case "semantic":
182
+ return ["semantic", String(args.query ?? ""), ".", ...capsule];
183
+ case "chain":
184
+ return ["chain", String(args.query ?? ""), ".", "--json", "--limit", String(limit)];
185
+ case "defs":
186
+ return [...capsule, `defs:${String(args.symbol ?? "")}`, "."];
187
+ case "callers":
188
+ return [...capsule, `callers:${String(args.symbol ?? "")}`, "."];
189
+ case "imports":
190
+ return [...capsule, `imports:${String(args.module ?? "")}`, "."];
191
+ case "index_status":
192
+ return ["status", ".", "--json"];
193
+ case "index_repo":
194
+ return [args.force === true ? "reindex" : "index", ".", "--json"];
195
+ case "catalog_search":
196
+ case "catalog_describe":
197
+ // No CLI equivalent — sticky/batch only.
198
+ return ["status", ".", "--json"];
199
+ default:
200
+ return [...capsule, String(args.query ?? ""), "."];
201
+ }
202
+ }
203
+ function num(value, fallback) {
204
+ if (typeof value === "number" && Number.isFinite(value))
205
+ return Math.trunc(value);
206
+ return fallback;
207
+ }
208
+ function toolFromArgs(args) {
209
+ if (args[0] === "semantic")
210
+ return "semantic";
211
+ if (args[0] === "chain")
212
+ return "chain";
213
+ if (args[0] === "status")
214
+ return "index_status";
215
+ if (args[0] === "index" || args[0] === "reindex")
216
+ return "index_repo";
217
+ if (args[0] === "codemode-batch" || args[0] === "codemode-serve")
218
+ return "search";
219
+ const query = args.length >= 2 ? args[args.length - 2] : "";
220
+ // Only classify prefix forms when the whole query is a navigator, so
221
+ // search({ query: "defs: auth in login" }) stays search.
222
+ if (/^defs:\s*\S+$/.test(query))
223
+ return "defs";
224
+ if (/^callers:\s*\S+$/.test(query))
225
+ return "callers";
226
+ if (/^imports:\s*\S+$/.test(query))
227
+ return "imports";
228
+ return "search";
229
+ }
230
+ function argsObjectFromArgv(args) {
231
+ if (args[0] === "status")
232
+ return {};
233
+ if (args[0] === "index")
234
+ return { force: false };
235
+ if (args[0] === "reindex")
236
+ return { force: true };
237
+ if (args[0] === "semantic" || args[0] === "chain") {
238
+ const query = args[1] ?? "";
239
+ const limit = readFlag(args, "--limit");
240
+ const excerptLines = readFlag(args, "--excerpt-lines");
241
+ const out = { query };
242
+ if (limit !== undefined)
243
+ out.limit = limit;
244
+ if (excerptLines !== undefined)
245
+ out.excerpt_lines = excerptLines;
246
+ return out;
247
+ }
248
+ const query = args.length >= 2 ? args[args.length - 2] : "";
249
+ const limit = readFlag(args, "--limit");
250
+ const excerptLines = readFlag(args, "--excerpt-lines");
251
+ if (/^defs:\s*/.test(query)) {
252
+ const out = { symbol: query.replace(/^defs:\s*/, "").trim() };
253
+ if (limit !== undefined)
254
+ out.limit = limit;
255
+ if (excerptLines !== undefined)
256
+ out.excerpt_lines = excerptLines;
257
+ return out;
258
+ }
259
+ if (/^callers:\s*/.test(query)) {
260
+ const out = { symbol: query.replace(/^callers:\s*/, "").trim() };
261
+ if (limit !== undefined)
262
+ out.limit = limit;
263
+ if (excerptLines !== undefined)
264
+ out.excerpt_lines = excerptLines;
265
+ return out;
266
+ }
267
+ if (/^imports:\s*/.test(query)) {
268
+ const out = { module: query.replace(/^imports:\s*/, "").trim() };
269
+ if (limit !== undefined)
270
+ out.limit = limit;
271
+ if (excerptLines !== undefined)
272
+ out.excerpt_lines = excerptLines;
273
+ return out;
274
+ }
275
+ const out = { query, format: "capsule" };
276
+ if (limit !== undefined)
277
+ out.limit = limit;
278
+ if (excerptLines !== undefined)
279
+ out.excerpt_lines = excerptLines;
280
+ return out;
281
+ }
282
+ function readFlag(args, flag) {
283
+ const idx = args.indexOf(flag);
284
+ if (idx < 0 || idx + 1 >= args.length)
285
+ return undefined;
286
+ const n = Number(args[idx + 1]);
287
+ return Number.isFinite(n) ? n : undefined;
288
+ }
289
+ export function asEnvelope(value, command) {
290
+ if (value &&
291
+ typeof value === "object" &&
292
+ value.tool === "asgrep" &&
293
+ typeof value.ok === "boolean") {
294
+ return value;
295
+ }
296
+ const record = value && typeof value === "object" ? value : { value };
297
+ // Overrides AFTER spread so tool/ok/schema cannot be clobbered by payload fields.
298
+ return {
299
+ ...record,
300
+ tool: "asgrep",
301
+ ...(command ? { command } : {}),
302
+ schema_version: "1.0.0",
303
+ ok: true,
304
+ };
305
+ }
306
+ /** One-shot batch via stdin (no tempfile) when spawn-with-stdin is available. */
307
+ export async function runNativeBatch(run, calls, context, options, writeBatch) {
308
+ const body = JSON.stringify({
309
+ root: context.cwd,
310
+ // Auto/serial warm by default — N parallel SQLite opens are usually slower.
311
+ parallel_mode: "auto",
312
+ calls,
313
+ });
314
+ if (writeBatch) {
315
+ const envelope = await writeBatch(body, context, options);
316
+ return envelopeToBatch(envelope);
317
+ }
318
+ // Fallback: tempfile + pi.exec (no stdin).
319
+ const dir = await mkdtemp(join(tmpdir(), "asgrep-codemode-"));
320
+ const requestsPath = join(dir, "requests.json");
321
+ try {
322
+ await writeFile(requestsPath, body, "utf8");
323
+ const envelope = await run(["codemode-batch", "--requests", requestsPath, "--json"], context, options);
324
+ return envelopeToBatch(envelope);
325
+ }
326
+ finally {
327
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
328
+ }
329
+ }
330
+ function envelopeToBatch(envelope) {
331
+ const results = Array.isArray(envelope.results)
332
+ ? envelope.results
333
+ : [];
334
+ const out = { results };
335
+ if (typeof envelope.mode === "string")
336
+ out.mode = envelope.mode;
337
+ if (typeof envelope.wall_ms === "number")
338
+ out.wall_ms = envelope.wall_ms;
339
+ if (typeof envelope.all_ok === "boolean")
340
+ out.all_ok = envelope.all_ok;
341
+ return out;
342
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Code Mode for ast-sgrep in Pi.
3
+ *
4
+ * Pattern (Cloudflare / Anthropic PTC / OpenAI PTC):
5
+ * the model writes JavaScript that calls typed `asgrep.*` methods inside a
6
+ * restricted executor. Intermediate results stay in the sandbox; only the
7
+ * shaped return value re-enters the model context. Parallel calls use
8
+ * `Promise.all` and are coalesced into sticky serve / one warm batch process.
9
+ *
10
+ * This package is intentionally independent of MCP. Both MCP and Code Mode
11
+ * sit on the native ast-sgrep binary / core; they never import each other.
12
+ */
13
+ export { createAsgrepConnector, type AsgrepConnector, type ConnectorHost, type ConnectorBundle, } from "./connector.js";
14
+ export { runCodemode, normalizeCode, type CodemodeRunResult } from "./sandbox.js";
15
+ export { CODEMODE_TYPES_FOR_MODEL, type SearchArgs, type ChainArgs } from "./types.js";
16
+ export { createCodemodeDispatcher, runNativeBatch, argvFor, asEnvelope, type DispatchStats, type BatchCapableHost, type StickyWorker, type BatchResult, } from "./dispatch.js";
17
+ export { startStickyWorker, runBatchViaStdin } from "./worker.js";
18
+ export { NativeSessionPool, sharedNativePool } from "./session-pool.js";
19
+ export { loadCodemodeNative, nativeAvailable, resetNativeCache, type CodemodeNativeBinding, type NativeSession, } from "./native.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Code Mode for ast-sgrep in Pi.
3
+ *
4
+ * Pattern (Cloudflare / Anthropic PTC / OpenAI PTC):
5
+ * the model writes JavaScript that calls typed `asgrep.*` methods inside a
6
+ * restricted executor. Intermediate results stay in the sandbox; only the
7
+ * shaped return value re-enters the model context. Parallel calls use
8
+ * `Promise.all` and are coalesced into sticky serve / one warm batch process.
9
+ *
10
+ * This package is intentionally independent of MCP. Both MCP and Code Mode
11
+ * sit on the native ast-sgrep binary / core; they never import each other.
12
+ */
13
+ export { createAsgrepConnector, } from "./connector.js";
14
+ export { runCodemode, normalizeCode } from "./sandbox.js";
15
+ export { CODEMODE_TYPES_FOR_MODEL } from "./types.js";
16
+ export { createCodemodeDispatcher, runNativeBatch, argvFor, asEnvelope, } from "./dispatch.js";
17
+ export { startStickyWorker, runBatchViaStdin } from "./worker.js";
18
+ export { NativeSessionPool, sharedNativePool } from "./session-pool.js";
19
+ export { loadCodemodeNative, nativeAvailable, resetNativeCache, } from "./native.js";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Load the in-process Code Mode NAPI addon.
3
+ *
4
+ * Same model as MCP: Rust `CodeModeSession` runs inside the Node process.
5
+ * No `asgrep` CLI spawn on the hot path.
6
+ *
7
+ * Resolution order:
8
+ * 1. `ASGREP_CODEMODE_NAPI_PATH` (dev override)
9
+ * 2. `@ast-sgrep/<platform>/ast-sgrep-codemode.node` via launcher (release install)
10
+ * 3. Local `extension/native/` / cargo `target/release` (dev builds)
11
+ */
12
+ export type NativeSessionConfig = {
13
+ root?: string;
14
+ indexPath?: string;
15
+ limit?: number;
16
+ useEmbed?: boolean;
17
+ };
18
+ export type NativeBatchCall = {
19
+ id: string;
20
+ tool: string;
21
+ args?: Record<string, unknown>;
22
+ };
23
+ export type NativeBatchResult = {
24
+ id: string;
25
+ ok: boolean;
26
+ value?: unknown;
27
+ error?: string;
28
+ };
29
+ export type NativeBatchResponse = {
30
+ allOk: boolean;
31
+ results: NativeBatchResult[];
32
+ callCount: number;
33
+ wallMs: number;
34
+ mode: string;
35
+ };
36
+ export type NativeSession = {
37
+ call(tool: string, args?: Record<string, unknown>): unknown;
38
+ readonly callCount: number;
39
+ readonly root: string;
40
+ };
41
+ export type CodemodeNativeBinding = {
42
+ Session: new (config?: NativeSessionConfig) => NativeSession;
43
+ batch(request: {
44
+ root?: string;
45
+ indexPath?: string;
46
+ useEmbed?: boolean;
47
+ limit?: number;
48
+ parallelMode?: string;
49
+ calls: NativeBatchCall[];
50
+ }): NativeBatchResponse;
51
+ bindingVersion(): string;
52
+ isNative(): boolean;
53
+ };
54
+ /** Load the NAPI binding once. Returns null if unavailable on this host. */
55
+ export declare function loadCodemodeNative(): CodemodeNativeBinding | null;
56
+ export declare function nativeAvailable(): boolean;
57
+ /** Reset cache (tests). */
58
+ export declare function resetNativeCache(): void;