pi-ast-sgrep 2.0.2 → 2.1.1

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.
Files changed (49) hide show
  1. package/README.md +15 -19
  2. package/dist/code-mode.d.ts +1 -1
  3. package/dist/code-mode.js +1 -1
  4. package/dist/codemode/connector.d.ts +17 -3
  5. package/dist/codemode/connector.js +59 -31
  6. package/dist/codemode/dispatch.d.ts +13 -1
  7. package/dist/codemode/dispatch.js +83 -23
  8. package/dist/codemode/guest-api.d.ts +16 -0
  9. package/dist/codemode/guest-api.js +194 -0
  10. package/dist/codemode/guest-worker.mjs +287 -0
  11. package/dist/codemode/index.d.ts +4 -3
  12. package/dist/codemode/index.js +4 -3
  13. package/dist/codemode/runner.d.ts +13 -9
  14. package/dist/codemode/runner.js +411 -213
  15. package/dist/codemode/session-pool.d.ts +6 -1
  16. package/dist/codemode/session-pool.js +125 -32
  17. package/dist/codemode/types.d.ts +33 -2
  18. package/dist/codemode/types.js +39 -13
  19. package/dist/codemode/worker.d.ts +1 -1
  20. package/dist/codemode/worker.js +25 -2
  21. package/dist/host/commands.d.ts +6 -0
  22. package/dist/host/commands.js +49 -0
  23. package/dist/host/results.d.ts +115 -0
  24. package/dist/host/results.js +80 -0
  25. package/dist/host/tools.d.ts +9 -0
  26. package/dist/host/tools.js +610 -0
  27. package/dist/index.d.ts +7 -34
  28. package/dist/index.js +5 -543
  29. package/dist/runtime/config.d.ts +36 -0
  30. package/dist/runtime/config.js +98 -0
  31. package/dist/runtime/freshness.d.ts +43 -0
  32. package/dist/runtime/freshness.js +413 -0
  33. package/dist/runtime/index-health.d.ts +16 -0
  34. package/dist/runtime/index-health.js +109 -0
  35. package/dist/runtime/runtime.d.ts +48 -0
  36. package/dist/runtime/runtime.js +265 -0
  37. package/dist/runtime/sqlite.d.ts +15 -0
  38. package/dist/runtime/sqlite.js +63 -0
  39. package/dist/runtime/types.d.ts +55 -0
  40. package/dist/runtime/types.js +25 -0
  41. package/dist/ui/card.d.ts +63 -0
  42. package/dist/ui/card.js +316 -0
  43. package/dist/{present.d.ts → ui/present.d.ts} +20 -2
  44. package/dist/{present.js → ui/present.js} +68 -9
  45. package/package.json +6 -6
  46. package/dist/codemode/sandbox-worker.d.ts +0 -1
  47. package/dist/codemode/sandbox-worker.js +0 -204
  48. package/dist/runtime.d.ts +0 -137
  49. package/dist/runtime.js +0 -799
package/README.md CHANGED
@@ -57,7 +57,7 @@ Pi can make one `asgrep` call like this:
57
57
 
58
58
  ```json
59
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}"
60
+ "code": "async () => {\n const seed = await asgrep.search('where are access tokens refreshed?', { limit: 5 });\n const hit = seed.hits?.[0];\n if (!hit) return { seed, next: seed.suggested_next };\n const [defs, window] = await Promise.all([\n asgrep.defs(hit.symbol, { limit: 5 }),\n asgrep.read({ refs: [hit.ref] }),\n ]);\n return { symbol: hit.symbol, defs: defs.hits, window };\n}"
61
61
  }
62
62
  ```
63
63
 
@@ -65,32 +65,28 @@ This workflow narrows the first result, runs independent follow-up searches toge
65
65
 
66
66
  ### Code Mode API
67
67
 
68
- The Code Mode program receives these asynchronous methods on `asgrep`:
68
+ The Code Mode program receives these asynchronous methods on `asgrep`. Positional arguments work; object forms remain valid.
69
69
 
70
70
  | Method | Use |
71
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. |
72
+ | `asgrep.search("query", { limit?, in?, lang? })` | Hybrid search: intent, symbol, or prefixed `defs:` / `callers:` / `pattern:` query. Bound a tree with `in`. |
73
+ | `asgrep.find("token")` | Lexical / identifier lookup (`word:`). Prefixed queries pass through. |
74
+ | `asgrep.defs("Symbol")` | Jump to definitions. |
75
+ | `asgrep.callers("Symbol")` | Reverse-walk callers. |
76
+ | `asgrep.read({ path, start, end }` or `{ refs }`) | Batched line windows from the index. Prefer one call with `refs`. |
77
+ | `asgrep.edit(path, oldText, newText)` | Unique string replace jailed to the project root, then targeted reindex. |
78
+ | `asgrep.indexStatus()` / `asgrep.doctor()` | Index health without a fifth outer Pi tool. |
82
79
 
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.
80
+ 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. A search with 0 hits includes `suggested_next`.
84
81
 
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.
82
+ Code Mode runs **in-process** in a restricted `node:vm` context (no Worker sandbox, no OS jail). `asgrep` and `console` are built inside the context; the host only exposes a JSON bridge and a log sink so host `Function` cannot leak. Return shapes are declared on `asgrep.*` (muscle memory). `find({ query: "blast:Symbol" })` reverse-walks callers; `blast:path/to/file.ts` uses imports. Same trust boundary as Pi `bash`. Prefer Code Mode **or** MCP for a client, never both.
86
83
 
87
84
  The bridge rejects oversized call arguments and serialized results, allows at
88
85
  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.
86
+ reaches the extension host. Raw-memory and WebAssembly globals are unavailable.
87
+ Native tool values are capped at 1 MiB each and complete batch responses at 4 MiB
88
+ before Node-API converts them into extension-host objects. These bounds do not
89
+ turn `node:vm` into an OS sandbox.
94
90
 
95
91
  ## Direct one-shot search
96
92
 
@@ -1,4 +1,4 @@
1
- import { type AstSgrepRuntime, type MachineEnvelope, type RunOptions, type RuntimeContext } from "./runtime.js";
1
+ import { type AstSgrepRuntime, type MachineEnvelope, type RunOptions, type RuntimeContext } from "./runtime/runtime.js";
2
2
  export type SgrepKind = "asgrep" | "def" | "caller" | "graph" | "anchor" | "import" | "pattern" | "embed";
3
3
  export type SgrepSignal = "exact" | "structural" | "semantic";
4
4
  export type SgrepRef = `${string}#L${number}-L${number}`;
package/dist/code-mode.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { constants } from "node:fs";
2
2
  import { lstat, open, realpath } from "node:fs/promises";
3
3
  import { isAbsolute, relative, resolve, sep } from "node:path";
4
- import { RuntimeError } from "./runtime.js";
4
+ import { RuntimeError } from "./runtime/runtime.js";
5
5
  const DEFAULT_LIMIT = 20;
6
6
  const MAX_LIMIT = 100;
7
7
  const MAX_EXCERPT_LINES = 100;
@@ -1,6 +1,6 @@
1
- import type { MachineEnvelope } from "../runtime.js";
2
- import type { ChainArgs, SearchArgs } from "./types.js";
3
- import { type BatchCapableHost, type DispatchStats } from "./dispatch.js";
1
+ import type { MachineEnvelope } from "../runtime/runtime.js";
2
+ import type { ChainArgs, EditArgs, FindArgs, ReadArgs, SearchArgs } from "./types.js";
3
+ import { type BatchCapableHost, type DispatchCall, type DispatchStats } from "./dispatch.js";
4
4
  /**
5
5
  * Spawn/CLI transport. Hosts provide argv `run` only — never a typed twin.
6
6
  * Typed entry lives solely on {@link DispatchSurface} (dispatcher output).
@@ -27,6 +27,15 @@ export type AsgrepConnector = {
27
27
  search(input: SearchArgs, options?: {
28
28
  signal?: AbortSignal;
29
29
  }): Promise<MachineEnvelope>;
30
+ find(input: FindArgs, options?: {
31
+ signal?: AbortSignal;
32
+ }): Promise<MachineEnvelope>;
33
+ read(input: ReadArgs, options?: {
34
+ signal?: AbortSignal;
35
+ }): Promise<MachineEnvelope>;
36
+ edit(input: EditArgs, options?: {
37
+ signal?: AbortSignal;
38
+ }): Promise<MachineEnvelope>;
30
39
  semantic(input: SearchArgs, options?: {
31
40
  signal?: AbortSignal;
32
41
  }): Promise<MachineEnvelope>;
@@ -73,10 +82,15 @@ export type AsgrepConnector = {
73
82
  }, options?: {
74
83
  signal?: AbortSignal;
75
84
  }): Promise<MachineEnvelope>;
85
+ doctor(options?: {
86
+ signal?: AbortSignal;
87
+ }): Promise<MachineEnvelope>;
76
88
  };
77
89
  export type ConnectorBundle = {
78
90
  asgrep: AsgrepConnector;
79
91
  stats: () => DispatchStats;
92
+ /** Per-call dispatch trace for the current run (lane + ms + ok, capped). */
93
+ trace: () => DispatchCall[];
80
94
  resetStats: () => void;
81
95
  };
82
96
  /**
@@ -1,3 +1,5 @@
1
+ import { coerceHostArgs } from "./guest-api.js";
2
+ import { defined } from "./types.js";
1
3
  import { createCodemodeDispatcher, } from "./dispatch.js";
2
4
  const DEFAULT_LIMIT = 8;
3
5
  function clampLimit(limit) {
@@ -32,48 +34,74 @@ export function createAsgrepConnector(host, context, options = {}) {
32
34
  return { signal: combined };
33
35
  };
34
36
  const call = (tool, args, signal) => dispatcher.host.call(tool, args, context, callOptions(signal));
35
- // Bound function properties (not methods) so vm call sites cannot lose `this`.
36
- const asgrep = {
37
- search: (input, callOptions) => call("search", {
38
- query: input.query,
39
- limit: clampLimit(input.limit),
40
- excerpt_lines: clampExcerpt(input.excerptLines),
41
- format: input.format === "agent" ? "agent" : "capsule",
42
- }, callOptions?.signal),
43
- semantic: (input, callOptions) => call("semantic", {
44
- query: input.query,
37
+ const searchPayload = (method, input) => {
38
+ const scoped = coerceHostArgs(method, { ...input });
39
+ const payload = {
40
+ query: scoped.query,
45
41
  limit: clampLimit(input.limit),
46
42
  excerpt_lines: clampExcerpt(input.excerptLines),
47
43
  format: input.format === "agent" ? "agent" : "capsule",
48
- }, callOptions?.signal),
49
- chain: (input, callOptions) => call("chain", {
50
- query: input.query,
51
- limit: clampLimit(input.limit),
52
- top_n: 20,
53
- }, callOptions?.signal),
54
- defs: (input, callOptions) => call("defs", {
55
- symbol: input.symbol,
56
- limit: clampLimit(input.limit),
57
- excerpt_lines: clampExcerpt(input.excerptLines),
58
- }, callOptions?.signal),
59
- callers: (input, callOptions) => call("callers", {
60
- symbol: input.symbol,
61
- limit: clampLimit(input.limit),
62
- excerpt_lines: clampExcerpt(input.excerptLines),
63
- }, callOptions?.signal),
64
- imports: (input, callOptions) => call("imports", {
65
- module: input.module,
66
- limit: clampLimit(input.limit),
67
- excerpt_lines: clampExcerpt(input.excerptLines),
68
- }, callOptions?.signal),
44
+ };
45
+ if (typeof scoped.lang === "string" && scoped.lang.trim())
46
+ payload.lang = scoped.lang.trim();
47
+ return payload;
48
+ };
49
+ // Bound function properties (not methods) so vm call sites cannot lose `this`.
50
+ const asgrep = {
51
+ search: (input, callOptions) => call("search", searchPayload("search", input), callOptions?.signal),
52
+ find: (input, callOptions) => call("find", searchPayload("find", input), callOptions?.signal),
53
+ read: (input, callOptions) => call("read", defined({
54
+ path: input.path,
55
+ start: input.start,
56
+ end: input.end,
57
+ ref: input.ref,
58
+ refs: input.refs,
59
+ context_lines: input.contextLines,
60
+ max_chars: input.maxChars,
61
+ }), callOptions?.signal),
62
+ edit: (input, callOptions) => {
63
+ // Multi-edit wire contract: every entry carries its own path; the
64
+ // top-level path is the default for entries that omit it.
65
+ const edits = input.edits?.map((entry) => ({
66
+ ...(typeof input.path === "string" ? { path: input.path } : {}),
67
+ ...entry,
68
+ }));
69
+ return call("edit", defined({
70
+ path: input.path,
71
+ oldText: input.oldText,
72
+ newText: input.newText,
73
+ edits,
74
+ }), callOptions?.signal);
75
+ },
76
+ semantic: (input, callOptions) => call("semantic", searchPayload("semantic", input), callOptions?.signal),
77
+ chain: (input, callOptions) => call("chain", { query: input.query, limit: clampLimit(input.limit), top_n: 20 }, callOptions?.signal),
78
+ defs: (input, callOptions) => {
79
+ const scoped = coerceHostArgs("defs", { ...input });
80
+ return call("defs", {
81
+ symbol: scoped.symbol,
82
+ limit: clampLimit(input.limit),
83
+ excerpt_lines: clampExcerpt(input.excerptLines),
84
+ }, callOptions?.signal);
85
+ },
86
+ callers: (input, callOptions) => {
87
+ const scoped = coerceHostArgs("callers", { ...input });
88
+ return call("callers", {
89
+ symbol: scoped.symbol,
90
+ limit: clampLimit(input.limit),
91
+ excerpt_lines: clampExcerpt(input.excerptLines),
92
+ }, callOptions?.signal);
93
+ },
94
+ imports: (input, callOptions) => call("imports", defined({ module: input.module, limit: clampLimit(input.limit), excerpt_lines: clampExcerpt(input.excerptLines) }), callOptions?.signal),
69
95
  indexStatus: (callOptions) => call("index_status", {}, callOptions?.signal),
70
96
  indexRepo: (input = {}, callOptions) => call("index_repo", { force: input.force === true }, callOptions?.signal),
71
97
  catalogSearch: (input, callOptions) => call("catalog_search", { query: input.query }, callOptions?.signal),
72
98
  catalogDescribe: (input, callOptions) => call("catalog_describe", { name: input.name }, callOptions?.signal),
99
+ doctor: (callOptions) => host.run(["doctor", ".", "--json"], context, callOptions),
73
100
  };
74
101
  return {
75
102
  asgrep,
76
103
  stats: dispatcher.stats,
104
+ trace: dispatcher.trace,
77
105
  resetStats: dispatcher.resetStats,
78
106
  };
79
107
  }
@@ -4,7 +4,7 @@
4
4
  * Amdahl: serial cost is process spawn + SQLite open. Sticky serve kills spawn
5
5
  * for the whole Code Mode program; batch coalescing kills it per Promise.all wave.
6
6
  */
7
- import type { MachineEnvelope } from "../runtime.js";
7
+ import type { MachineEnvelope } from "../runtime/runtime.js";
8
8
  import type { ConnectorHost, DispatchSurface } from "./connector.js";
9
9
  export type CodemodeToolCall = {
10
10
  tool: string;
@@ -18,6 +18,15 @@ export type DispatchStats = {
18
18
  stickyCalls: number;
19
19
  wallMs: number;
20
20
  };
21
+ /** One settled host call: which lane carried it and how long it took. */
22
+ export type DispatchCall = {
23
+ tool: string;
24
+ /** Short display target: query/symbol/module/path, whichever the call used. */
25
+ target: string;
26
+ lane: "sticky" | "batch" | "spawn" | "serial";
27
+ ok: boolean;
28
+ ms: number;
29
+ };
21
30
  export type BatchResult = {
22
31
  results: Array<{
23
32
  id: string;
@@ -41,6 +50,8 @@ export type StickyWorker = {
41
50
  signal?: AbortSignal;
42
51
  }): Promise<BatchResult>;
43
52
  end(): Promise<void>;
53
+ /** True after timeout/crash/end. Missing means "assume live". */
54
+ closed?: () => boolean;
44
55
  };
45
56
  export type BatchCapableHost = ConnectorHost & {
46
57
  /** One-shot warm batch (codemode-batch). */
@@ -63,6 +74,7 @@ export type BatchCapableHost = ConnectorHost & {
63
74
  export declare function createCodemodeDispatcher(host: BatchCapableHost): {
64
75
  host: DispatchSurface;
65
76
  stats: () => DispatchStats;
77
+ trace: () => DispatchCall[];
66
78
  resetStats: () => void;
67
79
  };
68
80
  export declare function argvFor(tool: string, args: Record<string, unknown>): string[];
@@ -7,8 +7,16 @@
7
7
  import { mkdtemp, writeFile, rm } from "node:fs/promises";
8
8
  import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
+ function callTarget(args) {
11
+ const refs = args.refs ?? args.ref;
12
+ const value = args.query ?? args.symbol ?? args.module ?? args.path ?? args.name
13
+ ?? (Array.isArray(refs) ? refs.map((r) => typeof r === "string" ? r : r?.path ?? "").filter(Boolean).join(", ")
14
+ : typeof refs === "string" ? refs : "");
15
+ const text = String(value).replace(/\s+/g, " ").trim();
16
+ return text.length > 60 ? text.slice(0, 59) + "…" : text;
17
+ }
10
18
  const MAX_WAVE = 32;
11
- const MUTATING_TOOLS = new Set(["index_repo"]);
19
+ const MUTATING_TOOLS = new Set(["index_repo", "edit"]);
12
20
  const abortError = () => Object.assign(new Error("codemode aborted"), { name: "AbortError" });
13
21
  function rejectWave(wave, cause) {
14
22
  for (const item of wave)
@@ -30,6 +38,21 @@ export function createCodemodeDispatcher(host) {
30
38
  let pending = [];
31
39
  let scheduled = false;
32
40
  let stats = emptyStats();
41
+ // Per-run call log: which lane each call took and how long it took. Bounded;
42
+ // surfaced in codemode result details so callers can see the dispatch shape.
43
+ const calls = [];
44
+ const TRACE_CAP = 128;
45
+ const recordCall = (item, ok) => {
46
+ if (calls.length >= TRACE_CAP)
47
+ return;
48
+ calls.push({ tool: item.tool, target: callTarget(item.args), lane: item.lane, ok, ms: Date.now() - item.startedAt });
49
+ };
50
+ // Mutations never batch and never overlap each other: edit/index_repo run on
51
+ // a serial tail so Promise.all([edit, edit]) cannot interleave writes, and a
52
+ // later wave's mutation queues behind an earlier one. Reads keep wave
53
+ // batching; ordering vs a concurrent write is intentionally unspecified
54
+ // (same semantics as today's shared batch — the store stays transactional).
55
+ let mutationTail = Promise.resolve();
33
56
  const flush = async () => {
34
57
  const wave = pending.filter((item) => !item.settled);
35
58
  pending = [];
@@ -39,18 +62,33 @@ export function createCodemodeDispatcher(host) {
39
62
  stats.waves += 1;
40
63
  stats.calls += wave.length;
41
64
  const waveStarted = Date.now();
65
+ const mutations = wave.filter((item) => MUTATING_TOOLS.has(item.tool) && !item.settled);
66
+ const reads = wave.filter((item) => !MUTATING_TOOLS.has(item.tool) && !item.settled);
67
+ let writesDone = mutationTail;
68
+ if (mutations.length > 0) {
69
+ writesDone = mutationTail.then(async () => {
70
+ for (const item of mutations) {
71
+ if (!item.settled)
72
+ await settleOne(host, item, stats);
73
+ }
74
+ });
75
+ mutationTail = writesDone;
76
+ }
42
77
  try {
43
- if (wave.length === 1) {
44
- await settleOne(host, wave[0], stats);
45
- return;
78
+ if (reads.length === 1) {
79
+ await settleOne(host, reads[0], stats);
46
80
  }
47
- // Chunk oversized waves (batch max = 32).
48
- for (let offset = 0; offset < wave.length; offset += MAX_WAVE) {
49
- const chunk = wave.slice(offset, offset + MAX_WAVE).filter((item) => !item.settled);
50
- if (chunk.length === 0)
51
- continue;
52
- await settleWave(host, chunk, stats);
81
+ else if (reads.length > 1) {
82
+ // Chunk oversized waves (batch max = 32).
83
+ for (let offset = 0; offset < reads.length; offset += MAX_WAVE) {
84
+ const chunk = reads.slice(offset, offset + MAX_WAVE).filter((item) => !item.settled);
85
+ if (chunk.length === 0)
86
+ continue;
87
+ await settleWave(host, chunk, stats);
88
+ }
53
89
  }
90
+ // Reads resolve independently; the wave is not done until queued writes land.
91
+ await writesDone;
54
92
  }
55
93
  finally {
56
94
  stats.wallMs += Date.now() - waveStarted;
@@ -64,6 +102,7 @@ export function createCodemodeDispatcher(host) {
64
102
  return;
65
103
  item.settled = true;
66
104
  cleanup();
105
+ recordCall(item, true);
67
106
  resolve(value);
68
107
  };
69
108
  item.reject = (reason) => {
@@ -71,6 +110,7 @@ export function createCodemodeDispatcher(host) {
71
110
  return;
72
111
  item.settled = true;
73
112
  cleanup();
113
+ recordCall(item, false);
74
114
  reject(reason);
75
115
  };
76
116
  const onAbort = () => item.reject(abortError());
@@ -94,6 +134,8 @@ export function createCodemodeDispatcher(host) {
94
134
  args,
95
135
  context,
96
136
  settled: false,
137
+ startedAt: Date.now(),
138
+ lane: MUTATING_TOOLS.has(tool) ? "serial" : "spawn",
97
139
  resolve: () => undefined,
98
140
  reject: () => undefined,
99
141
  };
@@ -105,14 +147,17 @@ export function createCodemodeDispatcher(host) {
105
147
  return {
106
148
  host: dispatchHost,
107
149
  stats: () => ({ ...stats }),
150
+ trace: () => calls.slice(),
108
151
  resetStats: () => {
109
152
  stats = emptyStats();
153
+ calls.length = 0;
110
154
  },
111
155
  };
112
156
  }
113
157
  async function settleOne(host, item, stats) {
114
158
  try {
115
159
  if (host.sticky) {
160
+ item.lane = "sticky";
116
161
  stats.stickyCalls += 1;
117
162
  item.resolve(await host.sticky.call(item.tool, item.args, item.options));
118
163
  return;
@@ -125,17 +170,18 @@ async function settleOne(host, item, stats) {
125
170
  item.reject(err);
126
171
  }
127
172
  }
173
+ /** Pending[] -> wire calls ({id, tool, args}) shared by both batch transports. */
174
+ function packCalls(wave) {
175
+ return wave.map((item, index) => ({ id: String(index), tool: item.tool, args: item.args }));
176
+ }
128
177
  async function settleWave(host, wave, stats) {
129
178
  if (host.sticky) {
130
179
  const transportOptions = sharedBatchOptions(wave);
131
180
  try {
132
- const calls = wave.map((item, index) => ({
133
- id: String(index),
134
- tool: item.tool,
135
- args: item.args,
136
- }));
137
- const batch = await host.sticky.batch(calls, transportOptions);
181
+ const batch = await host.sticky.batch(packCalls(wave), transportOptions);
138
182
  stats.stickyCalls += wave.length;
183
+ for (const item of wave)
184
+ item.lane = "sticky";
139
185
  settleFromBatch(wave, batch);
140
186
  return;
141
187
  }
@@ -159,13 +205,10 @@ async function settleWave(host, wave, stats) {
159
205
  if (host.runBatch) {
160
206
  const transportOptions = sharedBatchOptions(batchWave);
161
207
  try {
162
- const calls = batchWave.map((item, index) => ({
163
- id: String(index),
164
- tool: item.tool,
165
- args: item.args,
166
- }));
167
- const batch = await host.runBatch(calls, batchWave[0].context, transportOptions);
208
+ const batch = await host.runBatch(packCalls(batchWave), batchWave[0].context, transportOptions);
168
209
  stats.batchedCalls += batchWave.length;
210
+ for (const item of batchWave)
211
+ item.lane = "batch";
169
212
  settleFromBatch(batchWave, batch);
170
213
  return;
171
214
  }
@@ -221,6 +264,7 @@ function emptyStats() {
221
264
  }
222
265
  const ARGV_SPEC = {
223
266
  search: { form: "capsule", key: "query" },
267
+ find: { form: "find" },
224
268
  semantic: { form: "semantic" },
225
269
  chain: { form: "chain" },
226
270
  defs: { form: "capsule", key: "symbol", prefix: "defs" },
@@ -250,15 +294,31 @@ export function argvFor(tool, args) {
250
294
  return ["chain", argStr(args, "query"), ".", "--json", "--limit", String(limit)];
251
295
  }
252
296
  const excerpt = num(args.excerpt_lines ?? args.excerptLines, 0);
253
- const capsule = ["--json", "--format", "agent-capsule", "--limit", String(limit), "--excerpt-lines", String(excerpt)];
297
+ const capsule = withLang(["--json", "--format", "agent-capsule", "--limit", String(limit), "--excerpt-lines", String(excerpt)], args);
254
298
  if (spec.form === "semantic") {
255
299
  return ["semantic", argStr(args, "query"), ".", ...capsule];
256
300
  }
301
+ if (spec.form === "find") {
302
+ const raw = argStr(args, "query").trim();
303
+ let token = raw;
304
+ if (/^blast:/i.test(raw)) {
305
+ const target = raw.slice(raw.indexOf(":") + 1).trim();
306
+ token = /[\\/.]/.test(target) ? `imports:${target}` : `callers:${target}`;
307
+ }
308
+ else if (!/^(defs|callers|imports|literal|regex|word|pattern):/i.test(raw)) {
309
+ token = `word:${raw}`;
310
+ }
311
+ return [...capsule, token, "."];
312
+ }
257
313
  // capsule (+ optional prefix for defs/callers/imports)
258
314
  const raw = argStr(args, spec.key);
259
315
  const token = spec.prefix ? `${spec.prefix}:${raw}` : raw;
260
316
  return [...capsule, token, "."];
261
317
  }
318
+ function withLang(argv, args) {
319
+ const lang = typeof args.lang === "string" ? args.lang.trim() : "";
320
+ return lang ? ["--lang", lang, ...argv] : argv;
321
+ }
262
322
  function num(value, fallback) {
263
323
  if (typeof value === "number" && Number.isFinite(value))
264
324
  return Math.trunc(value);
@@ -0,0 +1,16 @@
1
+ /** Guest-call packing so the first shape a model tries actually works. */
2
+ import { type CodemodeHostMethod } from "./types.js";
3
+ export declare function resolveHostMethod(method: string): CodemodeHostMethod | undefined;
4
+ /** Turn positional guest calls into the host object shape. */
5
+ export declare function packGuestCall(method: string, args: unknown[]): Record<string, unknown>;
6
+ /** Accept query as a symbol alias and fold in:/fileFilter into the query string. */
7
+ export declare function coerceHostArgs(method: string, input: Record<string, unknown>): Record<string, unknown>;
8
+ export declare function applyQueryScope(query: string, args: Record<string, unknown>): string | undefined;
9
+ export declare function unknownMethodError(method: string): string;
10
+ export declare function timeoutHint(message: string): string;
11
+ /**
12
+ * Strip fences and invoke a function expression, including non-async arrows.
13
+ * A single expression with no `return` is returned automatically.
14
+ * Bare statements still wrap in an async IIFE.
15
+ */
16
+ export declare function normalizeCode(raw: string): string;