@oh-my-pi/pi-coding-agent 16.4.2 → 16.4.3

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 (122) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/cli.js +17514 -17799
  3. package/dist/types/advisor/config.d.ts +4 -3
  4. package/dist/types/commit/agentic/agent.d.ts +1 -0
  5. package/dist/types/config/settings-schema.d.ts +28 -0
  6. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +10 -22
  7. package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
  8. package/dist/types/modes/components/move-overlay.d.ts +1 -1
  9. package/dist/types/modes/components/status-line/component.d.ts +3 -0
  10. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  11. package/dist/types/modes/interactive-mode.d.ts +8 -0
  12. package/dist/types/modes/types.d.ts +2 -0
  13. package/dist/types/session/agent-session.d.ts +16 -0
  14. package/dist/types/task/executor.d.ts +31 -0
  15. package/dist/types/tools/__tests__/vibe-render.test.d.ts +1 -0
  16. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
  17. package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
  18. package/dist/types/tools/browser/run-output.d.ts +25 -0
  19. package/dist/types/tools/browser/tab-worker.d.ts +16 -0
  20. package/dist/types/tools/index.d.ts +1 -0
  21. package/dist/types/tools/vibe.d.ts +161 -0
  22. package/dist/types/utils/changelog.d.ts +35 -1
  23. package/dist/types/vibe/runtime.d.ts +124 -0
  24. package/dist/types/vibe/state.d.ts +4 -0
  25. package/dist/types/web/search/provider.d.ts +2 -0
  26. package/dist/types/web/search/providers/bing.d.ts +14 -0
  27. package/dist/types/web/search/providers/browser-headers.d.ts +9 -0
  28. package/dist/types/web/search/providers/browser-page.d.ts +32 -0
  29. package/dist/types/web/search/providers/ecosia.d.ts +14 -0
  30. package/dist/types/web/search/providers/google.d.ts +13 -0
  31. package/dist/types/web/search/providers/mojeek.d.ts +14 -0
  32. package/dist/types/web/search/providers/public.d.ts +37 -0
  33. package/dist/types/web/search/providers/startpage.d.ts +14 -0
  34. package/dist/types/web/search/providers/yahoo.d.ts +14 -0
  35. package/dist/types/web/search/types.d.ts +28 -0
  36. package/package.json +15 -18
  37. package/scripts/build-binary.ts +56 -73
  38. package/scripts/bundle-dist.ts +36 -40
  39. package/scripts/compile-binary.ts +68 -0
  40. package/scripts/generate-docs-index.ts +3 -93
  41. package/scripts/legacy-pi-virtual-module.ts +192 -0
  42. package/src/advisor/__tests__/advisor.test.ts +13 -0
  43. package/src/advisor/__tests__/config.test.ts +36 -0
  44. package/src/advisor/config.ts +11 -8
  45. package/src/commit/agentic/agent.ts +4 -0
  46. package/src/commit/agentic/index.ts +46 -21
  47. package/src/edit/index.ts +10 -76
  48. package/src/exec/non-interactive-env.ts +0 -1
  49. package/src/extensibility/plugins/legacy-pi-compat.ts +328 -162
  50. package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
  51. package/src/internal-urls/docs-index.ts +2 -1
  52. package/src/internal-urls/skill-protocol.ts +1 -1
  53. package/src/lsp/deferred-diagnostics.ts +66 -0
  54. package/src/main.ts +13 -13
  55. package/src/mcp/transports/stdio.test.ts +45 -1
  56. package/src/mcp/transports/stdio.ts +6 -3
  57. package/src/modes/acp/acp-agent.ts +65 -1
  58. package/src/modes/acp/acp-event-mapper.ts +5 -0
  59. package/src/modes/acp/acp-mode.ts +11 -0
  60. package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
  61. package/src/modes/components/advisor-config.ts +15 -7
  62. package/src/modes/components/move-overlay.ts +2 -3
  63. package/src/modes/components/status-line/component.ts +6 -0
  64. package/src/modes/components/status-line/segments.ts +6 -0
  65. package/src/modes/components/status-line/types.ts +3 -0
  66. package/src/modes/controllers/command-controller.ts +8 -10
  67. package/src/modes/interactive-mode.ts +117 -1
  68. package/src/modes/types.ts +2 -0
  69. package/src/prompts/system/eager-task.md +2 -2
  70. package/src/prompts/system/system-prompt.md +9 -2
  71. package/src/prompts/system/vibe-mode-active.md +23 -0
  72. package/src/prompts/tools/browser.md +3 -3
  73. package/src/prompts/tools/grep.md +1 -1
  74. package/src/prompts/tools/vibe-kill.md +3 -0
  75. package/src/prompts/tools/vibe-list.md +3 -0
  76. package/src/prompts/tools/vibe-send.md +9 -0
  77. package/src/prompts/tools/vibe-spawn.md +10 -0
  78. package/src/prompts/tools/vibe-turn-result.md +19 -0
  79. package/src/prompts/tools/vibe-wait.md +8 -0
  80. package/src/sdk.ts +5 -0
  81. package/src/session/agent-session.ts +103 -13
  82. package/src/session/snapcompact-inline.ts +3 -19
  83. package/src/slash-commands/builtin-registry.ts +24 -8
  84. package/src/task/agents.ts +0 -2
  85. package/src/task/executor.ts +105 -0
  86. package/src/tools/__tests__/vibe-render.test.ts +210 -0
  87. package/src/tools/bash-skill-urls.ts +1 -1
  88. package/src/tools/browser/cmux/cmux-tab.ts +46 -48
  89. package/src/tools/browser/cmux/rpc.ts +50 -0
  90. package/src/tools/browser/run-output.ts +76 -0
  91. package/src/tools/browser/tab-worker.ts +264 -129
  92. package/src/tools/glob.ts +20 -6
  93. package/src/tools/index.ts +1 -0
  94. package/src/tools/read.ts +17 -9
  95. package/src/tools/renderers.ts +6 -0
  96. package/src/tools/vibe.ts +608 -0
  97. package/src/tools/write.ts +15 -2
  98. package/src/utils/changelog.ts +106 -9
  99. package/src/utils/git.ts +0 -1
  100. package/src/utils/title-generator.ts +70 -7
  101. package/src/vibe/runtime.ts +710 -0
  102. package/src/vibe/state.ts +4 -0
  103. package/src/web/search/index.ts +14 -6
  104. package/src/web/search/provider.ts +37 -1
  105. package/src/web/search/providers/bing.ts +197 -0
  106. package/src/web/search/providers/browser-headers.ts +92 -0
  107. package/src/web/search/providers/browser-page.ts +123 -0
  108. package/src/web/search/providers/duckduckgo.ts +13 -34
  109. package/src/web/search/providers/ecosia.ts +178 -0
  110. package/src/web/search/providers/google.ts +193 -0
  111. package/src/web/search/providers/mojeek.ts +206 -0
  112. package/src/web/search/providers/public.ts +201 -0
  113. package/src/web/search/providers/startpage.ts +213 -0
  114. package/src/web/search/providers/yahoo.ts +179 -0
  115. package/src/web/search/types.ts +35 -0
  116. package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +0 -10
  117. package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +0 -10
  118. package/scripts/generate-legacy-pi-bundled-registry.ts +0 -420
  119. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +0 -1011
  120. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +0 -3430
  121. package/src/internal-urls/docs-index.generated.txt +0 -2
  122. package/src/prompts/agents/plan.md +0 -47
@@ -1,82 +1,62 @@
1
+ /// <reference path="./legacy-pi-virtual-modules.d.ts" />
1
2
  import * as fs from "node:fs";
2
3
  import { isBuiltin } from "node:module";
3
4
  import * as path from "node:path";
4
5
  import * as url from "node:url";
5
6
  import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
6
7
  import { registerPluginCacheInvalidator } from "../../discovery/helpers";
7
- import { BUNDLED_PI_REGISTRY_KEYS } from "./legacy-pi-bundled-keys";
8
8
 
9
9
  const IS_COMPILED_BINARY = isCompiledBinary();
10
10
 
11
- // === Bundled host-package registry (issue #3423) ===
11
+ // === Bundled host modules (issue #3423) ===
12
12
  //
13
13
  // Bun 1.3.14 stopped exposing `--compile` extras through any filesystem-style
14
14
  // API: `fs.existsSync`, `Bun.file().exists()`, `Bun.resolveSync`, and even
15
15
  // `import("/$bunfs/...")` / `import("file:///$bunfs/...")` all fail for the
16
- // embedded entries only the main binary itself answers from
17
- // `/$bunfs/root/<binary-name>`. The previous strategy of rewriting
18
- // `@(scope)/pi-*` imports to a `file:///$bunfs/...` URL therefore breaks
19
- // every legacy extension in compiled mode (issue #3423; see also issues
20
- // #3329, #2168). Bun.plugin `onResolve` for bare specifiers also no longer
21
- // fires for transitive imports inside runtime-loaded extensions, so the
22
- // fallback hook in `installLegacyPiSpecifierShim()` cannot rescue them.
16
+ // embedded entries. Bun.plugin `onResolve` also no longer fires for transitive
17
+ // imports inside runtime-loaded extensions.
23
18
  //
24
- // Instead we keep a JS-heap reference to every bundled pi-* surface (the
25
- // canonical host packages and the legacy shims) and re-export them through a
26
- // Bun.plugin `onLoad` against a custom namespace. Extension source rewrites
27
- // emit `omp-legacy-pi-bundled:<key>` specifiers that the synthetic loader
28
- // resolves against the registry no bunfs path ever leaves this module in
29
- // compiled mode. Dev / source-link / installed-package modes keep the
30
- // historical `file://` rewrite (the source files exist on disk and load fine
31
- // through Bun's standard URL loader).
32
- //
33
- // The registry lives in a sibling file (`legacy-pi-bundled-registry.ts`)
34
- // loaded via a conditional dynamic import: its transitive deps include the
35
- // coding-agent root which pulls in generated artifacts (e.g.
36
- // `export/html/tool-views.generated.js`) that only exist after a build, so a
37
- // static import would crash every dev/test run that touches
38
- // `legacy-pi-compat.ts`. This is the documented "conditional platform code"
39
- // exception to the static-import rule.
19
+ // Compiled builds therefore keep live JS-heap references to the host packages
20
+ // and serve requested surfaces through `omp-legacy-pi-bundled:<key>` synthetic
21
+ // modules. `scripts/legacy-pi-virtual-module.ts` derives the static import edges
22
+ // from current package exports inside a Bun build plugin: no generated source
23
+ // or duplicate key list exists on disk. Runtime extension loading stays lazy
24
+ // the virtual module is evaluated only when an extension requests a host
25
+ // package but the compiler still sees every possible edge at build time.
40
26
  const BUNDLED_VIRTUAL_SCHEME = "omp-legacy-pi-bundled:";
41
27
  const BUNDLED_VIRTUAL_NAMESPACE = "omp-legacy-pi-bundled";
42
- const BUNDLED_REGISTRY_GLOBAL = "__ompLegacyPiBundledRegistry";
43
- const TYPEBOX_BUNDLED_REGISTRY_KEY = "typebox";
28
+ const BUNDLED_MODULES_GLOBAL = "__ompLegacyPiBundledModules";
29
+ const TYPEBOX_BUNDLED_MODULE_KEY = "typebox";
44
30
 
45
- type BundledRegistry = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
31
+ type BundledModules = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
46
32
 
47
- let bundledRegistryPromise: Promise<BundledRegistry> | null = null;
33
+ let bundledModulesPromise: Promise<BundledModules> | null = null;
48
34
 
49
35
  /**
50
- * Lazy-load the bundled host-package registry and stash it on `globalThis`
51
- * for the synthetic loader emitted by `synthesizeBundledModuleSource`.
52
- *
53
- * `globalThis` is the bridge, not laziness: each synthesized
54
- * `omp-legacy-pi-bundled:<key>` module is a *separate* ES module Bun compiles
55
- * from a source string, so it cannot close over the registry in this file's
56
- * lexical scope and the live (non-serializable) function/object exports cannot
57
- * be inlined — the only runtime channel back to the host objects is a global.
36
+ * Lazy-load the build-supplied host modules and stash them on `globalThis` for
37
+ * the synthetic module source emitted by `synthesizeBundledModuleSource`.
58
38
  *
59
- * The dynamic import is gated by `IS_COMPILED_BINARY` so dev/test runs (where
60
- * the registry's transitive deps include build-time-generated artifacts)
61
- * never trigger the cascade.
39
+ * `globalThis` is the bridge: each `omp-legacy-pi-bundled:<key>` source string
40
+ * becomes a separate ES module and cannot close over this file's lexical scope.
41
+ * The dynamic import is intentional conditional build code. Dev/test runs
42
+ * never execute it; binary builds resolve the literal through the in-memory
43
+ * plugin in `scripts/legacy-pi-virtual-module.ts`.
62
44
  */
63
- function ensureBundledRegistryLoaded(): Promise<BundledRegistry> {
45
+ function ensureBundledModulesLoaded(): Promise<BundledModules> {
64
46
  if (!IS_COMPILED_BINARY) {
65
- return Promise.reject(
66
- new Error("omp:legacy-pi-shim: bundled registry is only available in compiled-binary mode"),
67
- );
47
+ return Promise.reject(new Error("omp:legacy-pi-shim: bundled modules are only available in compiled mode"));
68
48
  }
69
- if (!bundledRegistryPromise) {
70
- bundledRegistryPromise = import("./legacy-pi-bundled-registry").then(m => {
71
- (globalThis as Record<string, unknown>)[BUNDLED_REGISTRY_GLOBAL] = m.BUNDLED_PI_REGISTRY;
72
- return m.BUNDLED_PI_REGISTRY;
49
+ if (!bundledModulesPromise) {
50
+ bundledModulesPromise = import("omp-legacy-pi-modules").then(module => {
51
+ Reflect.set(globalThis, BUNDLED_MODULES_GLOBAL, module.BUNDLED_PI_MODULES);
52
+ return module.BUNDLED_PI_MODULES;
73
53
  });
74
54
  }
75
- return bundledRegistryPromise;
55
+ return bundledModulesPromise;
76
56
  }
77
57
 
78
- function bundledRegistryVirtualSpecifier(registryKey: string): string {
79
- return `${BUNDLED_VIRTUAL_SCHEME}${registryKey}`;
58
+ function bundledModuleVirtualSpecifier(moduleKey: string): string {
59
+ return `${BUNDLED_VIRTUAL_SCHEME}${moduleKey}`;
80
60
  }
81
61
 
82
62
  function isBundledVirtualSpecifier(value: string): boolean {
@@ -84,19 +64,16 @@ function isBundledVirtualSpecifier(value: string): boolean {
84
64
  }
85
65
 
86
66
  /**
87
- * Build the synthetic ES module source for a `omp-legacy-pi-bundled:<key>`
88
- * import against an explicit registry. Pure: takes the live module namespace
89
- * and emits a string of ES exports rooted in `globalThis[BUNDLED_REGISTRY_GLOBAL]`.
90
- * `synthesizeBundledModuleSource` wraps this with the lazy registry load —
91
- * tests use this sync helper directly to assert export-shape preservation.
67
+ * Build a synthetic ES module for one live bundled namespace. Every export
68
+ * reads through the global bridge; no bunfs path or copied package is involved.
92
69
  */
93
- function synthesizeBundledModuleSourceFromRegistry(registryKey: string, registry: BundledRegistry): string {
94
- const mod = registry[registryKey];
70
+ function synthesizeBundledModuleSourceFromModules(moduleKey: string, modules: BundledModules): string {
71
+ const mod = modules[moduleKey];
95
72
  if (!mod) {
96
- throw new Error(`omp:legacy-pi-shim: no bundled module registered for ${registryKey}`);
73
+ throw new Error(`omp:legacy-pi-shim: no bundled module registered for ${moduleKey}`);
97
74
  }
98
75
  const lines: string[] = [
99
- `const __omp_bundled = globalThis[${JSON.stringify(BUNDLED_REGISTRY_GLOBAL)}][${JSON.stringify(registryKey)}];`,
76
+ `const __omp_bundled = globalThis[${JSON.stringify(BUNDLED_MODULES_GLOBAL)}][${JSON.stringify(moduleKey)}];`,
100
77
  ];
101
78
  let hasDefault = false;
102
79
  for (const exportName in mod) {
@@ -114,36 +91,25 @@ function synthesizeBundledModuleSourceFromRegistry(registryKey: string, registry
114
91
  }
115
92
 
116
93
  /**
117
- * Build the synthetic ES module source served for an
118
- * `omp-legacy-pi-bundled:<key>` import. Enumerates the live module namespace
119
- * so legacy extensions see the same named/default exports they would have
120
- * gotten from a real `file://` load — without touching the inaccessible bunfs
121
- * filesystem.
94
+ * Build the synthetic source served for one
95
+ * `omp-legacy-pi-bundled:<key>` import.
122
96
  */
123
- async function synthesizeBundledModuleSource(registryKey: string): Promise<string> {
124
- const registry = await ensureBundledRegistryLoaded();
125
- return synthesizeBundledModuleSourceFromRegistry(registryKey, registry);
97
+ async function synthesizeBundledModuleSource(moduleKey: string): Promise<string> {
98
+ const modules = await ensureBundledModulesLoaded();
99
+ return synthesizeBundledModuleSourceFromModules(moduleKey, modules);
126
100
  }
127
101
 
128
- /**
129
- * Test seam: builds the synthetic ES module source for a virtual specifier
130
- * against an explicit registry. Pure (no globalThis read); the emitted source
131
- * still routes runtime lookups through `globalThis[BUNDLED_REGISTRY_GLOBAL]`.
132
- */
133
- export function __synthesizeLegacyPiBundledSourceWithRegistry(
134
- registryKey: string,
135
- registry: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
102
+ /** Test seam for the virtual module's named/default export forwarding. */
103
+ export function __synthesizeLegacyPiBundledSourceWithModules(
104
+ moduleKey: string,
105
+ modules: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
136
106
  ): string {
137
- return synthesizeBundledModuleSourceFromRegistry(registryKey, registry);
107
+ return synthesizeBundledModuleSourceFromModules(moduleKey, modules);
138
108
  }
139
109
 
140
- /**
141
- * Test seam: returns the globalThis key the synthetic loader reads from. Tests
142
- * assert that the emitted source addresses the exact stash key the install
143
- * function writes to, so a rename can't break extension loads silently.
144
- */
145
- export function __getLegacyPiBundledRegistryGlobal(): string {
146
- return BUNDLED_REGISTRY_GLOBAL;
110
+ /** Test seam for the global bridge key shared with synthetic module source. */
111
+ export function __getLegacyPiBundledModulesGlobal(): string {
112
+ return BUNDLED_MODULES_GLOBAL;
147
113
  }
148
114
 
149
115
  // Canonical scope for in-process pi packages. Plugins published against any of
@@ -207,6 +173,9 @@ const nodePackageRootCache = new Map<string, Promise<string | null>>();
207
173
  const packageManifestCache = new Map<string, Promise<Record<string, unknown> | null>>();
208
174
  const bareDependencyResolutionCache = new Map<string, Promise<string | null>>();
209
175
  const realpathCache = new Map<string, Promise<string>>();
176
+ const nativeAddonResolutionCache = new Map<string, Promise<string | null>>();
177
+ const nativeAddonRequireScanCache = new Map<string, Promise<boolean>>();
178
+ const nativeAddonLoaderModulePaths = new Set<string>();
210
179
 
211
180
  function clearLegacyPiResolutionCaches(): void {
212
181
  resolvedSpecifierFallbacks.clear();
@@ -215,6 +184,9 @@ function clearLegacyPiResolutionCaches(): void {
215
184
  nodePackageRootCache.clear();
216
185
  packageManifestCache.clear();
217
186
  bareDependencyResolutionCache.clear();
187
+ nativeAddonResolutionCache.clear();
188
+ nativeAddonRequireScanCache.clear();
189
+ nativeAddonLoaderModulePaths.clear();
218
190
  realpathCache.clear();
219
191
  }
220
192
 
@@ -232,7 +204,7 @@ const TYPEBOX_SPECIFIER_FILTER = /^(?:@sinclair\/typebox|typebox)$/;
232
204
 
233
205
  // Compat-shim path resolution. In compiled-binary mode every bundled surface
234
206
  // is served through the `omp-legacy-pi-bundled:` virtual namespace (see the
235
- // registry block above) — bunfs paths are unreachable on Bun 1.3.14+, so the
207
+ // bundled-module block above) — bunfs paths are unreachable on Bun 1.3.14+, so the
236
208
  // pre-#3423 helpers that derived `/$bunfs/root/...` paths from
237
209
  // `import.meta.dir` are gone. Dev / source-link / installed-package modes
238
210
  // still need a real filesystem path for the source shims, which
@@ -298,7 +270,7 @@ export function __resolveTypeBoxShimPath(
298
270
  pathExistsSync: (p: string) => boolean = fs.existsSync,
299
271
  ): string | null {
300
272
  if (isCompiled) {
301
- return bundledRegistryVirtualSpecifier(TYPEBOX_BUNDLED_REGISTRY_KEY);
273
+ return bundledModuleVirtualSpecifier(TYPEBOX_BUNDLED_MODULE_KEY);
302
274
  }
303
275
  return pathExistsSync(sourcePath) ? sourcePath : null;
304
276
  }
@@ -314,18 +286,18 @@ const TYPEBOX_SHIM_PATH = __resolveTypeBoxShimPath(IS_COMPILED_BINARY, sourceShi
314
286
  // imports such as `@oh-my-pi/pi-ai/oauth` continue to resolve directly
315
287
  // against the bundled pi-ai package.
316
288
  const LEGACY_PI_AI_SHIM_PATH = IS_COMPILED_BINARY
317
- ? bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-ai`)
289
+ ? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-ai`)
318
290
  : sourceShimPath("legacy-pi-ai-shim.ts");
319
291
 
320
292
  // The coding-agent's own `./src/index.ts` cannot be listed as an extra
321
293
  // `bun --compile` entrypoint alongside the CLI entry without breaking binary
322
294
  // startup (issue #1474 follow-up). In compiled-binary mode the legacy
323
295
  // `@(scope)/pi-coding-agent` root therefore resolves through the bundled
324
- // registry shim; in dev / source-link / installed-package mode it points at
325
- // the sibling source shim whose distinct file path avoids the #1474 collision
296
+ // module shim; in dev / source-link / installed-package mode it points at the
297
+ // sibling source shim whose distinct file path avoids the #1474 collision
326
298
  // while still re-exporting the canonical package surface.
327
299
  const LEGACY_PI_CODING_AGENT_SHIM_PATH = IS_COMPILED_BINARY
328
- ? bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-coding-agent`)
300
+ ? bundledModuleVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-coding-agent`)
329
301
  : sourceShimPath("legacy-pi-coding-agent-shim.ts");
330
302
 
331
303
  // Package-root overrides. Shim entries (`pi-ai`, `pi-coding-agent`) always
@@ -346,8 +318,8 @@ const LEGACY_PI_CODING_AGENT_SHIM_PATH = IS_COMPILED_BINARY
346
318
  /**
347
319
  * Drop overrides whose filesystem targets are missing so they can fall
348
320
  * through to the canonical-resolution path. Virtual `omp-legacy-pi-bundled:`
349
- * entries always pass — the bundled registry is the source of truth in
350
- * compiled-binary mode where bunfs paths are unreachable (issue #3423).
321
+ * entries always pass — live bundled module references are the source of truth
322
+ * in compiled mode where bunfs paths are unreachable (issue #3423).
351
323
  *
352
324
  * `pathExistsSync` defaults to `fs.existsSync`; tests inject a stub to
353
325
  * simulate the missing-entrypoint failure mode without touching the real FS.
@@ -356,41 +328,59 @@ export function __validateLegacyPiPackageRootOverrides(
356
328
  candidates: Record<string, string>,
357
329
  pathExistsSync: (p: string) => boolean = fs.existsSync,
358
330
  ): Record<string, string> {
359
- return Object.fromEntries(
360
- Object.entries(candidates).filter(
361
- ([, candidate]) => isBundledVirtualSpecifier(candidate) || pathExistsSync(candidate),
362
- ),
363
- );
331
+ const valid: Record<string, string> = {};
332
+ for (const key in candidates) {
333
+ const candidate = candidates[key];
334
+ if (candidate && (isBundledVirtualSpecifier(candidate) || pathExistsSync(candidate))) {
335
+ valid[key] = candidate;
336
+ }
337
+ }
338
+ return valid;
364
339
  }
365
340
 
366
341
  /**
367
342
  * Compute the override map keyed by every canonical specifier the host serves
368
343
  * directly: the pi-ai / pi-coding-agent roots (compat shims that re-attach
369
- * legacy helpers) plus, in compiled-binary mode, every other canonical pi-*
370
- * package root AND every non-wildcard subpath registered in the bundled
371
- * registry (see `legacy-pi-bundled-keys.ts`). Subpath coverage is what stops
372
- * `@(scope)/pi-ai/oauth` and friends from falling through to the extension's
373
- * own — possibly absent — peer install when bunfs filesystem walks fail
374
- * (issue #3442 follow-up to #3423). Exported as a test seam so the
375
- * compiled-binary branch is verifiable from dev tests.
344
+ * legacy helpers) plus, in compiled mode, every build-supplied module key.
345
+ * Subpath coverage stops `@(scope)/pi-ai/oauth` and friends from falling
346
+ * through to the extension's absent peer install when bunfs walks fail.
376
347
  */
377
- export function __buildLegacyPiPackageRootOverrides(isCompiled: boolean): Record<string, string> {
348
+ export function __buildLegacyPiPackageRootOverrides(
349
+ isCompiled: boolean,
350
+ bundledModuleKeys: Iterable<string> = [],
351
+ ): Record<string, string> {
378
352
  const candidates: Record<string, string> = {
379
353
  [`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
380
354
  [`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
381
355
  };
382
356
  if (isCompiled) {
383
- for (const key of BUNDLED_PI_REGISTRY_KEYS) {
384
- // Shim-bearing roots above already mapped to their compat surface;
385
- // the bundled typebox shim has a dedicated TYPEBOX_SHIM_PATH route.
386
- if (key in candidates || key === TYPEBOX_BUNDLED_REGISTRY_KEY) continue;
387
- candidates[key] = bundledRegistryVirtualSpecifier(key);
357
+ for (const key of bundledModuleKeys) {
358
+ // Shim-bearing roots already map to their compat surfaces; TypeBox
359
+ // has a dedicated TYPEBOX_SHIM_PATH route.
360
+ if (key in candidates || key === TYPEBOX_BUNDLED_MODULE_KEY) continue;
361
+ candidates[key] = bundledModuleVirtualSpecifier(key);
388
362
  }
389
363
  }
390
364
  return __validateLegacyPiPackageRootOverrides(candidates);
391
365
  }
392
366
 
393
- const LEGACY_PI_PACKAGE_ROOT_OVERRIDES = __buildLegacyPiPackageRootOverrides(IS_COMPILED_BINARY);
367
+ // Seeded with compat roots at module init; first compiled extension load adds
368
+ // every key supplied by the in-memory build module.
369
+ let legacyPiPackageRootOverrides = __buildLegacyPiPackageRootOverrides(IS_COMPILED_BINARY);
370
+ let legacyPiOverridesReadyPromise: Promise<void> | null = null;
371
+
372
+ /** Complete compiled-mode overrides once from the lazily evaluated host modules. */
373
+ function ensureLegacyPiOverridesReady(): Promise<void> {
374
+ if (!IS_COMPILED_BINARY) {
375
+ return Promise.resolve();
376
+ }
377
+ if (!legacyPiOverridesReadyPromise) {
378
+ legacyPiOverridesReadyPromise = ensureBundledModulesLoaded().then(modules => {
379
+ legacyPiPackageRootOverrides = __buildLegacyPiPackageRootOverrides(true, Object.keys(modules));
380
+ });
381
+ }
382
+ return legacyPiOverridesReadyPromise;
383
+ }
394
384
 
395
385
  let isLegacyPiSpecifierShimInstalled = false;
396
386
 
@@ -428,7 +418,7 @@ function getResolvedSpecifier(specifier: string): string {
428
418
  * specifiers.
429
419
  */
430
420
  function resolveCanonicalPiSpecifier(remappedSpecifier: string): string {
431
- const override = LEGACY_PI_PACKAGE_ROOT_OVERRIDES[remappedSpecifier];
421
+ const override = legacyPiPackageRootOverrides[remappedSpecifier];
432
422
  if (override) {
433
423
  return override;
434
424
  }
@@ -438,7 +428,7 @@ function resolveCanonicalPiSpecifier(remappedSpecifier: string): string {
438
428
  function toImportSpecifier(resolvedPath: string): string {
439
429
  // Virtual `omp-legacy-pi-bundled:` specifiers are served by the synthetic
440
430
  // onLoad in `installLegacyPiSpecifierShim()`; wrapping them as `file://`
441
- // would corrupt the scheme and bypass the bundled registry.
431
+ // would corrupt the scheme.
442
432
  if (isBundledVirtualSpecifier(resolvedPath)) {
443
433
  return resolvedPath;
444
434
  }
@@ -492,6 +482,9 @@ async function rewriteLegacyExtensionSource(
492
482
  importerPath: string,
493
483
  mtimeTag: string | null = null,
494
484
  ): Promise<string> {
485
+ // Compiled mode completes the override map from the build-supplied module
486
+ // keys on first use; every rewrite path must see the full map.
487
+ await ensureLegacyPiOverridesReady();
495
488
  const withPi = rewriteLegacyPiImports(source);
496
489
  // When the TypeBox shim is missing (release build dropped the entrypoint —
497
490
  // issue #3414), leave bare specifiers untouched so Bun resolves a real
@@ -506,10 +499,11 @@ async function rewriteLegacyExtensionSource(
506
499
  : withPi;
507
500
  const withPkg = await rewriteExtensionPackageImports(withTypeBox, importerPath, mtimeTag);
508
501
  const withBare = await rewriteExtensionBareImports(withPkg, importerPath, mtimeTag);
502
+ const withNativeAddons = await rewriteExtensionNativeAddonRequires(withBare, importerPath);
509
503
  if (!mtimeTag) {
510
- return withBare;
504
+ return withNativeAddons;
511
505
  }
512
- return withBare.replace(
506
+ return withNativeAddons.replace(
513
507
  RELATIVE_GRAPH_IMPORT_SPECIFIER_REGEX,
514
508
  (_match, prefix: string, specifier: string, suffix: string) => `${prefix}${specifier}?mtime=${mtimeTag}${suffix}`,
515
509
  );
@@ -950,6 +944,118 @@ async function resolveExtensionBareDependencyUncached(specifier: string, importe
950
944
  return resolveNodePackageDependency(specifier, importerPath);
951
945
  }
952
946
 
947
+ const NATIVE_ADDON_EXTENSION = ".node";
948
+
949
+ // Match CommonJS require calls so bare native-addon specifiers can be pinned
950
+ // to absolute paths. Only requires whose resolution lands on a `.node` addon
951
+ // are rewritten; everything else stays on Bun's native resolver.
952
+ const NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX = /(\brequire\s*\(\s*["'])([^"'()\s]+)(["']\s*\))/g;
953
+
954
+ /**
955
+ * Resolve a bare specifier whose target is a native `.node` addon — either a
956
+ * package subpath ending in `.node`, or a package whose `main` points at an
957
+ * addon (the napi-rs per-platform package convention, e.g.
958
+ * `@yuuang/ffi-rs-darwin-arm64` → `ffi-rs.darwin-arm64.node`). Returns the
959
+ * addon's absolute realpath, or null when the specifier is not a native addon.
960
+ */
961
+ async function resolveExtensionNativeAddon(specifier: string, importerPath: string): Promise<string | null> {
962
+ if (!isBareExtensionDependencySpecifier(specifier)) {
963
+ return null;
964
+ }
965
+
966
+ const cacheKey = `${specifier}\0${path.resolve(path.dirname(importerPath))}`;
967
+ const cached = nativeAddonResolutionCache.get(cacheKey);
968
+ if (cached) return cached;
969
+
970
+ const promise = resolveExtensionNativeAddonUncached(specifier, importerPath);
971
+ nativeAddonResolutionCache.set(cacheKey, promise);
972
+ return promise;
973
+ }
974
+
975
+ async function resolveExtensionNativeAddonUncached(specifier: string, importerPath: string): Promise<string | null> {
976
+ const parsed = splitBarePackageSpecifier(specifier);
977
+ if (!parsed) return null;
978
+ const packageRoot = await findNodePackageRoot(parsed.name, importerPath);
979
+ if (!packageRoot) return null;
980
+
981
+ let target: string | null = null;
982
+ if (parsed.subpath !== null) {
983
+ target = parsed.subpath.endsWith(NATIVE_ADDON_EXTENSION) ? path.join(packageRoot, parsed.subpath) : null;
984
+ } else {
985
+ const manifest = await readPackageManifest(packageRoot);
986
+ const main = manifest?.main;
987
+ target =
988
+ typeof main === "string" && main.endsWith(NATIVE_ADDON_EXTENSION) ? path.resolve(packageRoot, main) : null;
989
+ }
990
+ if (!target || !(await pathExists(target))) {
991
+ return null;
992
+ }
993
+ return realpathOrSelf(target);
994
+ }
995
+
996
+ /**
997
+ * Rewrite bare `require()` specifiers that resolve to native `.node` addons
998
+ * into absolute-path requires. In `bun build --compile` binaries, Bun's bare
999
+ * resolution fails for packages whose `main` is a `.node` addon ("Cannot find
1000
+ * module '@scope/pkg-<platform>'") even when the package sits in the
1001
+ * extension's own node_modules; requiring the addon by absolute path works.
1002
+ */
1003
+ async function rewriteExtensionNativeAddonRequires(source: string, importerPath: string): Promise<string> {
1004
+ let rewritten = "";
1005
+ let lastIndex = 0;
1006
+ for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
1007
+ const matchIndex = match.index;
1008
+ if (matchIndex === undefined) continue;
1009
+
1010
+ const [fullMatch, prefix, specifier, suffix] = match;
1011
+ if (!prefix || !specifier || !suffix) continue;
1012
+
1013
+ const resolved = await resolveExtensionNativeAddon(specifier, importerPath);
1014
+ if (!resolved) continue;
1015
+
1016
+ rewritten += source.slice(lastIndex, matchIndex);
1017
+ // Forward slashes keep Windows paths valid inside single- or double-quoted literals.
1018
+ rewritten += `${prefix}${stripWindowsExtendedLengthPathPrefix(resolved).replaceAll("\\", "/")}${suffix}`;
1019
+ lastIndex = matchIndex + fullMatch.length;
1020
+ }
1021
+
1022
+ if (lastIndex === 0) {
1023
+ return source;
1024
+ }
1025
+ return `${rewritten}${source.slice(lastIndex)}`;
1026
+ }
1027
+
1028
+ /**
1029
+ * Whether a module's source contains a bare require that resolves to a native
1030
+ * `.node` addon — i.e. a napi-rs style loader that must be hooked into the
1031
+ * extension graph so {@link rewriteExtensionNativeAddonRequires} can pin its
1032
+ * platform-package requires to absolute paths.
1033
+ */
1034
+ async function moduleRequiresNativeAddon(modulePath: string): Promise<boolean> {
1035
+ const cached = nativeAddonRequireScanCache.get(modulePath);
1036
+ if (cached) return cached;
1037
+
1038
+ const promise = moduleRequiresNativeAddonUncached(modulePath);
1039
+ nativeAddonRequireScanCache.set(modulePath, promise);
1040
+ return promise;
1041
+ }
1042
+
1043
+ async function moduleRequiresNativeAddonUncached(modulePath: string): Promise<boolean> {
1044
+ let source: string;
1045
+ try {
1046
+ source = await Bun.file(modulePath).text();
1047
+ } catch {
1048
+ return false;
1049
+ }
1050
+ for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
1051
+ const specifier = match[2];
1052
+ if (specifier && (await resolveExtensionNativeAddon(specifier, modulePath))) {
1053
+ return true;
1054
+ }
1055
+ }
1056
+ return false;
1057
+ }
1058
+
953
1059
  async function rewriteExtensionBareImports(
954
1060
  source: string,
955
1061
  importerPath: string,
@@ -986,7 +1092,9 @@ function escapeRegExp(value: string): string {
986
1092
  // `imports` aliases such as `#src/*`, and extension-local bare dependency
987
1093
  // entries. Bare imports inside node_modules dependencies remain native Bun
988
1094
  // resolutions; once the dependency entry is hooked, its relative children are
989
- // still collected and rewritten with the reload mtime tag.
1095
+ // still collected and rewritten with the reload mtime tag. `require()` calls
1096
+ // are scanned too so CJS entries and napi-rs loaders reached without an
1097
+ // import statement still join the graph.
990
1098
  const EXTENSION_GRAPH_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])([^"'()\s]+)(["'])/g;
991
1099
 
992
1100
  // Extension source realpaths already covered by an installed load-time hook for
@@ -1027,6 +1135,10 @@ async function realpathOrSelfUncached(p: string): Promise<string> {
1027
1135
  * Extension-local bare dependency entries are also included so their relative
1028
1136
  * children receive the reload mtime tag; bare imports inside those dependencies
1029
1137
  * remain native Bun resolutions to avoid taking over full third-party graphs.
1138
+ * CommonJS dependency entries stay native too, with one exception: napi-rs
1139
+ * style loaders whose bare requires resolve to `.node` addons are hooked so
1140
+ * their requires can be pinned to absolute paths (unresolvable by bare
1141
+ * specifier inside `bun build --compile` binaries).
1030
1142
  */
1031
1143
  async function collectExtensionModules(entryRealPath: string): Promise<Map<string, string>> {
1032
1144
  const modules = new Map<string, string>();
@@ -1047,14 +1159,24 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
1047
1159
  let source: string;
1048
1160
  try {
1049
1161
  source = await Bun.file(file).text();
1162
+ if (nativeAddonLoaderModulePaths.has(file)) {
1163
+ // CJS requires cannot await an async onLoad hook. Resolve and
1164
+ // rewrite native-addon paths before installing its sync hook.
1165
+ source = await rewriteExtensionNativeAddonRequires(source, file);
1166
+ }
1050
1167
  } catch {
1051
1168
  continue;
1052
1169
  }
1053
1170
  modules.set(file, source);
1054
1171
  const dir = path.dirname(file);
1172
+ const specifiers = new Set<string>();
1055
1173
  for (const match of source.matchAll(EXTENSION_GRAPH_SPECIFIER_REGEX)) {
1056
- const specifier = match[2];
1057
- if (!specifier) continue;
1174
+ if (match[2]) specifiers.add(match[2]);
1175
+ }
1176
+ for (const match of source.matchAll(NATIVE_ADDON_REQUIRE_SPECIFIER_REGEX)) {
1177
+ if (match[2]) specifiers.add(match[2]);
1178
+ }
1179
+ for (const specifier of specifiers) {
1058
1180
  try {
1059
1181
  let resolved: string | null = null;
1060
1182
  let nextFollowsBareDependencies = followBareDependencies;
@@ -1079,10 +1201,17 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
1079
1201
  dependencyExtension === ".cjs" ||
1080
1202
  dependencyExtension === ".cts" ||
1081
1203
  ((dependencyExtension === ".js" || dependencyExtension === ".jsx") && manifest?.type !== "module");
1082
- resolved =
1083
- dependencyEntry && hasSourceModuleExtension(dependencyEntry) && !isCommonJsEntry
1084
- ? await realpathOrSelf(dependencyEntry)
1085
- : null;
1204
+ const isHookableEntry = Boolean(dependencyEntry && hasSourceModuleExtension(dependencyEntry));
1205
+ const hookCommonJsEntry =
1206
+ isHookableEntry && isCommonJsEntry && dependencyEntry
1207
+ ? await moduleRequiresNativeAddon(dependencyEntry)
1208
+ : false;
1209
+ if (isHookableEntry && dependencyEntry && (!isCommonJsEntry || hookCommonJsEntry)) {
1210
+ resolved = await realpathOrSelf(dependencyEntry);
1211
+ if (hookCommonJsEntry) {
1212
+ nativeAddonLoaderModulePaths.add(resolved);
1213
+ }
1214
+ }
1086
1215
  nextFollowsBareDependencies = false;
1087
1216
  }
1088
1217
  if (resolved && !modules.has(resolved)) {
@@ -1100,39 +1229,71 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
1100
1229
  }
1101
1230
 
1102
1231
  /**
1103
- * Install a `Bun.plugin()` `onLoad` hook scoped to a set of extension-owned
1104
- * source modules. Runtime `onLoad` cannot fall through (Bun requires a result
1105
- * object), so every hook uses an exact-path alternation for modules known to be
1106
- * part of this entry's graph; reloads add supplemental hooks for newly
1107
- * discovered modules instead of widening an existing filter to unrelated files.
1232
+ * Install exact-path load hooks for the current extension graph. ESM/TS source
1233
+ * retains the async rewrite path. Native-addon CJS loaders use a synchronous
1234
+ * hook with source pre-rewritten during graph collection; Bun rejects a CJS
1235
+ * `require()` whose onLoad callback returns a promise.
1108
1236
  */
1109
- function installExtensionGraphHook(entryRealPath: string, modules: Map<string, string>): void {
1110
- const alternation = [...modules.keys()].map(escapeRegExp).join("|");
1111
- const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
1112
- const hookId = Bun.hash(`${entryRealPath}\0${[...modules.keys()].join("\0")}`).toString(36);
1113
- Bun.plugin({
1114
- name: `omp:legacy-pi-ext:${hookId}`,
1115
- setup(build) {
1116
- build.onLoad({ filter, namespace: "file" }, async args => {
1117
- const queryIndex = args.path.indexOf("?mtime=");
1118
- const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
1119
- const mtimeTag = queryIndex >= 0 ? args.path.slice(queryIndex + "?mtime=".length) : null;
1120
- const cached = modules.get(sourcePath);
1121
- let raw: string;
1122
- if (cached !== undefined) {
1123
- // consume-once: preserves ?mtime edit-pickup for the re-imported entry
1124
- modules.delete(sourcePath);
1125
- raw = cached;
1126
- } else {
1127
- raw = await Bun.file(sourcePath).text();
1128
- }
1129
- return {
1130
- contents: await rewriteLegacyExtensionSource(raw, sourcePath, mtimeTag),
1131
- loader: getLoader(sourcePath),
1132
- };
1133
- });
1134
- },
1135
- });
1237
+ function installExtensionGraphHook(
1238
+ entryRealPath: string,
1239
+ modules: Map<string, string>,
1240
+ ): { asyncModules: Map<string, string>; syncCommonJsModules: Map<string, string> } {
1241
+ const asyncModules = new Map<string, string>();
1242
+ const syncCommonJsModules = new Map<string, string>();
1243
+ for (const [modulePath, source] of modules) {
1244
+ const destination = nativeAddonLoaderModulePaths.has(modulePath) ? syncCommonJsModules : asyncModules;
1245
+ destination.set(modulePath, source);
1246
+ }
1247
+
1248
+ if (asyncModules.size > 0) {
1249
+ const alternation = [...asyncModules.keys()].map(escapeRegExp).join("|");
1250
+ const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
1251
+ const hookId = Bun.hash(`${entryRealPath}\0async\0${[...asyncModules.keys()].join("\0")}`).toString(36);
1252
+ Bun.plugin({
1253
+ name: `omp:legacy-pi-ext:${hookId}`,
1254
+ setup(build) {
1255
+ build.onLoad({ filter, namespace: "file" }, async args => {
1256
+ const queryIndex = args.path.indexOf("?mtime=");
1257
+ const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
1258
+ const mtimeTag = queryIndex >= 0 ? args.path.slice(queryIndex + "?mtime=".length) : null;
1259
+ const cached = asyncModules.get(sourcePath);
1260
+ let raw: string;
1261
+ if (cached !== undefined) {
1262
+ // consume-once: preserves ?mtime edit-pickup for re-imports
1263
+ asyncModules.delete(sourcePath);
1264
+ raw = cached;
1265
+ } else {
1266
+ raw = await Bun.file(sourcePath).text();
1267
+ }
1268
+ return {
1269
+ contents: await rewriteLegacyExtensionSource(raw, sourcePath, mtimeTag),
1270
+ loader: getLoader(sourcePath),
1271
+ };
1272
+ });
1273
+ },
1274
+ });
1275
+ }
1276
+
1277
+ if (syncCommonJsModules.size > 0) {
1278
+ const alternation = [...syncCommonJsModules.keys()].map(escapeRegExp).join("|");
1279
+ const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
1280
+ const hookId = Bun.hash(`${entryRealPath}\0sync-cjs\0${[...syncCommonJsModules.keys()].join("\0")}`).toString(36);
1281
+ Bun.plugin({
1282
+ name: `omp:legacy-pi-ext:${hookId}`,
1283
+ setup(build) {
1284
+ build.onLoad({ filter, namespace: "file" }, args => {
1285
+ const queryIndex = args.path.indexOf("?mtime=");
1286
+ const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
1287
+ const source = syncCommonJsModules.get(sourcePath);
1288
+ if (source === undefined) {
1289
+ throw new Error(`Missing pre-rewritten CommonJS extension source: ${sourcePath}`);
1290
+ }
1291
+ return { contents: source, loader: getLoader(sourcePath) };
1292
+ });
1293
+ },
1294
+ });
1295
+ }
1296
+ return { asyncModules, syncCommonJsModules };
1136
1297
  }
1137
1298
 
1138
1299
  /**
@@ -1140,10 +1301,10 @@ function installExtensionGraphHook(entryRealPath: string, modules: Map<string, s
1140
1301
  * rewrite hook. The entry graph can grow across reloads, so each call collects
1141
1302
  * the current graph and registers hooks for paths not covered by earlier loads.
1142
1303
  *
1143
- * Returns the newly collected path→source map so the caller can drop entries
1144
- * the import never consumed; `undefined` when no new modules were discovered.
1304
+ * Returns a clearable handle to drop cached sources that weren't consumed
1305
+ * during the initial load; `undefined` when no new modules were discovered.
1145
1306
  */
1146
- async function ensureExtensionGraphHook(entryRealPath: string): Promise<Map<string, string> | undefined> {
1307
+ async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(): void } | undefined> {
1147
1308
  const currentModules = await collectExtensionModules(entryRealPath);
1148
1309
  let hookedModules = extensionGraphHookModules.get(entryRealPath);
1149
1310
  if (!hookedModules) {
@@ -1161,11 +1322,16 @@ async function ensureExtensionGraphHook(entryRealPath: string): Promise<Map<stri
1161
1322
  return undefined;
1162
1323
  }
1163
1324
 
1164
- installExtensionGraphHook(entryRealPath, pendingModules);
1325
+ const { asyncModules, syncCommonJsModules } = installExtensionGraphHook(entryRealPath, pendingModules);
1165
1326
  for (const modulePath of pendingModules.keys()) {
1166
1327
  hookedModules.add(modulePath);
1167
1328
  }
1168
- return pendingModules;
1329
+ return {
1330
+ clear() {
1331
+ asyncModules.clear();
1332
+ syncCommonJsModules.clear();
1333
+ },
1334
+ };
1169
1335
  }
1170
1336
 
1171
1337
  /**
@@ -1184,6 +1350,7 @@ export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown>
1184
1350
  // `bun link`/pnpm installs) so the rewrite filter matches the path Bun
1185
1351
  // actually hands the hook.
1186
1352
  const entryRealPath = await realpathOrSelf(path.resolve(resolvedPath));
1353
+ await ensureLegacyPiOverridesReady();
1187
1354
  const pendingSources = await ensureExtensionGraphHook(entryRealPath);
1188
1355
  try {
1189
1356
  // Dynamic import is required: legacy extension entry paths are user/plugin supplied at runtime.
@@ -1261,9 +1428,8 @@ export function installLegacyPiSpecifierShim(): void {
1261
1428
  setup(build) {
1262
1429
  build.onResolve({ filter: LEGACY_PI_SPECIFIER_FILTER, namespace: "file" }, resolveLegacyPiSpecifier);
1263
1430
  build.onResolve({ filter: TYPEBOX_SPECIFIER_FILTER, namespace: "file" }, resolveTypeBoxSpecifier);
1264
- // Compiled-binary mode: serve `omp-legacy-pi-bundled:<key>` imports
1265
- // from the JS-heap registry. The rewrite path emits these specifiers
1266
- // in place of unreachable `file:///$bunfs/...` URLs (issue #3423).
1431
+ // Compiled mode serves `omp-legacy-pi-bundled:<key>` imports from
1432
+ // live host module references. No bunfs path leaves this loader.
1267
1433
  build.onLoad({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, async args => {
1268
1434
  return { contents: await synthesizeBundledModuleSource(args.path), loader: "js" };
1269
1435
  });