@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.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 (191) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/types/cli/file-processor.d.ts +1 -1
  3. package/dist/types/config/settings-schema.d.ts +45 -3
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/debug/raw-sse.d.ts +2 -0
  6. package/dist/types/edit/file-read-cache.d.ts +15 -4
  7. package/dist/types/edit/index.d.ts +3 -8
  8. package/dist/types/edit/renderer.d.ts +1 -2
  9. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  10. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  11. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  12. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  13. package/dist/types/eval/py/executor.d.ts +1 -2
  14. package/dist/types/eval/py/kernel.d.ts +6 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  16. package/dist/types/eval/session-id.d.ts +3 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  18. package/dist/types/hashline/anchors.d.ts +15 -9
  19. package/dist/types/hashline/constants.d.ts +0 -2
  20. package/dist/types/hashline/diff.d.ts +1 -2
  21. package/dist/types/hashline/executor.d.ts +52 -0
  22. package/dist/types/hashline/hash.d.ts +44 -93
  23. package/dist/types/hashline/index.d.ts +2 -1
  24. package/dist/types/hashline/input.d.ts +2 -9
  25. package/dist/types/hashline/recovery.d.ts +3 -9
  26. package/dist/types/hashline/tokenizer.d.ts +91 -0
  27. package/dist/types/hashline/types.d.ts +5 -7
  28. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  29. package/dist/types/modes/types.d.ts +1 -0
  30. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +2 -0
  32. package/dist/types/session/agent-session.d.ts +11 -15
  33. package/dist/types/session/agent-storage.d.ts +11 -10
  34. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  35. package/dist/types/slash-commands/types.d.ts +0 -5
  36. package/dist/types/task/executor.d.ts +2 -0
  37. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  38. package/dist/types/tools/index.d.ts +2 -8
  39. package/dist/types/tools/match-line-format.d.ts +4 -4
  40. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  41. package/dist/types/tools/review.d.ts +13 -0
  42. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  43. package/dist/types/tools/search.d.ts +4 -3
  44. package/dist/types/utils/edit-mode.d.ts +1 -1
  45. package/dist/types/web/kagi.d.ts +4 -2
  46. package/dist/types/web/parallel.d.ts +4 -3
  47. package/dist/types/web/scrapers/types.d.ts +2 -1
  48. package/dist/types/web/search/index.d.ts +12 -4
  49. package/dist/types/web/search/provider.d.ts +2 -1
  50. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  51. package/dist/types/web/search/providers/base.d.ts +34 -2
  52. package/dist/types/web/search/providers/brave.d.ts +8 -1
  53. package/dist/types/web/search/providers/codex.d.ts +13 -9
  54. package/dist/types/web/search/providers/exa.d.ts +10 -1
  55. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  56. package/dist/types/web/search/providers/jina.d.ts +2 -1
  57. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  58. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  59. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  60. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  61. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  62. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  63. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  64. package/dist/types/web/search/providers/utils.d.ts +8 -6
  65. package/dist/types/web/search/providers/zai.d.ts +12 -3
  66. package/package.json +7 -7
  67. package/src/cli/file-processor.ts +12 -2
  68. package/src/cli.ts +0 -8
  69. package/src/commands/commit.ts +8 -8
  70. package/src/config/prompt-templates.ts +6 -6
  71. package/src/config/settings-schema.ts +47 -3
  72. package/src/config/settings.ts +5 -5
  73. package/src/debug/raw-sse.ts +68 -3
  74. package/src/edit/file-read-cache.ts +68 -25
  75. package/src/edit/index.ts +6 -37
  76. package/src/edit/renderer.ts +9 -47
  77. package/src/edit/streaming.ts +43 -56
  78. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  79. package/src/eval/js/context-manager.ts +64 -53
  80. package/src/eval/js/shared/local-module-loader.ts +265 -0
  81. package/src/eval/js/shared/prelude.txt +4 -0
  82. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  83. package/src/eval/js/shared/runtime.ts +129 -86
  84. package/src/eval/js/worker-core.ts +23 -38
  85. package/src/eval/py/executor.ts +155 -84
  86. package/src/eval/py/kernel.ts +10 -1
  87. package/src/eval/py/prelude.py +22 -24
  88. package/src/eval/py/runner.py +203 -85
  89. package/src/eval/py/tool-bridge.ts +17 -10
  90. package/src/eval/session-id.ts +8 -0
  91. package/src/exec/bash-executor.ts +27 -16
  92. package/src/extensibility/extensions/runner.ts +0 -1
  93. package/src/extensibility/extensions/types.ts +1 -3
  94. package/src/hashline/anchors.ts +56 -65
  95. package/src/hashline/apply.ts +29 -31
  96. package/src/hashline/constants.ts +0 -3
  97. package/src/hashline/diff-preview.ts +4 -5
  98. package/src/hashline/diff.ts +30 -4
  99. package/src/hashline/execute.ts +91 -26
  100. package/src/hashline/executor.ts +239 -0
  101. package/src/hashline/grammar.lark +12 -10
  102. package/src/hashline/hash.ts +69 -114
  103. package/src/hashline/index.ts +2 -1
  104. package/src/hashline/input.ts +48 -41
  105. package/src/hashline/prefixes.ts +21 -11
  106. package/src/hashline/recovery.ts +63 -71
  107. package/src/hashline/stream.ts +2 -2
  108. package/src/hashline/tokenizer.ts +467 -0
  109. package/src/hashline/types.ts +6 -8
  110. package/src/internal-urls/docs-index.generated.ts +7 -7
  111. package/src/modes/components/extensions/types.ts +0 -5
  112. package/src/modes/components/session-observer-overlay.ts +11 -2
  113. package/src/modes/components/tree-selector.ts +10 -2
  114. package/src/modes/controllers/command-controller.ts +1 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  116. package/src/modes/controllers/selector-controller.ts +5 -5
  117. package/src/modes/types.ts +4 -1
  118. package/src/modes/utils/ui-helpers.ts +4 -0
  119. package/src/prompts/agents/explore.md +1 -1
  120. package/src/prompts/tools/ast-edit.md +1 -1
  121. package/src/prompts/tools/ast-grep.md +1 -1
  122. package/src/prompts/tools/eval.md +1 -1
  123. package/src/prompts/tools/hashline.md +73 -94
  124. package/src/prompts/tools/read.md +4 -4
  125. package/src/prompts/tools/search.md +3 -3
  126. package/src/sdk.ts +17 -23
  127. package/src/session/agent-session.ts +59 -66
  128. package/src/session/agent-storage.ts +13 -14
  129. package/src/slash-commands/acp-builtins.ts +3 -3
  130. package/src/slash-commands/types.ts +0 -6
  131. package/src/task/executor.ts +26 -57
  132. package/src/task/index.ts +8 -4
  133. package/src/tool-discovery/tool-index.ts +0 -134
  134. package/src/tools/ast-edit.ts +36 -13
  135. package/src/tools/ast-grep.ts +45 -4
  136. package/src/tools/browser/tab-worker.ts +3 -2
  137. package/src/tools/eval.ts +2 -1
  138. package/src/tools/fetch.ts +23 -14
  139. package/src/tools/index.ts +2 -8
  140. package/src/tools/irc.ts +59 -5
  141. package/src/tools/match-line-format.ts +5 -7
  142. package/src/tools/output-schema-validator.ts +132 -0
  143. package/src/tools/read.ts +142 -31
  144. package/src/tools/review.ts +23 -0
  145. package/src/tools/search-tool-bm25.ts +3 -30
  146. package/src/tools/search.ts +48 -16
  147. package/src/tools/write.ts +3 -3
  148. package/src/tools/yield.ts +32 -41
  149. package/src/utils/edit-mode.ts +1 -2
  150. package/src/utils/file-mentions.ts +2 -2
  151. package/src/web/kagi.ts +15 -6
  152. package/src/web/parallel.ts +9 -6
  153. package/src/web/scrapers/types.ts +7 -1
  154. package/src/web/scrapers/youtube.ts +13 -7
  155. package/src/web/search/index.ts +37 -11
  156. package/src/web/search/provider.ts +5 -3
  157. package/src/web/search/providers/anthropic.ts +30 -21
  158. package/src/web/search/providers/base.ts +35 -2
  159. package/src/web/search/providers/brave.ts +4 -4
  160. package/src/web/search/providers/codex.ts +118 -89
  161. package/src/web/search/providers/exa.ts +3 -2
  162. package/src/web/search/providers/gemini.ts +58 -155
  163. package/src/web/search/providers/jina.ts +4 -4
  164. package/src/web/search/providers/kagi.ts +17 -11
  165. package/src/web/search/providers/kimi.ts +29 -13
  166. package/src/web/search/providers/parallel.ts +171 -23
  167. package/src/web/search/providers/perplexity.ts +38 -37
  168. package/src/web/search/providers/searxng.ts +3 -1
  169. package/src/web/search/providers/synthetic.ts +16 -19
  170. package/src/web/search/providers/tavily.ts +23 -18
  171. package/src/web/search/providers/utils.ts +11 -17
  172. package/src/web/search/providers/zai.ts +16 -8
  173. package/dist/types/hashline/parser.d.ts +0 -7
  174. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  175. package/dist/types/tools/vim.d.ts +0 -58
  176. package/dist/types/vim/buffer.d.ts +0 -41
  177. package/dist/types/vim/commands.d.ts +0 -6
  178. package/dist/types/vim/engine.d.ts +0 -47
  179. package/dist/types/vim/parser.d.ts +0 -3
  180. package/dist/types/vim/render.d.ts +0 -25
  181. package/dist/types/vim/types.d.ts +0 -182
  182. package/src/hashline/parser.ts +0 -246
  183. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  184. package/src/prompts/tools/vim.md +0 -98
  185. package/src/tools/vim.ts +0 -949
  186. package/src/vim/buffer.ts +0 -309
  187. package/src/vim/commands.ts +0 -382
  188. package/src/vim/engine.ts +0 -2409
  189. package/src/vim/parser.ts +0 -134
  190. package/src/vim/render.ts +0 -252
  191. package/src/vim/types.ts +0 -197
@@ -0,0 +1,265 @@
1
+ import * as fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import * as vm from "node:vm";
6
+ import { collectModuleSourceSpecifiers, stripTypeScriptSyntax } from "./rewrite-imports";
7
+
8
+ interface LocalModuleEntry {
9
+ version: number;
10
+ identifier: string;
11
+ module: vm.SourceTextModule;
12
+ }
13
+
14
+ export type LocalImportResolution = { mode: "local"; value: unknown } | { mode: "external"; target: string };
15
+
16
+ const LOCAL_MODULE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx", ".mts"]);
17
+
18
+ export class LocalModuleLoader {
19
+ #context: vm.Context;
20
+ #sessionTag: string;
21
+ #moduleMtimes = new Map<string, number>();
22
+ #moduleDeps = new Map<string, Set<string>>();
23
+ #moduleParents = new Map<string, Set<string>>();
24
+ #moduleVersions = new Map<string, number>();
25
+ #moduleEntries = new Map<string, LocalModuleEntry>();
26
+ #moduleBuilds = new Map<string, Promise<LocalModuleEntry>>();
27
+ #externalModules = new Map<string, Promise<vm.Module>>();
28
+ #requireCache = new Map<string, NodeJS.Require>();
29
+
30
+ constructor(sessionId: string) {
31
+ this.#context = vm.createContext(globalThis);
32
+ this.#sessionTag = Bun.hash(sessionId).toString(16);
33
+ }
34
+
35
+ async resolveForRun(cwd: string, source: string): Promise<LocalImportResolution> {
36
+ this.#refreshTrackedLocalModules();
37
+ return await this.#resolveFromBase(cwd, source);
38
+ }
39
+
40
+ async resolveForModule(moduleUrl: string, source: string, cwd: string): Promise<LocalImportResolution> {
41
+ this.#refreshTrackedLocalModules();
42
+ const modulePath = this.filenameForUrl(moduleUrl);
43
+ const baseDir = modulePath ? path.dirname(modulePath) : cwd;
44
+ return await this.#resolveFromBase(baseDir, source);
45
+ }
46
+
47
+ requireForFile(moduleUrlOrPath: string | undefined, cwd: string): NodeJS.Require {
48
+ const basePath = this.filenameForUrl(moduleUrlOrPath) ?? path.join(cwd, "[eval]");
49
+ let cached = this.#requireCache.get(basePath);
50
+ if (!cached) {
51
+ cached = buildRequire(basePath);
52
+ this.#requireCache.set(basePath, cached);
53
+ }
54
+ return cached;
55
+ }
56
+
57
+ filenameForUrl(moduleUrlOrPath: string | undefined): string | null {
58
+ if (!moduleUrlOrPath) return null;
59
+ if (moduleUrlOrPath.startsWith("file://")) return fileURLToPath(moduleUrlOrPath);
60
+ return path.isAbsolute(moduleUrlOrPath) ? moduleUrlOrPath : null;
61
+ }
62
+
63
+ dirnameForUrl(moduleUrlOrPath: string | undefined, cwd: string): string {
64
+ const filename = this.filenameForUrl(moduleUrlOrPath);
65
+ return filename ? path.dirname(filename) : cwd;
66
+ }
67
+
68
+ async #resolveFromBase(baseDir: string, source: string): Promise<LocalImportResolution> {
69
+ const resolved = resolveImportSpecifier(baseDir, source);
70
+ if (isLocalPathSpecifier(source) && isManagedLocalModulePath(resolved)) {
71
+ const entry = await this.#ensureLocalModule(resolved);
72
+ return { mode: "local", value: entry.module.namespace };
73
+ }
74
+ return { mode: "external", target: normalizeImportTarget(resolved) };
75
+ }
76
+
77
+ async #ensureLocalModule(modulePath: string): Promise<LocalModuleEntry> {
78
+ const existing = this.#moduleEntries.get(modulePath);
79
+ if (existing) return existing;
80
+ const building = this.#moduleBuilds.get(modulePath);
81
+ if (building) return await building;
82
+ const buildPromise = this.#buildLocalModule(modulePath).finally(() => {
83
+ if (this.#moduleBuilds.get(modulePath) === buildPromise) this.#moduleBuilds.delete(modulePath);
84
+ });
85
+ this.#moduleBuilds.set(modulePath, buildPromise);
86
+ return await buildPromise;
87
+ }
88
+
89
+ async #buildLocalModule(modulePath: string): Promise<LocalModuleEntry> {
90
+ const rawSource = fs.readFileSync(modulePath, "utf8");
91
+ const stripped = stripTypeScriptSyntax(rawSource);
92
+ const moduleDir = path.dirname(modulePath);
93
+ const localDeps = new Set<string>();
94
+ for (const specifier of collectModuleSourceSpecifiers(stripped)) {
95
+ const resolved = resolveImportSpecifier(moduleDir, specifier);
96
+ if (isLocalPathSpecifier(specifier) && isManagedLocalModulePath(resolved)) {
97
+ localDeps.add(resolved);
98
+ }
99
+ }
100
+ this.#setModuleDependencies(modulePath, localDeps);
101
+ this.#moduleMtimes.set(modulePath, fs.statSync(modulePath).mtimeMs);
102
+ const version = this.#moduleVersions.get(modulePath) ?? 1;
103
+ this.#moduleVersions.set(modulePath, version);
104
+ const fileUrl = pathToFileURL(modulePath).href;
105
+ const identifier = `${fileUrl}?omp-session=${this.#sessionTag}&v=${version}`;
106
+ const wrappedSource = buildModuleSource(stripped, modulePath);
107
+ const module = new vm.SourceTextModule(wrappedSource, {
108
+ context: this.#context,
109
+ identifier,
110
+ initializeImportMeta: meta => {
111
+ (meta as { url?: string; path?: string; dir?: string }).url = fileUrl;
112
+ (meta as { url?: string; path?: string; dir?: string }).path = modulePath;
113
+ (meta as { url?: string; path?: string; dir?: string }).dir = moduleDir;
114
+ },
115
+ importModuleDynamically: async specifier => {
116
+ return await this.#resolveLinkedModule(modulePath, String(specifier));
117
+ },
118
+ });
119
+ const entry: LocalModuleEntry = { version, identifier, module };
120
+ this.#moduleEntries.set(modulePath, entry);
121
+ try {
122
+ await module.link(async specifier => await this.#resolveLinkedModule(modulePath, specifier));
123
+ await module.evaluate();
124
+ return entry;
125
+ } catch (error) {
126
+ this.#moduleEntries.delete(modulePath);
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ async #resolveLinkedModule(referrerPath: string, specifier: string): Promise<vm.Module> {
132
+ const baseDir = path.dirname(referrerPath);
133
+ const resolved = resolveImportSpecifier(baseDir, specifier);
134
+ if (isLocalPathSpecifier(specifier) && isManagedLocalModulePath(resolved)) {
135
+ return (await this.#ensureLocalModule(resolved)).module;
136
+ }
137
+ return await this.#ensureExternalModule(normalizeImportTarget(resolved));
138
+ }
139
+
140
+ async #ensureExternalModule(target: string): Promise<vm.Module> {
141
+ const existing = this.#externalModules.get(target);
142
+ if (existing) return await existing;
143
+ const loadPromise = (async () => {
144
+ const namespace = await import(target);
145
+ const exportNames = Object.keys(namespace);
146
+ const module = new vm.SyntheticModule(
147
+ exportNames,
148
+ function () {
149
+ for (const name of exportNames) {
150
+ this.setExport(name, namespace[name as keyof typeof namespace]);
151
+ }
152
+ },
153
+ { context: this.#context, identifier: target },
154
+ );
155
+ await module.link(() => {
156
+ throw new Error("Synthetic external modules have no dependencies");
157
+ });
158
+ await module.evaluate();
159
+ return module;
160
+ })();
161
+ this.#externalModules.set(target, loadPromise);
162
+ try {
163
+ return await loadPromise;
164
+ } catch (error) {
165
+ if (this.#externalModules.get(target) === loadPromise) this.#externalModules.delete(target);
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ #refreshTrackedLocalModules(): void {
171
+ const changed: string[] = [];
172
+ for (const [modulePath, previousMtime] of this.#moduleMtimes.entries()) {
173
+ let nextMtime: number | undefined;
174
+ try {
175
+ nextMtime = fs.statSync(modulePath).mtimeMs;
176
+ } catch {
177
+ nextMtime = undefined;
178
+ }
179
+ if (nextMtime === previousMtime) continue;
180
+ if (nextMtime === undefined) this.#moduleMtimes.delete(modulePath);
181
+ else this.#moduleMtimes.set(modulePath, nextMtime);
182
+ changed.push(modulePath);
183
+ }
184
+ for (const modulePath of changed) {
185
+ this.#invalidateModuleAndParents(modulePath, new Set());
186
+ }
187
+ }
188
+
189
+ #invalidateModuleAndParents(modulePath: string, seen: Set<string>): void {
190
+ if (seen.has(modulePath)) return;
191
+ seen.add(modulePath);
192
+ this.#moduleEntries.delete(modulePath);
193
+ this.#moduleBuilds.delete(modulePath);
194
+ this.#moduleVersions.set(modulePath, (this.#moduleVersions.get(modulePath) ?? 1) + 1);
195
+ const parents = [...(this.#moduleParents.get(modulePath) ?? [])];
196
+ for (const parent of parents) this.#invalidateModuleAndParents(parent, seen);
197
+ }
198
+
199
+ #setModuleDependencies(modulePath: string, deps: Set<string>): void {
200
+ const previousDeps = this.#moduleDeps.get(modulePath);
201
+ if (previousDeps) {
202
+ for (const dep of previousDeps) {
203
+ const parents = this.#moduleParents.get(dep);
204
+ if (!parents) continue;
205
+ parents.delete(modulePath);
206
+ if (parents.size === 0) this.#moduleParents.delete(dep);
207
+ }
208
+ }
209
+ this.#moduleDeps.set(modulePath, new Set(deps));
210
+ for (const dep of deps) {
211
+ const parents = this.#moduleParents.get(dep) ?? new Set<string>();
212
+ parents.add(modulePath);
213
+ this.#moduleParents.set(dep, parents);
214
+ }
215
+ }
216
+ }
217
+
218
+ function buildRequire(fromPath: string): NodeJS.Require {
219
+ const basePath = path.extname(fromPath) ? fromPath : path.join(fromPath, "[eval]");
220
+ return createRequire(pathToFileURL(basePath).href);
221
+ }
222
+
223
+ function buildModuleSource(source: string, modulePath: string): string {
224
+ const moduleDir = path.dirname(modulePath);
225
+ return [
226
+ `const require = globalThis.__omp_get_require__(${JSON.stringify(pathToFileURL(modulePath).href)});`,
227
+ `const __filename = ${JSON.stringify(modulePath)};`,
228
+ `const __dirname = ${JSON.stringify(moduleDir)};`,
229
+ source,
230
+ ].join("\n");
231
+ }
232
+
233
+ function resolveImportSpecifier(cwd: string, source: string): string {
234
+ if (/^[a-z][a-z0-9+.-]*:/i.test(source)) return source;
235
+ try {
236
+ return Bun.resolveSync(source, cwd);
237
+ } catch {
238
+ return source;
239
+ }
240
+ }
241
+
242
+ function isLocalPathSpecifier(source: string): boolean {
243
+ return (
244
+ source.startsWith("./") ||
245
+ source.startsWith("../") ||
246
+ source === "." ||
247
+ source === ".." ||
248
+ source.startsWith("/") ||
249
+ source.startsWith("~/") ||
250
+ /^[a-zA-Z]:[\\/]/.test(source)
251
+ );
252
+ }
253
+
254
+ function isManagedLocalModulePath(target: string): boolean {
255
+ return (
256
+ path.isAbsolute(target) &&
257
+ LOCAL_MODULE_EXTENSIONS.has(path.extname(target)) &&
258
+ !target.includes(`${path.sep}node_modules${path.sep}`)
259
+ );
260
+ }
261
+
262
+ function normalizeImportTarget(target: string): string {
263
+ if (path.isAbsolute(target)) return pathToFileURL(target).href;
264
+ return target;
265
+ }
@@ -51,6 +51,10 @@ if (!globalThis.__omp_js_prelude_loaded__) {
51
51
  warn: (...args) => globalThis.__omp_log__("warn", ...formatArgs(args)),
52
52
  error: (...args) => globalThis.__omp_log__("error", ...formatArgs(args)),
53
53
  debug: (...args) => globalThis.__omp_log__("debug", ...formatArgs(args)),
54
+ table: (data, columns) =>
55
+ columns === undefined
56
+ ? globalThis.__omp_table__(data)
57
+ : globalThis.__omp_table__(data, columns),
54
58
  };
55
59
 
56
60
  globalThis.console = consoleBridge;
@@ -57,6 +57,10 @@ type BabelExpressionStatement = {
57
57
  };
58
58
 
59
59
  type BabelProgramNode = BabelImportDeclaration | BabelLexicalDecl | BabelExpressionStatement | { type: string };
60
+ type BabelModuleSourceDeclaration = {
61
+ type: "ImportDeclaration" | "ExportNamedDeclaration" | "ExportAllDeclaration";
62
+ source?: { value: string; start: number; end: number } | null;
63
+ };
60
64
 
61
65
  type BabelNode = { type: string; start: number; end: number; [key: string]: unknown };
62
66
 
@@ -189,6 +193,84 @@ export function rewriteImports(code: string): string {
189
193
  }
190
194
  return result;
191
195
  }
196
+ export function collectModuleSourceSpecifiers(code: string): string[] {
197
+ const ast = parseProgram(code);
198
+ if (!ast) return [];
199
+ const sources: string[] = [];
200
+ for (const node of ast.program.body) {
201
+ if (
202
+ (node.type === "ImportDeclaration" ||
203
+ node.type === "ExportNamedDeclaration" ||
204
+ node.type === "ExportAllDeclaration") &&
205
+ typeof (node as BabelModuleSourceDeclaration).source?.value === "string"
206
+ ) {
207
+ sources.push((node as BabelModuleSourceDeclaration).source!.value);
208
+ }
209
+ }
210
+ return sources;
211
+ }
212
+
213
+ export function rewriteModuleSourceSpecifiers(code: string, replacer: (source: string) => string): string {
214
+ const ast = parseProgram(code);
215
+ if (!ast) return code;
216
+
217
+ type Edit = { start: number; end: number; text: string };
218
+ const edits: Edit[] = [];
219
+
220
+ for (const node of ast.program.body) {
221
+ if (
222
+ node.type !== "ImportDeclaration" &&
223
+ node.type !== "ExportNamedDeclaration" &&
224
+ node.type !== "ExportAllDeclaration"
225
+ ) {
226
+ continue;
227
+ }
228
+ const source = (node as BabelModuleSourceDeclaration).source;
229
+ if (!source || typeof source.value !== "string") continue;
230
+ const next = replacer(source.value);
231
+ if (next === source.value) continue;
232
+ edits.push({ start: source.start, end: source.end, text: JSON.stringify(next) });
233
+ }
234
+
235
+ if (edits.length === 0) return code;
236
+ edits.sort((a, b) => b.start - a.start);
237
+ let result = code;
238
+ for (const edit of edits) {
239
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
240
+ }
241
+ return result;
242
+ }
243
+
244
+ export function rewriteDynamicImports(code: string, callee = "__omp_import__"): string {
245
+ if (!code.includes("import")) return code;
246
+ const ast = parseProgram(code);
247
+ if (!ast) return code;
248
+
249
+ type Edit = { start: number; end: number; text: string };
250
+ const edits: Edit[] = [];
251
+ walkNodes(ast, node => {
252
+ if (node.type !== "CallExpression") return;
253
+ const call = node as unknown as { callee?: { type?: string; start?: number; end?: number } };
254
+ const callCallee = call.callee;
255
+ if (
256
+ !callCallee ||
257
+ callCallee.type !== "Import" ||
258
+ typeof callCallee.start !== "number" ||
259
+ typeof callCallee.end !== "number"
260
+ ) {
261
+ return;
262
+ }
263
+ edits.push({ start: callCallee.start, end: callCallee.end, text: callee });
264
+ });
265
+
266
+ if (edits.length === 0) return code;
267
+ edits.sort((a, b) => b.start - a.start);
268
+ let result = code;
269
+ for (const edit of edits) {
270
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
271
+ }
272
+ return result;
273
+ }
192
274
 
193
275
  function collectBindingNames(pattern: unknown, names: string[]): void {
194
276
  if (!pattern || typeof pattern !== "object") return;
@@ -390,6 +472,9 @@ function stripTypeScript(code: string): string {
390
472
  return code;
391
473
  }
392
474
  }
475
+ export function stripTypeScriptSyntax(code: string): string {
476
+ return stripTypeScript(code);
477
+ }
393
478
 
394
479
  // Heuristic: any of the obvious TS-only tokens. Plain JS using `as` only inside strings
395
480
  // won't match because we require a leading word boundary plus a colon/keyword neighbor.
@@ -1,23 +1,23 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { Console } from "node:console";
1
3
  import * as fs from "node:fs";
2
4
  import { createRequire } from "node:module";
3
5
  import * as path from "node:path";
4
- import { pathToFileURL } from "node:url";
6
+ import { Writable } from "node:stream";
5
7
  import * as util from "node:util";
6
8
 
7
9
  import { logger } from "@oh-my-pi/pi-utils";
8
10
 
9
- import { ToolError } from "../../../tools/tool-errors";
10
11
  import { createHelpers, type HelperBundle } from "./helpers";
11
12
  import { awaitMaybePromise, indirectEval } from "./indirect-eval";
13
+ import { LocalModuleLoader } from "./local-module-loader";
12
14
  import { JAVASCRIPT_PRELUDE_SOURCE } from "./prelude";
13
15
  import { wrapCode } from "./rewrite-imports";
14
16
  import type { JsDisplayOutput, JsStatusEvent } from "./types";
15
17
 
16
18
  /**
17
- * Per-run callbacks. Returned by `getHooks()` on each helper/tool/display invocation so
18
- * the embedding worker can route emissions to the currently active run. Returning `null`
19
- * makes status/display/tool calls reject with an error — useful for guarding against
20
- * helpers being invoked outside a run window.
19
+ * Per-run callbacks. Runtime globals resolve these from AsyncLocalStorage so
20
+ * overlapping async cells can route output/tool calls back to their own run.
21
21
  */
22
22
  export interface RuntimeHooks {
23
23
  onText(chunk: string): void;
@@ -25,11 +25,17 @@ export interface RuntimeHooks {
25
25
  callTool(name: string, args: unknown): Promise<unknown>;
26
26
  }
27
27
 
28
+ export interface RunContext {
29
+ runId: string;
30
+ hooks: RuntimeHooks;
31
+ cwd: string;
32
+ finalExpressionSet: boolean;
33
+ finalExpressionValue: unknown;
34
+ }
35
+
28
36
  export interface RuntimeOptions {
29
37
  initialCwd: string;
30
38
  sessionId: string;
31
- /** Resolve hooks for the run currently in flight, or `null` if nothing is active. */
32
- getHooks(): RuntimeHooks | null;
33
39
  /**
34
40
  * Extra globals installed alongside `__omp_helpers__` / prelude. Use for stable, lifetime-
35
41
  * of-the-worker bindings (e.g. browser's `page`, `browser`). Per-run scope should be set
@@ -118,19 +124,18 @@ export class JsRuntime {
118
124
  #cwd: string;
119
125
  readonly sessionId: string;
120
126
  #env: Map<string, string>;
121
- #getHooks: () => RuntimeHooks | null;
122
- #finalExpressionSet = false;
123
- #finalExpressionValue: unknown;
127
+ #als = new AsyncLocalStorage<RunContext>();
128
+ #moduleLoader: LocalModuleLoader;
124
129
 
125
130
  constructor(opts: RuntimeOptions) {
126
131
  this.#cwd = opts.initialCwd;
127
132
  this.sessionId = opts.sessionId;
128
133
  this.#env = new Map();
129
- this.#getHooks = opts.getHooks;
134
+ this.#moduleLoader = new LocalModuleLoader(this.sessionId);
130
135
  this.helpers = createHelpers({
131
- cwd: () => this.#cwd,
136
+ cwd: () => this.#activeCwd(),
132
137
  env: this.#env,
133
- emitStatus: event => this.#getHooks()?.onDisplay({ type: "status", event }),
138
+ emitStatus: event => this.#activeHooks("emitStatus")?.onDisplay({ type: "status", event }),
134
139
  });
135
140
  this.#install(opts.extraGlobals);
136
141
  }
@@ -154,29 +159,42 @@ export class JsRuntime {
154
159
  Object.assign(globalThis, scope);
155
160
  }
156
161
 
157
- async run(code: string, filename?: string): Promise<unknown> {
158
- this.#finalExpressionSet = false;
159
- this.#finalExpressionValue = undefined;
160
- const wrapped = wrapCode(code);
161
- const value = indirectEval(wrapped.source, filename);
162
- if (wrapped.finalExpressionReturned) {
163
- const awaited = await awaitMaybePromise(value);
164
- if (this.#finalExpressionSet) {
165
- const finalValue = this.#finalExpressionValue;
166
- this.#finalExpressionSet = false;
167
- this.#finalExpressionValue = undefined;
168
- const resolved = await awaitMaybePromise(finalValue);
169
- return resolved;
162
+ async run(
163
+ code: string,
164
+ filename: string | undefined,
165
+ hooks: RuntimeHooks,
166
+ options: { runId?: string; cwd?: string } = {},
167
+ ): Promise<unknown> {
168
+ const context: RunContext = {
169
+ runId: options.runId ?? crypto.randomUUID(),
170
+ hooks,
171
+ cwd: options.cwd ?? this.#cwd,
172
+ finalExpressionSet: false,
173
+ finalExpressionValue: undefined,
174
+ };
175
+ return await this.#als.run(context, async () => {
176
+ const wrapped = wrapCode(code);
177
+ const value = indirectEval(wrapped.source, filename);
178
+ if (wrapped.finalExpressionReturned) {
179
+ const awaited = await awaitMaybePromise(value);
180
+ if (context.finalExpressionSet) {
181
+ const finalValue = context.finalExpressionValue;
182
+ context.finalExpressionSet = false;
183
+ context.finalExpressionValue = undefined;
184
+ return await awaitMaybePromise(finalValue);
185
+ }
186
+ return awaited;
170
187
  }
171
- return awaited;
172
- }
173
- return await awaitMaybePromise(value);
188
+ return await awaitMaybePromise(value);
189
+ });
174
190
  }
175
191
 
176
- displayValue(value: unknown): void {
192
+ displayValue(value: unknown, hooks: RuntimeHooks | undefined = this.#als.getStore()?.hooks): void {
177
193
  if (value === undefined) return;
178
- const hooks = this.#getHooks();
179
- if (!hooks) return;
194
+ if (!hooks) {
195
+ logger.warn("js runtime display called outside an active run");
196
+ return;
197
+ }
180
198
  if (value && typeof value === "object") {
181
199
  const record = value as Record<string, unknown>;
182
200
  if (record.type === "image" && typeof record.mimeType === "string") {
@@ -207,45 +225,108 @@ export class JsRuntime {
207
225
  hooks.onText(`${String(value)}\n`);
208
226
  }
209
227
 
228
+ #activeCwd(): string {
229
+ return this.#als.getStore()?.cwd ?? this.#cwd;
230
+ }
231
+
232
+ #activeHooks(action: string): RuntimeHooks | undefined {
233
+ const hooks = this.#als.getStore()?.hooks;
234
+ if (!hooks) {
235
+ logger.warn("js runtime helper called outside an active run", { action });
236
+ }
237
+ return hooks;
238
+ }
239
+
240
+ #activeRequire(moduleUrlOrPath?: string): NodeJS.Require {
241
+ return this.#moduleLoader.requireForFile(moduleUrlOrPath, this.#activeCwd());
242
+ }
243
+
244
+ #moduleFilename(moduleUrlOrPath?: string): string {
245
+ return this.#moduleLoader.filenameForUrl(moduleUrlOrPath) ?? path.join(this.#activeCwd(), "[eval]");
246
+ }
247
+
248
+ #moduleDirname(moduleUrlOrPath?: string): string {
249
+ return this.#moduleLoader.dirnameForUrl(moduleUrlOrPath, this.#activeCwd());
250
+ }
251
+
252
+ #buildDynamicRequire(): NodeJS.Require {
253
+ const dynamicRequire = ((id: string) => this.#activeRequire()(id)) as NodeJS.Require;
254
+ const resolve = ((id: string, options?: { paths?: string[] }) =>
255
+ this.#activeRequire().resolve(id, options)) as NodeJS.Require["resolve"] & {
256
+ paths(request: string): string[] | null;
257
+ };
258
+ resolve.paths = request => this.#activeRequire().resolve.paths(request);
259
+ Object.defineProperties(dynamicRequire, {
260
+ resolve: { value: resolve, configurable: true },
261
+ cache: { get: () => this.#activeRequire().cache, configurable: true },
262
+ extensions: { get: () => this.#activeRequire().extensions, configurable: true },
263
+ main: { get: () => this.#activeRequire().main, configurable: true },
264
+ });
265
+ return dynamicRequire;
266
+ }
267
+
210
268
  #install(extraGlobals: Record<string, unknown> | undefined): void {
211
269
  const injected: Record<string, unknown> = {
212
270
  __omp_session__: { cwd: this.#cwd, sessionId: this.sessionId },
213
271
  __omp_helpers__: this.helpers,
214
272
  __omp_call_tool__: async (name: string, args: unknown) => {
215
- const hooks = this.#getHooks();
216
- if (!hooks) throw new ToolError("Tool calls are only valid inside an active run");
273
+ const hooks = this.#activeHooks("tool");
274
+ if (!hooks) return undefined;
217
275
  return await hooks.callTool(name, args);
218
276
  },
219
277
  __omp_import__: async (source: string, options?: ImportCallOptions) => {
220
- const target = resolveImportSpecifier(this.#cwd, source);
221
- // Always invalidate cached module records for user-owned source files so edits
222
- // between cells are picked up. Bun ignores query-string busting on `file:` URLs
223
- // but honors `delete require.cache[absPath]`; bare specifiers and URL schemes are
224
- // left alone to keep package identity stable across cells.
225
- if (isLocalPathSpecifier(source) && path.isAbsolute(target)) {
226
- delete require.cache[target];
227
- }
278
+ const resolved = await this.#moduleLoader.resolveForRun(this.#activeCwd(), source);
279
+ if (resolved.mode === "local") return resolved.value;
280
+ const target = resolved.target;
281
+ return options !== undefined ? await import(target, options) : await import(target);
282
+ },
283
+ __omp_import_from__: async (moduleUrl: string, source: string, options?: ImportCallOptions) => {
284
+ const resolved = await this.#moduleLoader.resolveForModule(moduleUrl, source, this.#activeCwd());
285
+ if (resolved.mode === "local") return resolved.value;
286
+ const target = resolved.target;
228
287
  return options !== undefined ? await import(target, options) : await import(target);
229
288
  },
289
+ __omp_get_require__: (moduleUrl?: string) => this.#activeRequire(moduleUrl),
290
+ __omp_get_filename__: (moduleUrl?: string) => this.#moduleFilename(moduleUrl),
291
+ __omp_get_dirname__: (moduleUrl?: string) => this.#moduleDirname(moduleUrl),
230
292
  __omp_emit_status__: (op: string, data: Record<string, unknown> = {}) => {
231
293
  const event: JsStatusEvent = { op, ...data };
232
- this.#getHooks()?.onDisplay({ type: "status", event });
294
+ this.#activeHooks("emitStatus")?.onDisplay({ type: "status", event });
233
295
  },
234
296
  __omp_log__: (level: string, ...args: unknown[]) => {
235
297
  const prefix = level === "error" ? "[error] " : level === "warn" ? "[warn] " : "";
236
298
  const text = `${prefix}${formatConsoleArgs(args)}`;
237
- this.#getHooks()?.onText(text.endsWith("\n") ? text : `${text}\n`);
299
+ this.#activeHooks("log")?.onText(text.endsWith("\n") ? text : `${text}\n`);
300
+ },
301
+ __omp_table__: (...args: unknown[]) => {
302
+ const hooks = this.#activeHooks("table");
303
+ if (!hooks) return;
304
+ let buffer = "";
305
+ const stream = new Writable({
306
+ write(chunk, _enc, cb) {
307
+ buffer += typeof chunk === "string" ? chunk : (chunk as Buffer).toString("utf8");
308
+ cb();
309
+ },
310
+ });
311
+ const tableConsole = new Console({ stdout: stream, colorMode: false });
312
+ (tableConsole.table as (...a: unknown[]) => void)(...args);
313
+ hooks.onText(buffer.endsWith("\n") ? buffer : `${buffer}\n`);
238
314
  },
239
315
  __omp_display__: (value: unknown) => this.displayValue(value),
240
316
  __omp_set_final_expr__: (value: unknown) => {
241
- this.#finalExpressionSet = true;
242
- this.#finalExpressionValue = value;
317
+ const context = this.#als.getStore();
318
+ if (!context) {
319
+ logger.warn("js runtime final expression set outside an active run");
320
+ return;
321
+ }
322
+ context.finalExpressionSet = true;
323
+ context.finalExpressionValue = value;
243
324
  },
244
325
  webcrypto: crypto,
245
326
  // `process` is intentionally not overridden — user code gets the host worker's real
246
327
  // `process` object. Subsetting it caused segfaults in workers that share state with
247
328
  // puppeteer/worker_threads internals.
248
- require: buildRequire(this.#cwd),
329
+ require: this.#buildDynamicRequire(),
249
330
  createRequire,
250
331
  fs,
251
332
  };
@@ -261,41 +342,3 @@ function formatConsoleArgs(args: unknown[]): string {
261
342
  .map(arg => (typeof arg === "string" ? arg : util.inspect(arg, { depth: 6, colors: false, breakLength: 120 })))
262
343
  .join(" ");
263
344
  }
264
-
265
- function buildRequire(cwd: string): NodeJS.Require {
266
- return createRequire(pathToFileURL(path.join(cwd, "[eval]")).href);
267
- }
268
-
269
- /**
270
- * Resolve an import specifier emitted by `rewriteImports` against the active session
271
- * cwd. Relative paths (`./`, `../`, `/`) and bare specifiers (`pkg`, `@scope/pkg`) both go
272
- * through `Bun.resolveSync` rooted at the cwd so user-pasted ESM behaves as if it lived in
273
- * the project — not next to the worker module. URL-like specifiers (`file://`, `data:`,
274
- * `node:`, `http:`) are passed through unchanged.
275
- */
276
- function resolveImportSpecifier(cwd: string, source: string): string {
277
- if (/^[a-z][a-z0-9+.-]*:/i.test(source)) return source;
278
- try {
279
- return Bun.resolveSync(source, cwd);
280
- } catch {
281
- return source;
282
- }
283
- }
284
-
285
- /**
286
- * Returns true when the original specifier is a relative or absolute filesystem path
287
- * (i.e. user-owned source the agent is iterating on). Bare specifiers and URL schemes
288
- * are excluded — `node:` built-ins cannot be reloaded, and busting bare packages would
289
- * defeat module identity for every cell while bringing no editing benefit.
290
- */
291
- function isLocalPathSpecifier(source: string): boolean {
292
- return (
293
- source.startsWith("./") ||
294
- source.startsWith("../") ||
295
- source === "." ||
296
- source === ".." ||
297
- source.startsWith("/") ||
298
- source.startsWith("~/") ||
299
- /^[a-zA-Z]:[\\/]/.test(source)
300
- );
301
- }