pi-lazy-extensions 0.1.11 → 0.1.13
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.
- package/README.md +8 -1
- package/package.json +7 -7
- package/registry.ts +33 -39
package/README.md
CHANGED
|
@@ -15,13 +15,20 @@ _ToolSearch-style proxy tool — extensions only load when you need them._
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## Pi 1.0 compatibility (0.1.12)
|
|
19
|
+
|
|
20
|
+
Tested against Pi **1.0.0** with exact SDK development pins and wildcard host peers.
|
|
21
|
+
Lazy entrypoints now force Jiti transpilation: native ESM must not bypass mapped host constructors. Failed entrypoint evaluation is not retried with native import, and API interception restores exact original methods. Host probes cover TS/ESM identity, callable-only idle withdrawal/reactivation and native nested execution.
|
|
22
|
+
|
|
23
|
+
Run `bun run test:host` for the offline real-host regression. Set `PI1_HOST_PACKAGE` to an installed Pi package directory and `PI1_HOST_ENTRY=bundle` to test its bundled CLI runtime.
|
|
24
|
+
|
|
18
25
|
## Pi 0.99 compatibility (0.1.11)
|
|
19
26
|
|
|
20
27
|
Tested with Pi **0.99.0**. Host-provided Pi packages and TypeBox are peers (`*`), not bundled runtime dependencies; development uses exact Pi 0.99.0 pins and host-compatible TypeBox where needed.
|
|
21
28
|
|
|
22
29
|
Lazy modules reuse Pi's exact extension-loader virtual modules (SDK and bundled CLI), including import-only ESM/compat exports; they never import a second physical copy of the host runtime. Idle unload withdraws `codemode`/`deferred` tools with native `hidden` exposure; reactivation restores their original exposure without rerunning factories.
|
|
23
30
|
|
|
24
|
-
Run `bun run test:host` for the offline real-host load, native codemode/nested-call, module-identity and reload checks. Set `
|
|
31
|
+
Run `bun run test:host` for the offline real-host load, native codemode/nested-call, module-identity and reload checks. Set `PI1_HOST_PACKAGE` to an installed Pi package directory to test that host explicitly; add `PI1_HOST_ENTRY=bundle` to check the bundled CLI runtime's constructors.
|
|
25
32
|
|
|
26
33
|
## The Problem
|
|
27
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lazy-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Lazy-load pi extensions on demand via a ToolSearch-style proxy tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"typebox": "*"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
-
"test:host": "node scripts/
|
|
41
|
+
"test:host": "node scripts/pi1-host.mjs",
|
|
42
42
|
"test": "vitest run",
|
|
43
43
|
"test:watch": "vitest",
|
|
44
44
|
"test:coverage": "vitest run --coverage",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
"knip": "6.14.1",
|
|
54
54
|
"typescript": "6.0.3",
|
|
55
55
|
"vitest": "4.1.7",
|
|
56
|
-
"@earendil-works/pi-coding-agent": "
|
|
57
|
-
"@earendil-works/pi-ai": "
|
|
58
|
-
"@earendil-works/pi-agent-core": "
|
|
59
|
-
"@earendil-works/pi-tui": "
|
|
56
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
57
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
58
|
+
"@earendil-works/pi-agent-core": "1.1.0",
|
|
59
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
60
60
|
"typebox": "1.3.27"
|
|
61
61
|
},
|
|
62
62
|
"overrides": {
|
|
63
|
-
"brace-expansion": "5.0.
|
|
63
|
+
"brace-expansion": "5.0.12",
|
|
64
64
|
"fast-xml-builder": "1.2.0",
|
|
65
65
|
"protobufjs": "8.4.0",
|
|
66
66
|
"ws": "8.20.1"
|
package/registry.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Extension registry - loads and tracks lazy extensions.
|
|
3
3
|
*
|
|
4
4
|
* This is the core of pi-lazy-extensions. It handles:
|
|
5
|
-
* - Dynamic loading of extensions via jiti
|
|
5
|
+
* - Dynamic loading of extensions via host-mapped jiti + factory(pi)
|
|
6
6
|
* - Tracking registered tools/commands per extension
|
|
7
7
|
* - Idle timeout unloading
|
|
8
8
|
* - Deduplication guards
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
11
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
14
|
import type { ActivationResult, LazyExtensionsState } from "./types.js";
|
|
13
15
|
import { resolveExtensionPath } from "./config.js";
|
|
@@ -157,38 +159,38 @@ async function performActivation(
|
|
|
157
159
|
const definitions = new Map<string, Parameters<ExtensionAPI["registerTool"]>[0]>();
|
|
158
160
|
const interceptedEvents: string[] = [];
|
|
159
161
|
|
|
160
|
-
const origRegisterShortcut = (pi as any).registerShortcut
|
|
161
|
-
const origRegisterFlag = (pi as any).registerFlag
|
|
162
|
-
const origRegisterMessageRenderer = (pi as any).registerMessageRenderer
|
|
163
|
-
const origRegisterTool = pi.registerTool
|
|
164
|
-
const origOn = pi.on
|
|
162
|
+
const origRegisterShortcut = (pi as any).registerShortcut;
|
|
163
|
+
const origRegisterFlag = (pi as any).registerFlag;
|
|
164
|
+
const origRegisterMessageRenderer = (pi as any).registerMessageRenderer;
|
|
165
|
+
const origRegisterTool = pi.registerTool;
|
|
166
|
+
const origOn = pi.on;
|
|
165
167
|
|
|
166
168
|
(pi as any).registerShortcut = (shortcut: string, options: any) => {
|
|
167
169
|
interceptedShortcuts.push(shortcut);
|
|
168
|
-
origRegisterShortcut?.(shortcut, options);
|
|
170
|
+
origRegisterShortcut?.call(pi, shortcut, options);
|
|
169
171
|
};
|
|
170
172
|
(pi as any).registerFlag = (flagName: string, options: any) => {
|
|
171
173
|
interceptedFlags.push(flagName);
|
|
172
|
-
origRegisterFlag?.(flagName, options);
|
|
174
|
+
origRegisterFlag?.call(pi, flagName, options);
|
|
173
175
|
};
|
|
174
176
|
(pi as any).registerMessageRenderer = (customType: string, renderer: any) => {
|
|
175
177
|
interceptedRenderers.push(customType);
|
|
176
|
-
origRegisterMessageRenderer?.(customType, renderer);
|
|
178
|
+
origRegisterMessageRenderer?.call(pi, customType, renderer);
|
|
177
179
|
};
|
|
178
180
|
pi.registerTool = (tool: any) => {
|
|
179
181
|
interceptedTools.push(tool.name);
|
|
180
|
-
// Lazy factories share our API owner. In 0
|
|
182
|
+
// Lazy factories share our API owner. In 1.0 re-registering would replace
|
|
181
183
|
// that owner's tool, not silently lose a collision; preserve existing tools.
|
|
182
184
|
if (toolsBefore.has(tool.name)) return;
|
|
183
185
|
definitions.set(tool.name, tool);
|
|
184
|
-
origRegisterTool(tool);
|
|
186
|
+
origRegisterTool.call(pi, tool);
|
|
185
187
|
};
|
|
186
188
|
// 0.86.0: pi.on returns an unsubscribe function and its event name is a
|
|
187
189
|
// closed overload set, so the generic string-typed proxy is asserted to
|
|
188
190
|
// ExtensionAPI["on"]. The `never` cast lets the bound overload set resolve.
|
|
189
191
|
pi.on = ((event: string, handler: any) => {
|
|
190
192
|
interceptedEvents.push(event);
|
|
191
|
-
return origOn(event as never, handler);
|
|
193
|
+
return origOn.call(pi, event as never, handler);
|
|
192
194
|
}) as ExtensionAPI["on"];
|
|
193
195
|
|
|
194
196
|
const extPath = resolveExtensionPath(extState.config.path, state.baseDir);
|
|
@@ -299,8 +301,8 @@ export function resetJitiAliases(): void { hostModules = undefined; }
|
|
|
299
301
|
/**
|
|
300
302
|
* Load an extension module and extract its default factory function.
|
|
301
303
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
+
* Jiti is required for both JS and TS. Never retry a failed evaluation with
|
|
305
|
+
* native import: it can execute side effects twice and bypass host mapping.
|
|
304
306
|
*
|
|
305
307
|
* Jiti receives the same live module objects as Pi's own extension loader,
|
|
306
308
|
* so lazy imports preserve class identity in both SDK and bundled CLI hosts.
|
|
@@ -308,30 +310,22 @@ export function resetJitiAliases(): void { hostModules = undefined; }
|
|
|
308
310
|
async function loadExtensionFactory(
|
|
309
311
|
extPath: string,
|
|
310
312
|
): Promise<((pi: ExtensionAPI) => void | Promise<void>) | undefined> {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return
|
|
327
|
-
} catch {
|
|
328
|
-
// jiti unavailable or failed — fall through to raw import
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Attempt 2: raw import() — works for .js files, no TypeScript support
|
|
332
|
-
const mod = await import(extPath);
|
|
333
|
-
if (typeof mod.default === "function") {
|
|
334
|
-
return mod.default as (pi: ExtensionAPI) => void | Promise<void>;
|
|
313
|
+
const { createJiti } = await import("jiti");
|
|
314
|
+
const jiti = createJiti(import.meta.url, {
|
|
315
|
+
moduleCache: false,
|
|
316
|
+
virtualModules: getHostModules(),
|
|
317
|
+
tryNative: false,
|
|
318
|
+
});
|
|
319
|
+
// Jiti 2.7 treats .mjs (and type:module .js) as native even with
|
|
320
|
+
// tryNative:false. Force the entrypoint through its mapped evaluator so
|
|
321
|
+
// bundled hosts never patch a second SDK and evaluation errors are not retried.
|
|
322
|
+
const filename = fileURLToPath(jiti.esmResolve(extPath));
|
|
323
|
+
const mod = await jiti.evalModule(await readFile(filename, "utf8"), {
|
|
324
|
+
filename, async: true, forceTranspile: true,
|
|
325
|
+
});
|
|
326
|
+
if (typeof mod === "function") return mod as (pi: ExtensionAPI) => void | Promise<void>;
|
|
327
|
+
if (typeof (mod as any)?.default === "function") {
|
|
328
|
+
return (mod as any).default as (pi: ExtensionAPI) => void | Promise<void>;
|
|
335
329
|
}
|
|
336
330
|
return undefined;
|
|
337
331
|
}
|
|
@@ -426,7 +420,7 @@ function idleUnloadExtension(name: string, state: LazyExtensionsState, pi: Exten
|
|
|
426
420
|
const activeTools = pi.getActiveTools();
|
|
427
421
|
extState.activeBeforeUnload = activeTools.filter(t => extState.registeredTools.includes(t));
|
|
428
422
|
const remaining = activeTools.filter(t => !extState.registeredTools.includes(t));
|
|
429
|
-
// Removing an active name does not withdraw codemode/deferred tools in 0.
|
|
423
|
+
// Removing an active name does not withdraw codemode/deferred tools in 1.0.
|
|
430
424
|
// Hidden exposure also removes them from ctx.tools / native nested dispatch.
|
|
431
425
|
for (const definition of extState.toolDefinitions ?? []) {
|
|
432
426
|
if (definition.exposure === "codemode" || definition.exposure === "deferred") {
|