@oh-my-pi/pi-coding-agent 16.1.18 → 16.1.19
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/CHANGELOG.md +9 -0
- package/dist/cli.js +15389 -15103
- package/dist/types/edit/hashline/filesystem.d.ts +1 -18
- package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +4 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +12 -0
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +0 -2
- package/dist/types/modes/controllers/input-controller.d.ts +0 -1
- package/dist/types/tools/path-utils.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +20 -0
- package/scripts/generate-legacy-pi-bundled-registry.ts +404 -0
- package/src/edit/hashline/filesystem.ts +14 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +987 -0
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +3330 -18
- package/src/extensibility/plugins/legacy-pi-compat.ts +29 -14
- package/src/internal-urls/local-protocol.ts +116 -9
- package/src/internal-urls/skill-protocol.ts +3 -3
- package/src/internal-urls/types.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +7 -5
- package/src/modes/components/custom-editor.ts +6 -16
- package/src/modes/controllers/input-controller.ts +0 -71
- package/src/session/messages.ts +70 -47
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/find.ts +1 -0
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/read.ts +43 -17
- package/src/tools/search.ts +4 -0
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coding-agent specific {@link Filesystem} adapter for the hashline patcher.
|
|
3
|
-
*
|
|
4
|
-
* Wires hashline's storage abstraction to the agent runtime:
|
|
5
|
-
*
|
|
6
|
-
* - Section paths are resolved through the plan-mode redirect so a bare
|
|
7
|
-
* `PLAN.md` lands at the canonical session artifact location.
|
|
8
|
-
* - Reads go through `readEditFileText` (notebook-aware) and the
|
|
9
|
-
* auto-generated-file guard.
|
|
10
|
-
* - Writes go through `serializeEditFileText` (notebook-aware) and the
|
|
11
|
-
* LSP writethrough, with FS-scan cache invalidation on success. The
|
|
12
|
-
* resulting `FileDiagnosticsResult` is captured per-path so the
|
|
13
|
-
* orchestrator can attach it to the tool result.
|
|
14
|
-
*
|
|
15
|
-
* Construct one per `executeHashlineSingle` call: per-section state
|
|
16
|
-
* (batch request, diagnostics) lives on the instance and isn't safe to
|
|
17
|
-
* share across concurrent edit tools.
|
|
18
|
-
*/
|
|
19
1
|
import { Filesystem, type WriteResult } from "@oh-my-pi/hashline";
|
|
20
2
|
import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
21
3
|
import type { ToolSession } from "../../tools";
|
|
@@ -50,6 +32,7 @@ export declare class HashlineFilesystem extends Filesystem {
|
|
|
50
32
|
consumeDiagnostics(path: string): FileDiagnosticsResult | undefined;
|
|
51
33
|
resolveAbsolute(relativePath: string): string;
|
|
52
34
|
canonicalPath(relativePath: string): string;
|
|
35
|
+
allowTagPathRecovery(authoredPath: string, resolvedPath: string): boolean;
|
|
53
36
|
readText(relativePath: string): Promise<string>;
|
|
54
37
|
preflightWrite(relativePath: string): Promise<void>;
|
|
55
38
|
writeText(relativePath: string, content: string): Promise<WriteResult>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical keys served by the `omp-legacy-pi-bundled:` virtual namespace.
|
|
3
|
+
* Mirrors `Object.keys(BUNDLED_PI_REGISTRY)` from
|
|
4
|
+
* `legacy-pi-bundled-keys.ts`'s sibling registry file. `legacy-pi-compat.ts`
|
|
5
|
+
* statically imports this set to seed `LEGACY_PI_PACKAGE_ROOT_OVERRIDES` in
|
|
6
|
+
* compiled-binary mode without dragging the heavy registry's transitive
|
|
7
|
+
* graph into dev/test runs (the registry itself stays behind a dynamic
|
|
8
|
+
* import — see `ensureBundledRegistryLoaded` in `legacy-pi-compat.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string>;
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Canonical specifier → live module namespace. Keys MUST match the right-hand
|
|
3
3
|
* side of `bundledRegistryVirtualSpecifier(...)` calls in
|
|
4
4
|
* `legacy-pi-compat.ts`; the synthesizer enumerates each namespace's own
|
|
5
|
-
* enumerable exports at extension load time.
|
|
5
|
+
* enumerable exports at extension load time. The companion
|
|
6
|
+
* `legacy-pi-bundled-keys.ts` mirrors `Object.keys(BUNDLED_PI_REGISTRY)` and
|
|
7
|
+
* is statically imported by `legacy-pi-compat.ts` to seed the override map
|
|
8
|
+
* without paying the cascade above.
|
|
6
9
|
*/
|
|
7
10
|
export declare const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
@@ -49,6 +49,18 @@ export declare function __resolveTypeBoxShimPath(isCompiled: boolean, sourcePath
|
|
|
49
49
|
* simulate the missing-entrypoint failure mode without touching the real FS.
|
|
50
50
|
*/
|
|
51
51
|
export declare function __validateLegacyPiPackageRootOverrides(candidates: Record<string, string>, pathExistsSync?: (p: string) => boolean): Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Compute the override map keyed by every canonical specifier the host serves
|
|
54
|
+
* directly: the pi-ai / pi-coding-agent roots (compat shims that re-attach
|
|
55
|
+
* legacy helpers) plus, in compiled-binary mode, every other canonical pi-*
|
|
56
|
+
* package root AND every non-wildcard subpath registered in the bundled
|
|
57
|
+
* registry (see `legacy-pi-bundled-keys.ts`). Subpath coverage is what stops
|
|
58
|
+
* `@(scope)/pi-ai/oauth` and friends from falling through to the extension's
|
|
59
|
+
* own — possibly absent — peer install when bunfs filesystem walks fail
|
|
60
|
+
* (issue #3442 follow-up to #3423). Exported as a test seam so the
|
|
61
|
+
* compiled-binary branch is verifiable from dev tests.
|
|
62
|
+
*/
|
|
63
|
+
export declare function __buildLegacyPiPackageRootOverrides(isCompiled: boolean): Record<string, string>;
|
|
52
64
|
/**
|
|
53
65
|
* Load a legacy Pi extension module from its real on-disk location.
|
|
54
66
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
1
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Validate that a path is safe (no traversal, no absolute paths).
|
|
4
4
|
*/
|
|
@@ -9,6 +9,6 @@ export declare function validateRelativePath(relativePath: string): void;
|
|
|
9
9
|
export declare class SkillProtocolHandler implements ProtocolHandler {
|
|
10
10
|
readonly scheme = "skill";
|
|
11
11
|
readonly immutable = true;
|
|
12
|
-
resolve(url: InternalUrl): Promise<InternalResource>;
|
|
12
|
+
resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource>;
|
|
13
13
|
complete(): Promise<UrlCompletion[]>;
|
|
14
14
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`) are resolved by tools like read,
|
|
5
5
|
* providing access to agent outputs and server resources without exposing filesystem paths.
|
|
6
6
|
*/
|
|
7
|
+
import type { Skill } from "../extensibility/skills";
|
|
7
8
|
import type { LocalProtocolOptions } from "./local-protocol";
|
|
8
9
|
/**
|
|
9
10
|
* Raw resource payload returned by protocol handlers. The `immutable` flag is
|
|
@@ -85,6 +86,8 @@ export interface ResolveContext {
|
|
|
85
86
|
* [#1608](https://github.com/can1357/oh-my-pi/issues/1608).
|
|
86
87
|
*/
|
|
87
88
|
localProtocolOptions?: LocalProtocolOptions;
|
|
89
|
+
/** Calling session's loaded skills. Prefer this over process-global skill state. */
|
|
90
|
+
skills?: readonly Skill[];
|
|
88
91
|
}
|
|
89
92
|
/**
|
|
90
93
|
* Caller context for write operations dispatched to host-owned URI handlers.
|
|
@@ -87,8 +87,6 @@ export declare class CustomEditor extends Editor {
|
|
|
87
87
|
onPasteImage?: () => Promise<boolean>;
|
|
88
88
|
/** Called when a bracketed paste contains one or more image-file paths. */
|
|
89
89
|
onPasteImagePath?: (path: string) => void | Promise<void>;
|
|
90
|
-
/** Called when a bracketed paste contains one or more non-image file paths. */
|
|
91
|
-
onPasteFilePath?: (path: string) => void | Promise<void>;
|
|
92
90
|
/** Called when the configured raw text-paste shortcut is pressed. */
|
|
93
91
|
onPasteTextRaw?: () => void;
|
|
94
92
|
/** Called when the configured dequeue shortcut is pressed. */
|
|
@@ -38,7 +38,6 @@ export declare class InputController {
|
|
|
38
38
|
abort?: boolean;
|
|
39
39
|
currentText?: string;
|
|
40
40
|
}): number;
|
|
41
|
-
handleFilePathPaste(filePath: string): Promise<void>;
|
|
42
41
|
handleImagePathPaste(path: string): Promise<void>;
|
|
43
42
|
handleImagePaste(): Promise<boolean>;
|
|
44
43
|
handleClipboardTextRawPaste(): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Skill } from "../extensibility/skills";
|
|
1
2
|
import { type LocalProtocolOptions } from "../internal-urls";
|
|
2
3
|
export declare function expandTilde(filePath: string, home?: string): string;
|
|
3
4
|
export declare function expandPath(filePath: string): string;
|
|
@@ -189,6 +190,8 @@ export interface ToolScopeOptions {
|
|
|
189
190
|
signal?: AbortSignal;
|
|
190
191
|
/** Calling session's `local://` root mapping — pins resolutions to the calling session. */
|
|
191
192
|
localProtocolOptions?: LocalProtocolOptions;
|
|
193
|
+
/** Calling session's loaded skills — lets skill:// resolve without process-global state. */
|
|
194
|
+
skills?: readonly Skill[];
|
|
192
195
|
}
|
|
193
196
|
export interface ToolScopeResolution {
|
|
194
197
|
searchPath: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.19",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
49
49
|
"@babel/parser": "^7.29.7",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@oh-my-pi/hashline": "16.1.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.1.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.1.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.1.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.1.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
51
|
+
"@oh-my-pi/hashline": "16.1.19",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.1.19",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.1.19",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.1.19",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.1.19",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.1.19",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.1.19",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.1.19",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.1.19",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.1.19",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.1.19",
|
|
62
62
|
"@opentelemetry/api": "^1.9.1",
|
|
63
63
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
64
64
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/scripts/build-binary.ts
CHANGED
|
@@ -51,6 +51,17 @@ async function main(): Promise<void> {
|
|
|
51
51
|
try {
|
|
52
52
|
await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]);
|
|
53
53
|
await runCommand(["bun", "scripts/generate-docs-index.ts", "--generate"]);
|
|
54
|
+
// `legacy-pi-bundled-registry.ts` static-imports
|
|
55
|
+
// `@oh-my-pi/pi-coding-agent/export/html` (one of pi-coding-agent's
|
|
56
|
+
// named subpath exports, see scripts/generate-legacy-pi-bundled-registry.ts),
|
|
57
|
+
// whose source pulls in `tool-views.generated.js`. The root
|
|
58
|
+
// `package.json` "prepare" lifecycle hook builds that file on
|
|
59
|
+
// `bun install`, but a clean binary build that skips install hooks
|
|
60
|
+
// would `bun build --compile` against the registry entry and fail
|
|
61
|
+
// resolving the missing generated bundle. Rebuilding the tool views
|
|
62
|
+
// here makes the compile self-contained and matches what `prepack`
|
|
63
|
+
// does for the npm bundle.
|
|
64
|
+
await runCommand(["bun", "--cwd=../collab-web", "run", "build:tool-views"]);
|
|
54
65
|
await runCommand(
|
|
55
66
|
["bun", "--cwd=../natives", "run", "embed:native"],
|
|
56
67
|
crossTarget
|
|
@@ -58,6 +69,15 @@ async function main(): Promise<void> {
|
|
|
58
69
|
: Bun.env,
|
|
59
70
|
);
|
|
60
71
|
await runCommand(["bun", "scripts/embed-mupdf-wasm.ts", "--generate"]);
|
|
72
|
+
// Regenerate the bundled-pi registry + key set before the compile so any
|
|
73
|
+
// new pi-* subpath export added under `packages/*/package.json` is served
|
|
74
|
+
// from the host's in-process copy. Without this, `bun build --compile`
|
|
75
|
+
// would freeze whatever the committed registry happened to enumerate at
|
|
76
|
+
// the time of the last manual `--generate`, and a new subpath added
|
|
77
|
+
// since then would crash extension validation with `Cannot find module`
|
|
78
|
+
// (issue #3442). The generator also normalizes formatting, so the diff
|
|
79
|
+
// against the committed copy stays clean.
|
|
80
|
+
await runCommand(["bun", "scripts/generate-legacy-pi-bundled-registry.ts", "--generate"]);
|
|
61
81
|
try {
|
|
62
82
|
const buildEnv = shouldAdhocSignDarwinBinary() ? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" } : Bun.env;
|
|
63
83
|
await runCommand(
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate the bundled-pi registry + key set served by the compiled binary's
|
|
5
|
+
* `omp-legacy-pi-bundled:` virtual namespace.
|
|
6
|
+
*
|
|
7
|
+
* Compiled-mode extension validation routes every `@(scope)/pi-*` import
|
|
8
|
+
* through this registry — bunfs filesystem APIs are unreachable on Bun 1.3.14+
|
|
9
|
+
* (issue #3423), so the binary serves bundled module surfaces from JS-heap
|
|
10
|
+
* references captured at build time. Bare package roots and every
|
|
11
|
+
* `non-wildcard` subpath export declared in each bundled pi-* package.json
|
|
12
|
+
* become a registry entry; wildcard subpath patterns are intentionally
|
|
13
|
+
* unbundled — those resolve from the extension's own peer deps as before.
|
|
14
|
+
*
|
|
15
|
+
* The generator emits two files:
|
|
16
|
+
* - `legacy-pi-bundled-registry.ts` (heavy): static imports of every
|
|
17
|
+
* subpath module + the `BUNDLED_PI_REGISTRY` map. Dynamically loaded by
|
|
18
|
+
* `legacy-pi-compat.ts` so dev/test runs never pay the cascade.
|
|
19
|
+
* - `legacy-pi-bundled-keys.ts` (light): just the canonical-key set.
|
|
20
|
+
* Statically imported by `legacy-pi-compat.ts` to seed
|
|
21
|
+
* `LEGACY_PI_PACKAGE_ROOT_OVERRIDES` without touching the heavy graph.
|
|
22
|
+
*
|
|
23
|
+
* Run via `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`
|
|
24
|
+
* (also invoked from `scripts/build-binary.ts` before `bun build --compile`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from "node:fs/promises";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
|
|
30
|
+
const packageDir = path.resolve(import.meta.dir, "..");
|
|
31
|
+
const repoRoot = path.resolve(packageDir, "..", "..");
|
|
32
|
+
const registryOutPath = path.join(packageDir, "src/extensibility/plugins/legacy-pi-bundled-registry.ts");
|
|
33
|
+
const keysOutPath = path.join(packageDir, "src/extensibility/plugins/legacy-pi-bundled-keys.ts");
|
|
34
|
+
|
|
35
|
+
const GENERATE_FLAG = "--generate";
|
|
36
|
+
const CHECK_FLAG = "--check";
|
|
37
|
+
|
|
38
|
+
interface BundledPackage {
|
|
39
|
+
readonly dir: string;
|
|
40
|
+
readonly name: string;
|
|
41
|
+
/** Identifier prefix for generated namespace imports (`PiAi`, `PiCodingAgent`, …). */
|
|
42
|
+
readonly identifier: string;
|
|
43
|
+
/** Root import — the shim path for surfaces that wrap the bundled namespace, `null` otherwise. */
|
|
44
|
+
readonly rootShim: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PACKAGES: readonly BundledPackage[] = [
|
|
48
|
+
{ dir: "packages/agent", name: "@oh-my-pi/pi-agent-core", identifier: "PiAgentCore", rootShim: null },
|
|
49
|
+
{
|
|
50
|
+
dir: "packages/ai",
|
|
51
|
+
name: "@oh-my-pi/pi-ai",
|
|
52
|
+
identifier: "PiAi",
|
|
53
|
+
// pi-ai 15.1.0 dropped the runtime `Type` builder from the package root;
|
|
54
|
+
// the shim re-attaches it for extensions that still import `Type` from
|
|
55
|
+
// `@(scope)/pi-ai`. Subpaths bypass the shim — they're untouched by the
|
|
56
|
+
// schema-runtime split.
|
|
57
|
+
rootShim: "../legacy-pi-ai-shim",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
dir: "packages/coding-agent",
|
|
61
|
+
name: "@oh-my-pi/pi-coding-agent",
|
|
62
|
+
identifier: "PiCodingAgent",
|
|
63
|
+
// pi-coding-agent root carries legacy helpers (`defineTool`,
|
|
64
|
+
// `createCodingTools`, …) the canonical entry never exposed; the shim
|
|
65
|
+
// re-exports the canonical surface plus those helpers.
|
|
66
|
+
rootShim: "../legacy-pi-coding-agent-shim",
|
|
67
|
+
},
|
|
68
|
+
{ dir: "packages/natives", name: "@oh-my-pi/pi-natives", identifier: "PiNatives", rootShim: null },
|
|
69
|
+
{ dir: "packages/tui", name: "@oh-my-pi/pi-tui", identifier: "PiTui", rootShim: null },
|
|
70
|
+
{ dir: "packages/utils", name: "@oh-my-pi/pi-utils", identifier: "PiUtils", rootShim: null },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// `typebox` is published under an upstream alias; legacy extensions import the
|
|
74
|
+
// bare name expecting the host-provided Zod-backed shim. Tracked alongside the
|
|
75
|
+
// pi-* surfaces so the override map and synthesizer cover it uniformly.
|
|
76
|
+
const TYPEBOX_REGISTRY_KEY = "typebox";
|
|
77
|
+
const TYPEBOX_SHIM_IMPORT = "../typebox";
|
|
78
|
+
|
|
79
|
+
interface RegistryEntry {
|
|
80
|
+
/** Canonical registry key, e.g. `@oh-my-pi/pi-ai/oauth`. */
|
|
81
|
+
readonly key: string;
|
|
82
|
+
/** Identifier bound in the generated module's static import. */
|
|
83
|
+
readonly binding: string;
|
|
84
|
+
/**
|
|
85
|
+
* ES module specifier the generated file imports. For bundled subpaths this
|
|
86
|
+
* is the canonical `@oh-my-pi/<pkg>/<subpath>` string Bun resolves via the
|
|
87
|
+
* package's exports field; for shimmed surfaces (root of pi-ai / pi-coding-agent
|
|
88
|
+
* / typebox) it's the relative path to the in-tree shim.
|
|
89
|
+
*/
|
|
90
|
+
readonly importSpecifier: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function bindingForSubpath(identifier: string, subpath: string): string {
|
|
94
|
+
const segments = subpath
|
|
95
|
+
.split("/")
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map(segment =>
|
|
98
|
+
segment
|
|
99
|
+
.split(/[-_]/)
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
102
|
+
.join(""),
|
|
103
|
+
);
|
|
104
|
+
return `bundled${identifier}${segments.join("")}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Skip files whose presence on disk is meaningful to the build pipeline rather
|
|
108
|
+
// than something a plugin would import: editor backups (`_*`, `.*`), tests,
|
|
109
|
+
// declaration files, and conventional `index` files (already covered by the
|
|
110
|
+
// non-wildcard root of the same directory when one is declared).
|
|
111
|
+
const SKIPPED_WILDCARD_BASENAMES = new Set(["index"]);
|
|
112
|
+
|
|
113
|
+
function isSafeWildcardBasename(basename: string): boolean {
|
|
114
|
+
if (!basename || basename.startsWith(".") || basename.startsWith("_")) return false;
|
|
115
|
+
if (SKIPPED_WILDCARD_BASENAMES.has(basename)) return false;
|
|
116
|
+
if (/\.(test|spec|d|generated|bench)$/.test(basename)) return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface WildcardPattern {
|
|
121
|
+
readonly exportPrefix: string;
|
|
122
|
+
readonly exportSuffix: string;
|
|
123
|
+
readonly sourcePrefix: string;
|
|
124
|
+
readonly sourceSuffix: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse a single-asterisk Node exports wildcard into its prefix/suffix halves.
|
|
129
|
+
* Returns `null` for patterns with more than one asterisk or non-relative
|
|
130
|
+
* sources — neither shows up in our packages today and the generator stays
|
|
131
|
+
* conservative rather than guessing.
|
|
132
|
+
*/
|
|
133
|
+
function parseWildcardPattern(exportKey: string, sourcePattern: string): WildcardPattern | null {
|
|
134
|
+
const exportStar = exportKey.indexOf("*");
|
|
135
|
+
const sourceStar = sourcePattern.indexOf("*");
|
|
136
|
+
if (exportStar === -1 || sourceStar === -1) return null;
|
|
137
|
+
if (exportKey.indexOf("*", exportStar + 1) !== -1) return null;
|
|
138
|
+
if (sourcePattern.indexOf("*", sourceStar + 1) !== -1) return null;
|
|
139
|
+
if (!sourcePattern.startsWith("./")) return null;
|
|
140
|
+
return {
|
|
141
|
+
exportPrefix: exportKey.slice(2, exportStar),
|
|
142
|
+
exportSuffix: exportKey.slice(exportStar + 1),
|
|
143
|
+
sourcePrefix: sourcePattern.slice(2, sourceStar),
|
|
144
|
+
sourceSuffix: sourcePattern.slice(sourceStar + 1),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function exportImportTarget(value: unknown): string | null {
|
|
149
|
+
if (typeof value === "string") return value;
|
|
150
|
+
if (value && typeof value === "object" && "import" in value) {
|
|
151
|
+
const target = (value as { import?: unknown }).import;
|
|
152
|
+
return typeof target === "string" ? target : null;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function collectEntries(): Promise<RegistryEntry[]> {
|
|
158
|
+
const entries: RegistryEntry[] = [];
|
|
159
|
+
const seenKeys = new Set<string>();
|
|
160
|
+
function pushEntry(key: string, binding: string, importSpecifier: string): void {
|
|
161
|
+
if (seenKeys.has(key)) return;
|
|
162
|
+
seenKeys.add(key);
|
|
163
|
+
entries.push({ key, binding, importSpecifier });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const pkg of PACKAGES) {
|
|
167
|
+
const manifestPath = path.join(repoRoot, pkg.dir, "package.json");
|
|
168
|
+
const manifest = (await Bun.file(manifestPath).json()) as { name?: string; exports?: Record<string, unknown> };
|
|
169
|
+
if (manifest.name !== pkg.name) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`generate-legacy-pi-bundled-registry: package.json at ${manifestPath} declares "${manifest.name}", expected "${pkg.name}"`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const exportsField = manifest.exports ?? {};
|
|
175
|
+
// Root: shim if one is declared, otherwise the canonical package.
|
|
176
|
+
pushEntry(pkg.name, `bundled${pkg.identifier}`, pkg.rootShim ?? pkg.name);
|
|
177
|
+
// Pass 1: every non-wildcard subpath export becomes its own registry key.
|
|
178
|
+
for (const exportKey in exportsField) {
|
|
179
|
+
if (!exportKey.startsWith("./") || exportKey === "." || exportKey.includes("*")) continue;
|
|
180
|
+
const subpath = exportKey.slice(2);
|
|
181
|
+
pushEntry(`${pkg.name}/${subpath}`, bindingForSubpath(pkg.identifier, subpath), `${pkg.name}/${subpath}`);
|
|
182
|
+
}
|
|
183
|
+
// Pass 2: expand wildcard exports against the source tree so plugins can
|
|
184
|
+
// import concrete subpath targets — e.g. `@(scope)/pi-ai/oauth/anthropic`
|
|
185
|
+
// remaps to `@oh-my-pi/pi-ai/oauth/anthropic`, covered by pi-ai's
|
|
186
|
+
// `./oauth/*` export pattern, which Node only resolves at runtime against
|
|
187
|
+
// a real `node_modules`. Compiled bunfs can't resolve at runtime, so we
|
|
188
|
+
// statically enumerate the concrete files now (issue #3442 follow-up).
|
|
189
|
+
// Root catch-all patterns (`./*`, `./*.js`) are skipped intentionally:
|
|
190
|
+
// the pi-coding-agent root is the binary entry's source tree, so static-
|
|
191
|
+
// importing every top-level file would drag `cli.ts`/`main.ts` through a
|
|
192
|
+
// second graph and explode the bundle for no plugin-facing benefit.
|
|
193
|
+
for (const exportKey in exportsField) {
|
|
194
|
+
if (!exportKey.startsWith("./") || exportKey === "." || !exportKey.includes("*")) continue;
|
|
195
|
+
const sourcePattern = exportImportTarget(exportsField[exportKey]);
|
|
196
|
+
if (!sourcePattern) continue;
|
|
197
|
+
const pattern = parseWildcardPattern(exportKey, sourcePattern);
|
|
198
|
+
if (!pattern) continue;
|
|
199
|
+
// Limit to JS-loadable source modules. `./prompts/*` mapping to
|
|
200
|
+
// `*.md` would emit a `import * as foo from "@(pkg)/prompts/<name>"`
|
|
201
|
+
// that Bun can't load as a JS module.
|
|
202
|
+
if (!/\.(ts|tsx|mts|cts|js|mjs|cjs|jsx)$/.test(pattern.sourceSuffix)) continue;
|
|
203
|
+
// Skip root catch-alls (prefix is empty before the wildcard). See
|
|
204
|
+
// the explanatory block comment above for the bundle-explosion
|
|
205
|
+
// reasoning. Named wildcards like `./oauth/*` keep `oauth/` here.
|
|
206
|
+
if (pattern.exportPrefix === "" || pattern.exportPrefix === "/") continue;
|
|
207
|
+
|
|
208
|
+
const sourceDir = path.join(repoRoot, pkg.dir, pattern.sourcePrefix);
|
|
209
|
+
try {
|
|
210
|
+
const glob = new Bun.Glob(`*${pattern.sourceSuffix}`);
|
|
211
|
+
const matches: string[] = [];
|
|
212
|
+
for await (const match of glob.scan({ cwd: sourceDir, onlyFiles: true })) {
|
|
213
|
+
matches.push(match);
|
|
214
|
+
}
|
|
215
|
+
matches.sort();
|
|
216
|
+
for (const match of matches) {
|
|
217
|
+
if (!match.endsWith(pattern.sourceSuffix)) continue;
|
|
218
|
+
const basename = match.slice(0, match.length - pattern.sourceSuffix.length);
|
|
219
|
+
if (!isSafeWildcardBasename(basename)) continue;
|
|
220
|
+
if (basename.includes("/")) continue;
|
|
221
|
+
const subpath = `${pattern.exportPrefix}${basename}${pattern.exportSuffix}`;
|
|
222
|
+
const key = `${pkg.name}/${subpath}`;
|
|
223
|
+
pushEntry(key, bindingForSubpath(pkg.identifier, subpath), key);
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// Missing source dir means the wildcard is declared in
|
|
227
|
+
// package.json but the implementation tree hasn't shipped that
|
|
228
|
+
// folder yet. Leave it to runtime resolution.
|
|
229
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
entries.push({
|
|
234
|
+
key: TYPEBOX_REGISTRY_KEY,
|
|
235
|
+
binding: "bundledTypeBoxShim",
|
|
236
|
+
importSpecifier: TYPEBOX_SHIM_IMPORT,
|
|
237
|
+
});
|
|
238
|
+
const seenBindings = new Set<string>();
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (seenBindings.has(entry.binding)) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`generate-legacy-pi-bundled-registry: duplicate binding ${entry.binding} for key ${entry.key}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
seenBindings.add(entry.binding);
|
|
246
|
+
}
|
|
247
|
+
return entries;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderRegistry(entries: readonly RegistryEntry[]): string {
|
|
251
|
+
const importLines = entries.map(
|
|
252
|
+
entry => `import * as ${entry.binding} from ${JSON.stringify(entry.importSpecifier)};`,
|
|
253
|
+
);
|
|
254
|
+
const registryLines = entries.map(
|
|
255
|
+
entry => `\t${JSON.stringify(entry.key)}: ${entry.binding} as unknown as Readonly<Record<string, unknown>>,`,
|
|
256
|
+
);
|
|
257
|
+
return [
|
|
258
|
+
"// AUTO-GENERATED by scripts/generate-legacy-pi-bundled-registry.ts.",
|
|
259
|
+
"// Do not edit by hand — run `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`.",
|
|
260
|
+
"/**",
|
|
261
|
+
" * Static handles on every bundled `@oh-my-pi/pi-*` surface — package",
|
|
262
|
+
" * roots plus every non-wildcard subpath export declared in each package's",
|
|
263
|
+
" * `exports` field. Loaded lazily by `legacy-pi-compat.ts` in compiled-binary",
|
|
264
|
+
" * mode (issue #3423) and re-exported through the `omp-legacy-pi-bundled:`",
|
|
265
|
+
" * virtual namespace — bunfs paths cannot be resolved at runtime on Bun",
|
|
266
|
+
" * 1.3.14+, so the only way to re-route extension imports onto the host's",
|
|
267
|
+
" * in-process copy is via live module references captured at compile time.",
|
|
268
|
+
" *",
|
|
269
|
+
" * This module is split out from `legacy-pi-compat.ts` so dev/test runs that",
|
|
270
|
+
" * touch the compat layer never trigger the cascade through",
|
|
271
|
+
" * `legacy-pi-coding-agent-shim.ts → ../index → export/html/...` (which",
|
|
272
|
+
" * requires generated artifacts that only exist after a `bun run build`).",
|
|
273
|
+
" *",
|
|
274
|
+
" * The bundler reaches every entry below via standard static-import analysis,",
|
|
275
|
+
" * so no `--compile` extras are required in `scripts/build-binary.ts`.",
|
|
276
|
+
" */",
|
|
277
|
+
...importLines,
|
|
278
|
+
"",
|
|
279
|
+
"/**",
|
|
280
|
+
" * Canonical specifier → live module namespace. Keys MUST match the right-hand",
|
|
281
|
+
" * side of `bundledRegistryVirtualSpecifier(...)` calls in",
|
|
282
|
+
" * `legacy-pi-compat.ts`; the synthesizer enumerates each namespace's own",
|
|
283
|
+
" * enumerable exports at extension load time. The companion",
|
|
284
|
+
" * `legacy-pi-bundled-keys.ts` mirrors `Object.keys(BUNDLED_PI_REGISTRY)` and",
|
|
285
|
+
" * is statically imported by `legacy-pi-compat.ts` to seed the override map",
|
|
286
|
+
" * without paying the cascade above.",
|
|
287
|
+
" */",
|
|
288
|
+
"export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {",
|
|
289
|
+
...registryLines,
|
|
290
|
+
"};",
|
|
291
|
+
"",
|
|
292
|
+
].join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderKeys(entries: readonly RegistryEntry[]): string {
|
|
296
|
+
const keyLines = entries.map(entry => `\t${JSON.stringify(entry.key)},`);
|
|
297
|
+
return [
|
|
298
|
+
"// AUTO-GENERATED by scripts/generate-legacy-pi-bundled-registry.ts.",
|
|
299
|
+
"// Do not edit by hand — run `bun scripts/generate-legacy-pi-bundled-registry.ts --generate`.",
|
|
300
|
+
"/**",
|
|
301
|
+
" * Canonical keys served by the `omp-legacy-pi-bundled:` virtual namespace.",
|
|
302
|
+
" * Mirrors `Object.keys(BUNDLED_PI_REGISTRY)` from",
|
|
303
|
+
" * `legacy-pi-bundled-keys.ts`'s sibling registry file. `legacy-pi-compat.ts`",
|
|
304
|
+
" * statically imports this set to seed `LEGACY_PI_PACKAGE_ROOT_OVERRIDES` in",
|
|
305
|
+
" * compiled-binary mode without dragging the heavy registry's transitive",
|
|
306
|
+
" * graph into dev/test runs (the registry itself stays behind a dynamic",
|
|
307
|
+
" * import — see `ensureBundledRegistryLoaded` in `legacy-pi-compat.ts`).",
|
|
308
|
+
" */",
|
|
309
|
+
"export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([",
|
|
310
|
+
...keyLines,
|
|
311
|
+
"]);",
|
|
312
|
+
"",
|
|
313
|
+
].join("\n");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function formatInPlace(targets: readonly string[]): Promise<void> {
|
|
317
|
+
// `biome check --write` runs the formatter AND the assist's organizeImports
|
|
318
|
+
// pass; `biome format --write` alone leaves the import order untouched.
|
|
319
|
+
const proc = Bun.spawn(["bunx", "biome", "check", "--write", ...targets], {
|
|
320
|
+
cwd: packageDir,
|
|
321
|
+
stdin: "ignore",
|
|
322
|
+
stdout: "pipe",
|
|
323
|
+
stderr: "pipe",
|
|
324
|
+
});
|
|
325
|
+
const exit = await proc.exited;
|
|
326
|
+
if (exit !== 0) {
|
|
327
|
+
const stderr = await new Response(proc.stderr).text();
|
|
328
|
+
throw new Error(`biome check --write failed (exit ${exit}): ${stderr}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function main(): Promise<void> {
|
|
333
|
+
const args = process.argv.slice(2);
|
|
334
|
+
const check = args.includes(CHECK_FLAG);
|
|
335
|
+
if (!check && !args.includes(GENERATE_FLAG)) {
|
|
336
|
+
console.log(
|
|
337
|
+
`Skipping bundled-pi registry generation; pass ${GENERATE_FLAG} to write the output files (or ${CHECK_FLAG} to verify the committed copy).`,
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const entries = await collectEntries();
|
|
343
|
+
const registrySource = renderRegistry(entries);
|
|
344
|
+
const keysSource = renderKeys(entries);
|
|
345
|
+
|
|
346
|
+
if (check) {
|
|
347
|
+
// biome ignores paths outside its `includes` glob (the `*.ts.candidate`
|
|
348
|
+
// suffix above would be rejected with "No files were processed"), so
|
|
349
|
+
// write the candidates into a sibling `.<name>.tmp/` directory whose
|
|
350
|
+
// basenames match the committed copies. The directory lives under the
|
|
351
|
+
// same package so biome's repo-relative `includes` still cover it.
|
|
352
|
+
const tmpDir = path.join(packageDir, "src/extensibility/plugins/.legacy-pi-bundled-candidates");
|
|
353
|
+
const tmpRegistry = path.join(tmpDir, path.basename(registryOutPath));
|
|
354
|
+
const tmpKeys = path.join(tmpDir, path.basename(keysOutPath));
|
|
355
|
+
try {
|
|
356
|
+
await Bun.write(tmpRegistry, registrySource);
|
|
357
|
+
await Bun.write(tmpKeys, keysSource);
|
|
358
|
+
await formatInPlace([tmpRegistry, tmpKeys]);
|
|
359
|
+
const drift: string[] = [];
|
|
360
|
+
const pairs: readonly (readonly [string, string])[] = [
|
|
361
|
+
[registryOutPath, tmpRegistry],
|
|
362
|
+
[keysOutPath, tmpKeys],
|
|
363
|
+
];
|
|
364
|
+
for (const [committedPath, candidatePath] of pairs) {
|
|
365
|
+
let committed: string;
|
|
366
|
+
try {
|
|
367
|
+
committed = await Bun.file(committedPath).text();
|
|
368
|
+
} catch {
|
|
369
|
+
committed = "";
|
|
370
|
+
}
|
|
371
|
+
const candidate = await Bun.file(candidatePath).text();
|
|
372
|
+
if (committed !== candidate) {
|
|
373
|
+
drift.push(path.relative(repoRoot, committedPath));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (drift.length > 0) {
|
|
377
|
+
console.error(
|
|
378
|
+
`generate-legacy-pi-bundled-registry: stale output — rerun with ${GENERATE_FLAG}. Files out of sync:\n ${drift.join("\n ")}`,
|
|
379
|
+
);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
console.log("generate-legacy-pi-bundled-registry: OK");
|
|
383
|
+
} finally {
|
|
384
|
+
// `fs.rm` recursively handles both files and the parent tmp dir; the
|
|
385
|
+
// individual file deletes the previous draft used `Bun.file().delete()`
|
|
386
|
+
// for would leave the empty directory behind.
|
|
387
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await Bun.write(registryOutPath, registrySource);
|
|
393
|
+
await Bun.write(keysOutPath, keysSource);
|
|
394
|
+
// Hand-emitted formatting can't perfectly match biome's organizeImports +
|
|
395
|
+
// long-line wrapping (lineWidth 120 + 60+ entries with long keys/bindings),
|
|
396
|
+
// so let biome rewrite the files in place. The committed output then matches
|
|
397
|
+
// what `bun check` enforces, and `--check` confirms zero drift on CI.
|
|
398
|
+
await formatInPlace([registryOutPath, keysOutPath]);
|
|
399
|
+
console.log(
|
|
400
|
+
`Generated ${path.relative(repoRoot, registryOutPath)} and ${path.relative(repoRoot, keysOutPath)} (${entries.length} entries)`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
await main();
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* (batch request, diagnostics) lives on the instance and isn't safe to
|
|
17
17
|
* share across concurrent edit tools.
|
|
18
18
|
*/
|
|
19
|
+
import * as path from "node:path";
|
|
19
20
|
import { Filesystem, NotFoundError, type WriteResult } from "@oh-my-pi/hashline";
|
|
20
21
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
21
22
|
import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
@@ -23,6 +24,7 @@ import type { ToolSession } from "../../tools";
|
|
|
23
24
|
import { routeWriteThroughBridge } from "../../tools/acp-bridge";
|
|
24
25
|
import { assertEditableFileContent } from "../../tools/auto-generated-guard";
|
|
25
26
|
import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
|
|
27
|
+
import { isInternalUrlPath } from "../../tools/path-utils";
|
|
26
28
|
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
27
29
|
import { canonicalSnapshotKey } from "../file-snapshot-store";
|
|
28
30
|
import { readEditFileText, serializeEditFileText } from "../read-file";
|
|
@@ -86,6 +88,18 @@ export class HashlineFilesystem extends Filesystem {
|
|
|
86
88
|
return canonicalSnapshotKey(this.resolveAbsolute(relativePath));
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
allowTagPathRecovery(authoredPath: string, resolvedPath: string): boolean {
|
|
92
|
+
// Internal-URL authored targets (`local://`, `vault://`, …) are approved
|
|
93
|
+
// at the lower "read" privilege; never let one redirect onto a "write".
|
|
94
|
+
if (isInternalUrlPath(authoredPath)) return false;
|
|
95
|
+
// Recovery only fixes a mistyped working-tree path: confine the resolved
|
|
96
|
+
// target to the working tree (realpath-normalized). A plain-path "write"
|
|
97
|
+
// approval must not redirect into the artifact sandbox, the secret vault,
|
|
98
|
+
// or any out-of-tree file the user never named.
|
|
99
|
+
const root = canonicalSnapshotKey(this.session.cwd);
|
|
100
|
+
return resolvedPath === root || resolvedPath.startsWith(`${root}${path.sep}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
async readText(relativePath: string): Promise<string> {
|
|
90
104
|
const absolutePath = this.resolveAbsolute(relativePath);
|
|
91
105
|
let content: string;
|