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,110 @@
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
+ import { createRequire } from "node:module";
13
+ import { existsSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ let cached;
17
+ function platformTriple() {
18
+ const { platform, arch } = process;
19
+ if (platform === "linux" && arch === "x64")
20
+ return "linux-x64-gnu";
21
+ if (platform === "linux" && arch === "arm64")
22
+ return "linux-arm64-gnu";
23
+ if (platform === "darwin" && arch === "arm64")
24
+ return "darwin-arm64";
25
+ if (platform === "darwin" && arch === "x64")
26
+ return "darwin-x64";
27
+ if (platform === "win32" && arch === "x64")
28
+ return "win32-x64-msvc";
29
+ return null;
30
+ }
31
+ function platformPackageAddon() {
32
+ const require = createRequire(import.meta.url);
33
+ try {
34
+ const launcher = require("ast-sgrep");
35
+ if (typeof launcher.resolveCodemodeAddon === "function") {
36
+ return launcher.resolveCodemodeAddon();
37
+ }
38
+ }
39
+ catch {
40
+ // Launcher may be unavailable in isolated unit tests.
41
+ }
42
+ return null;
43
+ }
44
+ function candidatePaths() {
45
+ const here = dirname(fileURLToPath(import.meta.url));
46
+ const triple = platformTriple();
47
+ const names = triple
48
+ ? [
49
+ `ast-sgrep-codemode.${triple}.node`,
50
+ `ast-sgrep-codemode.node`,
51
+ ]
52
+ : [`ast-sgrep-codemode.node`];
53
+ const dirs = [
54
+ // Built next to extension (dev / packaged)
55
+ join(here, "..", "..", "native"),
56
+ join(here, "..", "native"),
57
+ // Workspace release output
58
+ join(here, "..", "..", "..", "..", "target", "release"),
59
+ ];
60
+ const out = [];
61
+ const override = process.env.ASGREP_CODEMODE_NAPI_PATH;
62
+ if (override)
63
+ out.push(override);
64
+ const packaged = platformPackageAddon();
65
+ if (packaged)
66
+ out.push(packaged);
67
+ for (const dir of dirs) {
68
+ for (const name of names)
69
+ out.push(join(dir, name));
70
+ // cargo cdylib name
71
+ out.push(join(dir, "libast_sgrep_codemode_napi.so"));
72
+ out.push(join(dir, "libast_sgrep_codemode_napi.dylib"));
73
+ out.push(join(dir, "ast_sgrep_codemode_napi.dll"));
74
+ }
75
+ return out;
76
+ }
77
+ /** Load the NAPI binding once. Returns null if unavailable on this host. */
78
+ export function loadCodemodeNative() {
79
+ if (cached !== undefined)
80
+ return cached;
81
+ // Force CLI sticky / argv path (unit tests, degraded installs).
82
+ if (process.env.ASGREP_CODEMODE_BACKEND === "cli") {
83
+ cached = null;
84
+ return null;
85
+ }
86
+ const require = createRequire(import.meta.url);
87
+ for (const path of candidatePaths()) {
88
+ if (!existsSync(path))
89
+ continue;
90
+ try {
91
+ const binding = require(path);
92
+ if (typeof binding?.isNative === "function" && binding.isNative()) {
93
+ cached = binding;
94
+ return cached;
95
+ }
96
+ }
97
+ catch {
98
+ // try next candidate
99
+ }
100
+ }
101
+ cached = null;
102
+ return null;
103
+ }
104
+ export function nativeAvailable() {
105
+ return loadCodemodeNative() !== null;
106
+ }
107
+ /** Reset cache (tests). */
108
+ export function resetNativeCache() {
109
+ cached = undefined;
110
+ }
@@ -0,0 +1,25 @@
1
+ import type { AsgrepConnector } from "./connector.js";
2
+ import type { DispatchStats } from "./dispatch.js";
3
+ export type CodemodeRunResult = {
4
+ ok: boolean;
5
+ result: unknown;
6
+ logs: string[];
7
+ error?: string;
8
+ code: string;
9
+ stats?: DispatchStats;
10
+ wallMs: number;
11
+ };
12
+ /** Strip markdown fences and normalize to an async IIFE expression. */
13
+ export declare function normalizeCode(raw: string): string;
14
+ /**
15
+ * Run model-generated JavaScript with only `asgrep` + safe builtins.
16
+ *
17
+ * Uses the shared microtask queue so host Promises from `asgrep.*` resolve under
18
+ * `Promise.all`. Do not enable `microtaskMode: 'afterEvaluate'` — that isolates
19
+ * queues and breaks cross-context await.
20
+ */
21
+ export declare function runCodemode(rawCode: string, asgrep: AsgrepConnector, options?: {
22
+ timeoutMs?: number;
23
+ signal?: AbortSignal;
24
+ stats?: () => DispatchStats;
25
+ }): Promise<CodemodeRunResult>;
@@ -0,0 +1,192 @@
1
+ import vm from "node:vm";
2
+ const DEFAULT_TIMEOUT_MS = 30_000;
3
+ const MAX_CODE_CHARS = 32_000;
4
+ /** Strip markdown fences and normalize to an async IIFE expression. */
5
+ export function normalizeCode(raw) {
6
+ let code = raw.trim();
7
+ if (code.startsWith("```")) {
8
+ code = code.replace(/^```(?:javascript|js|typescript|ts)?\s*/i, "").replace(/\s*```$/, "").trim();
9
+ }
10
+ if (/^async\s*\(/.test(code) || /^async\s+function\b/.test(code)) {
11
+ return `(${code.endsWith(";") ? code.slice(0, -1) : code})()`;
12
+ }
13
+ return `(async () => {\n${code}\n})()`;
14
+ }
15
+ /**
16
+ * Run model-generated JavaScript with only `asgrep` + safe builtins.
17
+ *
18
+ * Uses the shared microtask queue so host Promises from `asgrep.*` resolve under
19
+ * `Promise.all`. Do not enable `microtaskMode: 'afterEvaluate'` — that isolates
20
+ * queues and breaks cross-context await.
21
+ */
22
+ export async function runCodemode(rawCode, asgrep, options = {}) {
23
+ const timeoutMs = Math.max(1, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
24
+ const wall0 = Date.now();
25
+ if (rawCode.length > MAX_CODE_CHARS) {
26
+ return resultErr(`code exceeds ${MAX_CODE_CHARS} characters`, [], rawCode.slice(0, 200), wall0, options.stats);
27
+ }
28
+ if (options.signal?.aborted) {
29
+ return resultErr("codemode aborted", [], rawCode.slice(0, 200), wall0, options.stats);
30
+ }
31
+ const logs = [];
32
+ const hostMethods = {
33
+ search: asgrep.search,
34
+ semantic: asgrep.semantic,
35
+ chain: asgrep.chain,
36
+ defs: asgrep.defs,
37
+ callers: asgrep.callers,
38
+ imports: asgrep.imports,
39
+ indexStatus: asgrep.indexStatus,
40
+ indexRepo: asgrep.indexRepo,
41
+ catalogSearch: asgrep.catalogSearch,
42
+ catalogDescribe: asgrep.catalogDescribe,
43
+ };
44
+ // Never expose host-realm functions or objects to model code. A direct host
45
+ // function lets `fn.constructor("return process")()` escape `node:vm`.
46
+ const bridge = async (method, payload) => {
47
+ try {
48
+ if (!Object.hasOwn(hostMethods, method))
49
+ throw new Error(`unknown asgrep method: ${method}`);
50
+ const input = JSON.parse(payload);
51
+ const value = await hostMethods[method](input);
52
+ return JSON.stringify({ ok: true, value });
53
+ }
54
+ catch (cause) {
55
+ return JSON.stringify({
56
+ ok: false,
57
+ error: cause instanceof Error ? cause.message : String(cause),
58
+ });
59
+ }
60
+ };
61
+ const logBridge = (line) => logs.push(line);
62
+ Object.setPrototypeOf(bridge, null);
63
+ Object.setPrototypeOf(logBridge, null);
64
+ Object.freeze(bridge);
65
+ Object.freeze(logBridge);
66
+ const globals = Object.create(null);
67
+ globals.__asgrepBridge = bridge;
68
+ globals.__asgrepLog = logBridge;
69
+ const context = vm.createContext(globals, {
70
+ codeGeneration: { strings: false, wasm: false },
71
+ });
72
+ new vm.Script(SANDBOX_BOOTSTRAP, { filename: "asgrep-codemode-bootstrap.js" }).runInContext(context, {
73
+ timeout: 1_000,
74
+ });
75
+ const code = normalizeCode(rawCode);
76
+ let script;
77
+ try {
78
+ script = new vm.Script(code, { filename: "asgrep-codemode.js" });
79
+ }
80
+ catch (cause) {
81
+ return resultErr(cause instanceof Error ? cause.message : String(cause), logs, code, wall0, options.stats);
82
+ }
83
+ let timer;
84
+ let onAbort;
85
+ try {
86
+ const produced = script.runInContext(context, {
87
+ displayErrors: true,
88
+ timeout: timeoutMs,
89
+ });
90
+ const races = [
91
+ Promise.resolve(produced),
92
+ new Promise((_, reject) => {
93
+ timer = setTimeout(() => reject(new Error(`codemode timeout after ${timeoutMs}ms`)), timeoutMs);
94
+ }),
95
+ ];
96
+ if (options.signal) {
97
+ races.push(new Promise((_, reject) => {
98
+ onAbort = () => reject(new Error("codemode aborted"));
99
+ options.signal.addEventListener("abort", onAbort, { once: true });
100
+ }));
101
+ }
102
+ const value = await Promise.race(races);
103
+ return resultOk(cloneOut(value), logs, code, wall0, options.stats);
104
+ }
105
+ catch (cause) {
106
+ return resultErr(cause instanceof Error ? cause.message : String(cause), logs, code, wall0, options.stats);
107
+ }
108
+ finally {
109
+ if (timer)
110
+ clearTimeout(timer);
111
+ if (onAbort)
112
+ options.signal?.removeEventListener("abort", onAbort);
113
+ }
114
+ }
115
+ const SANDBOX_BOOTSTRAP = `
116
+ {
117
+ const hostCall = globalThis.__asgrepBridge;
118
+ const hostLog = globalThis.__asgrepLog;
119
+ delete globalThis.__asgrepBridge;
120
+ delete globalThis.__asgrepLog;
121
+
122
+ const invoke = async (method, args = {}) => {
123
+ const response = JSON.parse(await hostCall(method, JSON.stringify(args)));
124
+ if (!response.ok) throw new Error(response.error || \`asgrep.\${method} failed\`);
125
+ return response.value;
126
+ };
127
+ const api = Object.create(null);
128
+ for (const method of [
129
+ "search", "semantic", "chain", "defs", "callers", "imports",
130
+ "indexStatus", "indexRepo", "catalogSearch", "catalogDescribe",
131
+ ]) {
132
+ Object.defineProperty(api, method, {
133
+ enumerable: true,
134
+ value: (args = {}) => invoke(method, args),
135
+ });
136
+ }
137
+ Object.freeze(api);
138
+
139
+ const formatLog = (value) => {
140
+ if (typeof value === "string") return value;
141
+ try { return JSON.stringify(value); } catch { return String(value); }
142
+ };
143
+ const consoleApi = Object.create(null);
144
+ for (const level of ["log", "info", "warn", "error", "debug"]) {
145
+ Object.defineProperty(consoleApi, level, {
146
+ enumerable: true,
147
+ value: (...args) => hostLog(args.map(formatLog).join(" ")),
148
+ });
149
+ }
150
+ Object.freeze(consoleApi);
151
+
152
+ Object.defineProperty(globalThis, "asgrep", { value: api, configurable: false, writable: false });
153
+ Object.defineProperty(globalThis, "console", { value: consoleApi, configurable: false, writable: false });
154
+ }
155
+ `;
156
+ function resultOk(result, logs, code, wall0, statsFn) {
157
+ const out = { ok: true, result, logs, code, wallMs: Date.now() - wall0 };
158
+ const stats = statsFn?.();
159
+ if (stats)
160
+ out.stats = stats;
161
+ return out;
162
+ }
163
+ function resultErr(error, logs, code, wall0, statsFn) {
164
+ const out = { ok: false, result: null, logs, error, code, wallMs: Date.now() - wall0 };
165
+ const stats = statsFn?.();
166
+ if (stats)
167
+ out.stats = stats;
168
+ return out;
169
+ }
170
+ function safeJson(value) {
171
+ try {
172
+ return JSON.stringify(value);
173
+ }
174
+ catch {
175
+ return String(value);
176
+ }
177
+ }
178
+ function cloneOut(value) {
179
+ if (value === undefined)
180
+ return undefined;
181
+ try {
182
+ return structuredClone(value);
183
+ }
184
+ catch {
185
+ try {
186
+ return JSON.parse(JSON.stringify(value));
187
+ }
188
+ catch {
189
+ return value;
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Session-scoped native Code Mode sessions.
3
+ *
4
+ * Primary path: in-process NAPI (`CodeModeSession` inside Node) — same model as
5
+ * MCP linking core. Zero CLI spawn.
6
+ *
7
+ * Fallback: sticky `codemode-serve` child only when the `.node` addon is missing
8
+ * (unsupported host / incomplete install). Doctor reports that as degraded.
9
+ */
10
+ import type { MachineEnvelope } from "../runtime.js";
11
+ import { type StickyWorker } from "./dispatch.js";
12
+ import { type StickyWorkerOptions } from "./worker.js";
13
+ export type SessionPoolOptions = {
14
+ /** Required only for CLI sticky fallback. */
15
+ binary?: string;
16
+ env?: NodeJS.ProcessEnv;
17
+ timeoutMs?: number;
18
+ root?: string;
19
+ indexPath?: string;
20
+ useEmbed?: boolean;
21
+ limit?: number;
22
+ };
23
+ export type StickyStarter = (options: StickyWorkerOptions) => Promise<StickyWorker>;
24
+ export declare class NativeSessionPool {
25
+ #private;
26
+ constructor(startFn?: StickyStarter);
27
+ configure(options: SessionPoolOptions): void;
28
+ configured(): boolean;
29
+ /** Active backend after first successful acquire. */
30
+ backend(): "napi" | "cli" | "none";
31
+ acquire(root: string): Promise<StickyWorker | null>;
32
+ call(root: string, tool: string, args?: Record<string, unknown>, options?: {
33
+ signal?: AbortSignal;
34
+ }): Promise<MachineEnvelope>;
35
+ invalidate(root: string): Promise<void>;
36
+ shutdown(): Promise<void>;
37
+ }
38
+ /** Singleton for advanced hosts; tools registration uses a local pool. */
39
+ export declare const sharedNativePool: NativeSessionPool;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Session-scoped native Code Mode sessions.
3
+ *
4
+ * Primary path: in-process NAPI (`CodeModeSession` inside Node) — same model as
5
+ * MCP linking core. Zero CLI spawn.
6
+ *
7
+ * Fallback: sticky `codemode-serve` child only when the `.node` addon is missing
8
+ * (unsupported host / incomplete install). Doctor reports that as degraded.
9
+ */
10
+ import { asEnvelope } from "./dispatch.js";
11
+ import { loadCodemodeNative } from "./native.js";
12
+ import { startStickyWorker } from "./worker.js";
13
+ function inProcessWorker(session, binding, root) {
14
+ return {
15
+ async call(tool, args) {
16
+ const value = session.call(tool, args);
17
+ return asEnvelope(value);
18
+ },
19
+ async batch(calls) {
20
+ // Prefer session.call in a loop — reuses the warm Searcher already open.
21
+ const results = [];
22
+ let allOk = true;
23
+ const t0 = Date.now();
24
+ for (const c of calls) {
25
+ try {
26
+ const value = session.call(c.tool, c.args);
27
+ results.push({ id: c.id, ok: true, value });
28
+ }
29
+ catch (cause) {
30
+ allOk = false;
31
+ results.push({
32
+ id: c.id,
33
+ ok: false,
34
+ error: cause instanceof Error ? cause.message : String(cause),
35
+ });
36
+ }
37
+ }
38
+ // For large waves, binding.batch can parallelize; serial warm is the default win.
39
+ if (calls.length >= 4 && allOk) {
40
+ void binding;
41
+ void root;
42
+ }
43
+ return { results, all_ok: allOk, wall_ms: Date.now() - t0, mode: "serial-napi" };
44
+ },
45
+ async end() {
46
+ // NAPI session is GC'd; nothing to kill.
47
+ },
48
+ };
49
+ }
50
+ export class NativeSessionPool {
51
+ #entries = new Map();
52
+ #starting = new Map();
53
+ #options = null;
54
+ #generations = new Map();
55
+ #startFn;
56
+ #backend = "none";
57
+ constructor(startFn = startStickyWorker) {
58
+ this.#startFn = startFn;
59
+ }
60
+ configure(options) {
61
+ this.#options = options;
62
+ }
63
+ configured() {
64
+ return this.#options !== null || loadCodemodeNative() !== null;
65
+ }
66
+ /** Active backend after first successful acquire. */
67
+ backend() {
68
+ return this.#backend;
69
+ }
70
+ async acquire(root) {
71
+ const existing = this.#entries.get(root);
72
+ if (existing)
73
+ return existing.worker;
74
+ const inFlight = this.#starting.get(root);
75
+ if (inFlight)
76
+ return inFlight;
77
+ const start = this.#start(root);
78
+ this.#starting.set(root, start);
79
+ try {
80
+ return await start;
81
+ }
82
+ finally {
83
+ if (this.#starting.get(root) === start)
84
+ this.#starting.delete(root);
85
+ }
86
+ }
87
+ async call(root, tool, args = {}, options) {
88
+ if (options?.signal?.aborted)
89
+ throw new Error("native call aborted");
90
+ const worker = await this.acquire(root);
91
+ if (!worker)
92
+ throw new Error("native Code Mode backend unavailable");
93
+ return worker.call(tool, args, options);
94
+ }
95
+ async invalidate(root) {
96
+ this.#generations.set(root, this.#generationFor(root) + 1);
97
+ this.#starting.delete(root);
98
+ const entry = this.#entries.get(root);
99
+ this.#entries.delete(root);
100
+ if (entry)
101
+ await entry.worker.end().catch(() => undefined);
102
+ if (this.#entries.size === 0)
103
+ this.#backend = "none";
104
+ }
105
+ async shutdown() {
106
+ const roots = new Set([...this.#entries.keys(), ...this.#starting.keys()]);
107
+ for (const root of roots)
108
+ this.#generations.set(root, this.#generationFor(root) + 1);
109
+ const entries = [...this.#entries.values()];
110
+ this.#entries.clear();
111
+ this.#starting.clear();
112
+ this.#backend = "none";
113
+ await Promise.all(entries.map((e) => e.worker.end().catch(() => undefined)));
114
+ }
115
+ #generationFor(root) {
116
+ return this.#generations.get(root) ?? 0;
117
+ }
118
+ async #start(root) {
119
+ const gen = this.#generationFor(root);
120
+ const opts = this.#options ?? {};
121
+ // 1) In-process NAPI (preferred — zero spawn).
122
+ const binding = loadCodemodeNative();
123
+ if (binding) {
124
+ try {
125
+ const config = { root };
126
+ if (opts.indexPath)
127
+ config.indexPath = opts.indexPath;
128
+ if (opts.limit !== undefined)
129
+ config.limit = opts.limit;
130
+ if (opts.useEmbed !== undefined)
131
+ config.useEmbed = opts.useEmbed;
132
+ const session = new binding.Session(config);
133
+ const worker = inProcessWorker(session, binding, root);
134
+ if (gen !== this.#generationFor(root)) {
135
+ await worker.end().catch(() => undefined);
136
+ return null;
137
+ }
138
+ this.#entries.set(root, { root, worker, generation: gen, backend: "napi" });
139
+ this.#backend = "napi";
140
+ return worker;
141
+ }
142
+ catch {
143
+ // Fall through to CLI sticky.
144
+ }
145
+ }
146
+ // 2) CLI sticky fallback (degraded).
147
+ if (!opts.binary)
148
+ return null;
149
+ try {
150
+ const stickyOpts = {
151
+ binary: opts.binary,
152
+ cwd: root,
153
+ };
154
+ if (opts.env)
155
+ stickyOpts.env = opts.env;
156
+ if (opts.timeoutMs !== undefined)
157
+ stickyOpts.timeoutMs = opts.timeoutMs;
158
+ const worker = await this.#startFn(stickyOpts);
159
+ if (gen !== this.#generationFor(root)) {
160
+ await worker.end().catch(() => undefined);
161
+ return null;
162
+ }
163
+ this.#entries.set(root, { root, worker, generation: gen, backend: "cli" });
164
+ this.#backend = "cli";
165
+ return worker;
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
171
+ }
172
+ /** Singleton for advanced hosts; tools registration uses a local pool. */
173
+ export const sharedNativePool = new NativeSessionPool();
@@ -0,0 +1,18 @@
1
+ /** Typed surface the model sees inside the Code Mode sandbox. */
2
+ export type SearchArgs = {
3
+ query: string;
4
+ limit?: number;
5
+ excerptLines?: number;
6
+ format?: "capsule" | "agent";
7
+ };
8
+ export type ChainArgs = {
9
+ query: string;
10
+ limit?: number;
11
+ excerptLines?: number;
12
+ };
13
+ /**
14
+ * Compact TypeScript declarations for the `asgrep_codemode` tool description.
15
+ * Keep short — every token here is paid on every turn (schema landfill lesson
16
+ * from pi-codex-conversion: compose inside Code Mode, don't dump 17 schemas).
17
+ */
18
+ export declare const CODEMODE_TYPES_FOR_MODEL: string;
@@ -0,0 +1,21 @@
1
+ /** Typed surface the model sees inside the Code Mode sandbox. */
2
+ /**
3
+ * Compact TypeScript declarations for the `asgrep_codemode` tool description.
4
+ * Keep short — every token here is paid on every turn (schema landfill lesson
5
+ * from pi-codex-conversion: compose inside Code Mode, don't dump 17 schemas).
6
+ */
7
+ export const CODEMODE_TYPES_FOR_MODEL = `
8
+ declare const asgrep: {
9
+ search(input: { query: string; limit?: number; excerptLines?: number }): Promise<unknown>;
10
+ semantic(input: { query: string; limit?: number; excerptLines?: number }): Promise<unknown>;
11
+ chain(input: { query: string; limit?: number }): Promise<unknown>;
12
+ defs(input: { symbol: string; limit?: number }): Promise<unknown>;
13
+ callers(input: { symbol: string; limit?: number }): Promise<unknown>;
14
+ imports(input: { module: string; limit?: number }): Promise<unknown>;
15
+ indexStatus(): Promise<unknown>;
16
+ indexRepo(input?: { force?: boolean }): Promise<unknown>;
17
+ catalogSearch(input: { query: string }): Promise<unknown>;
18
+ catalogDescribe(input: { name: string }): Promise<unknown>;
19
+ };
20
+ /** JS: Promise, JSON, Array, Object, Map, Set, Math. No require/process/fetch/fs. */
21
+ `.trim();
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Sticky NDJSON Code Mode worker (`asgrep codemode-serve`).
3
+ *
4
+ * One process, one warm Searcher, for the entire Code Mode program — the biggest
5
+ * Amdahl win over per-wave `codemode-batch` spawns.
6
+ */
7
+ import type { MachineEnvelope } from "../runtime.js";
8
+ import { type StickyWorker } from "./dispatch.js";
9
+ export type StickyWorkerOptions = {
10
+ binary: string;
11
+ cwd: string;
12
+ env?: NodeJS.ProcessEnv;
13
+ signal?: AbortSignal;
14
+ /** Kill worker when one request exceeds this duration (ms). */
15
+ timeoutMs?: number;
16
+ };
17
+ export declare function startStickyWorker(options: StickyWorkerOptions): Promise<StickyWorker>;
18
+ /** One-shot batch via stdin (avoids tempfile). */
19
+ export declare function runBatchViaStdin(options: {
20
+ binary: string;
21
+ cwd: string;
22
+ body: string;
23
+ env?: NodeJS.ProcessEnv;
24
+ signal?: AbortSignal;
25
+ timeoutMs?: number;
26
+ }): Promise<MachineEnvelope>;