@oh-my-pi/pi-coding-agent 14.8.1 → 14.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/package.json +16 -7
- package/src/config/model-resolver.ts +92 -35
- package/src/config/prompt-templates.ts +1 -1
- package/src/debug/index.ts +21 -0
- package/src/debug/raw-sse-buffer.ts +229 -0
- package/src/debug/raw-sse.ts +213 -0
- package/src/edit/index.ts +9 -10
- package/src/edit/streaming.ts +6 -5
- package/src/eval/js/context-manager.ts +91 -47
- package/src/extensibility/extensions/loader.ts +9 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
- package/src/hashline/anchors.ts +113 -0
- package/src/hashline/apply.ts +732 -0
- package/src/hashline/bigrams.json +649 -0
- package/src/hashline/constants.ts +8 -0
- package/src/hashline/diff-preview.ts +43 -0
- package/src/hashline/diff.ts +56 -0
- package/src/hashline/execute.ts +268 -0
- package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
- package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
- package/src/hashline/index.ts +14 -0
- package/src/hashline/input.ts +110 -0
- package/src/hashline/parser.ts +220 -0
- package/src/hashline/prefixes.ts +101 -0
- package/src/hashline/recovery.ts +72 -0
- package/src/hashline/stream.ts +123 -0
- package/src/hashline/types.ts +69 -0
- package/src/hashline/utils.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +4 -0
- package/src/memories/index.ts +13 -4
- package/src/modes/components/assistant-message.ts +55 -9
- package/src/modes/components/welcome.ts +114 -38
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/interactive-mode.ts +9 -9
- package/src/modes/rpc/rpc-client.ts +53 -2
- package/src/modes/rpc/rpc-mode.ts +67 -1
- package/src/modes/rpc/rpc-types.ts +17 -2
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/prompts/agents/reviewer.md +14 -0
- package/src/prompts/tools/hashline.md +57 -10
- package/src/sdk.ts +4 -3
- package/src/session/agent-session.ts +195 -30
- package/src/session/compaction/branch-summarization.ts +4 -2
- package/src/session/compaction/compaction.ts +22 -3
- package/src/task/executor.ts +21 -2
- package/src/task/index.ts +4 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/match-line-format.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/title-generator.ts +11 -0
- package/src/edit/modes/hashline.ts +0 -2039
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { type Component, matchesKey, padding, replaceTabs, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { theme } from "../modes/theme/theme";
|
|
4
|
+
import { copyToClipboard } from "../utils/clipboard";
|
|
5
|
+
import { formatRawSseIsoTime, type RawSseDebugBuffer, rawSseRecordLines } from "./raw-sse-buffer";
|
|
6
|
+
|
|
7
|
+
const MIN_VIEWER_WIDTH = 20;
|
|
8
|
+
const VIEWER_FRAME_LINES = 5;
|
|
9
|
+
|
|
10
|
+
function sanitizeFrameLine(line: string, width: number): string {
|
|
11
|
+
return truncateToWidth(replaceTabs(sanitizeText(line)), width);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RawSseViewerOptions {
|
|
15
|
+
buffer: RawSseDebugBuffer;
|
|
16
|
+
terminalRows: number;
|
|
17
|
+
onExit: () => void;
|
|
18
|
+
onStatus?: (message: string) => void;
|
|
19
|
+
onUpdate?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class RawSseViewerComponent implements Component {
|
|
23
|
+
readonly #buffer: RawSseDebugBuffer;
|
|
24
|
+
readonly #terminalRows: number;
|
|
25
|
+
readonly #onExit: () => void;
|
|
26
|
+
readonly #onStatus?: (message: string) => void;
|
|
27
|
+
readonly #onUpdate?: () => void;
|
|
28
|
+
readonly #unsubscribe: () => void;
|
|
29
|
+
#scrollOffset = 0;
|
|
30
|
+
#followTail = true;
|
|
31
|
+
#lastRenderWidth = MIN_VIEWER_WIDTH;
|
|
32
|
+
#statusMessage: string | undefined;
|
|
33
|
+
|
|
34
|
+
constructor(options: RawSseViewerOptions) {
|
|
35
|
+
this.#buffer = options.buffer;
|
|
36
|
+
this.#terminalRows = options.terminalRows;
|
|
37
|
+
this.#onExit = options.onExit;
|
|
38
|
+
this.#onStatus = options.onStatus;
|
|
39
|
+
this.#onUpdate = options.onUpdate;
|
|
40
|
+
this.#unsubscribe = this.#buffer.subscribe(() => {
|
|
41
|
+
this.#followIfNeeded();
|
|
42
|
+
this.#onUpdate?.();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
handleInput(keyData: string): void {
|
|
47
|
+
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
48
|
+
this.#unsubscribe();
|
|
49
|
+
this.#onExit();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (matchesKey(keyData, "ctrl+c")) {
|
|
54
|
+
this.#copyAll();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (matchesKey(keyData, "up")) {
|
|
59
|
+
this.#followTail = false;
|
|
60
|
+
this.#scrollOffset = Math.max(0, this.#scrollOffset - 1);
|
|
61
|
+
this.#onUpdate?.();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (matchesKey(keyData, "down")) {
|
|
66
|
+
this.#followTail = false;
|
|
67
|
+
this.#scrollOffset = Math.min(this.#maxScrollOffset(), this.#scrollOffset + 1);
|
|
68
|
+
this.#onUpdate?.();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (matchesKey(keyData, "pageUp")) {
|
|
73
|
+
this.#followTail = false;
|
|
74
|
+
this.#scrollOffset = Math.max(0, this.#scrollOffset - this.#bodyHeight());
|
|
75
|
+
this.#onUpdate?.();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (matchesKey(keyData, "pageDown")) {
|
|
80
|
+
this.#followTail = false;
|
|
81
|
+
this.#scrollOffset = Math.min(this.#maxScrollOffset(), this.#scrollOffset + this.#bodyHeight());
|
|
82
|
+
this.#onUpdate?.();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (matchesKey(keyData, "end")) {
|
|
87
|
+
this.#followTail = true;
|
|
88
|
+
this.#scrollToTail();
|
|
89
|
+
this.#onUpdate?.();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
invalidate(): void {}
|
|
94
|
+
|
|
95
|
+
render(width: number): string[] {
|
|
96
|
+
this.#lastRenderWidth = Math.max(MIN_VIEWER_WIDTH, width);
|
|
97
|
+
this.#followIfNeeded();
|
|
98
|
+
|
|
99
|
+
const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
|
|
100
|
+
const bodyHeight = this.#bodyHeight();
|
|
101
|
+
const rawLines = this.#renderRawLines(innerWidth);
|
|
102
|
+
const body = rawLines.slice(this.#scrollOffset, this.#scrollOffset + bodyHeight);
|
|
103
|
+
while (body.length < bodyHeight) body.push("");
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
this.#frameTop(innerWidth),
|
|
107
|
+
this.#frameLine(this.#summaryText(), innerWidth),
|
|
108
|
+
this.#frameSeparator(innerWidth),
|
|
109
|
+
...body.map(line => this.#frameLine(line, innerWidth)),
|
|
110
|
+
this.#frameLine(this.#statusText(), innerWidth),
|
|
111
|
+
this.#frameBottom(innerWidth),
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#renderRawLines(innerWidth: number): string[] {
|
|
116
|
+
const snapshot = this.#buffer.snapshot();
|
|
117
|
+
if (snapshot.records.length === 0) {
|
|
118
|
+
return [
|
|
119
|
+
theme.fg("muted", "No raw SSE frames captured yet."),
|
|
120
|
+
theme.fg("muted", "HTTP SSE providers populate this view while a model response is streaming."),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lines: string[] = [];
|
|
125
|
+
if (snapshot.droppedRecords > 0) {
|
|
126
|
+
lines.push(
|
|
127
|
+
theme.fg(
|
|
128
|
+
"warning",
|
|
129
|
+
`: omp-debug-dropped records=${snapshot.droppedRecords} chars=${snapshot.droppedChars}`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
lines.push("");
|
|
133
|
+
}
|
|
134
|
+
for (const record of snapshot.records) {
|
|
135
|
+
for (const line of rawSseRecordLines(record)) {
|
|
136
|
+
lines.push(sanitizeFrameLine(line, innerWidth));
|
|
137
|
+
}
|
|
138
|
+
if (record.kind === "event" && record.truncated) {
|
|
139
|
+
lines.push(theme.fg("warning", `: omp-debug-event-truncated originalChars=${record.originalChars}`));
|
|
140
|
+
}
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#summaryText(): string {
|
|
147
|
+
const snapshot = this.#buffer.snapshot();
|
|
148
|
+
const last = snapshot.lastUpdatedAt ? ` last=${formatRawSseIsoTime(snapshot.lastUpdatedAt)}` : "";
|
|
149
|
+
const follow = this.#followTail ? "follow:on" : "follow:off";
|
|
150
|
+
return ` # raw SSE | events=${snapshot.totalEvents} records=${snapshot.records.length}${last} | ${follow} | Esc back Ctrl+C copy End follow`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#statusText(): string {
|
|
154
|
+
return this.#statusMessage ?? " Up/Down scroll PgUp/PgDn page";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#bodyHeight(): number {
|
|
158
|
+
return Math.max(3, this.#terminalRows - VIEWER_FRAME_LINES);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#followIfNeeded(): void {
|
|
162
|
+
if (this.#followTail) this.#scrollToTail();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#scrollToTail(): void {
|
|
166
|
+
this.#scrollOffset = this.#maxScrollOffset();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#maxScrollOffset(): number {
|
|
170
|
+
const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
|
|
171
|
+
return Math.max(0, this.#renderRawLines(innerWidth).length - this.#bodyHeight());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#copyAll(): void {
|
|
175
|
+
const payload = this.#buffer.toRawText();
|
|
176
|
+
if (payload.trim().length === 0) {
|
|
177
|
+
const message = "No raw SSE frames to copy";
|
|
178
|
+
this.#statusMessage = message;
|
|
179
|
+
this.#onStatus?.(message);
|
|
180
|
+
this.#onUpdate?.();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
copyToClipboard(payload);
|
|
186
|
+
const message = "Copied raw SSE stream";
|
|
187
|
+
this.#statusMessage = message;
|
|
188
|
+
this.#onStatus?.(message);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
+
this.#statusMessage = `Copy failed: ${message}`;
|
|
192
|
+
}
|
|
193
|
+
this.#onUpdate?.();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#frameTop(innerWidth: number): string {
|
|
197
|
+
return `${theme.boxSharp.topLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.topRight}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#frameSeparator(innerWidth: number): string {
|
|
201
|
+
return `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.teeLeft}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#frameBottom(innerWidth: number): string {
|
|
205
|
+
return `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.bottomRight}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#frameLine(content: string, innerWidth: number): string {
|
|
209
|
+
const truncated = truncateToWidth(content, innerWidth);
|
|
210
|
+
const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
211
|
+
return `${theme.boxSharp.vertical}${truncated}${padding(remaining)}${theme.boxSharp.vertical}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/edit/index.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { Static } from "@sinclair/typebox";
|
|
4
|
+
import {
|
|
5
|
+
executeHashlineSingle,
|
|
6
|
+
HashlineMismatchError,
|
|
7
|
+
type HashlineParams,
|
|
8
|
+
hashlineEditParamsSchema,
|
|
9
|
+
} from "../hashline";
|
|
10
|
+
import hashlineGrammarTemplate from "../hashline/grammar.lark" with { type: "text" };
|
|
11
|
+
import { resolveHashlineGrammarPlaceholders } from "../hashline/hash";
|
|
4
12
|
import {
|
|
5
13
|
createLspWritethrough,
|
|
6
14
|
type FileDiagnosticsResult,
|
|
@@ -16,16 +24,8 @@ import type { ToolSession } from "../tools";
|
|
|
16
24
|
import { VimTool, vimSchema } from "../tools/vim";
|
|
17
25
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
18
26
|
import type { VimToolDetails } from "../vim/types";
|
|
19
|
-
import { resolveHashlineGrammarPlaceholders } from "./line-hash";
|
|
20
27
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
21
28
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
22
|
-
import {
|
|
23
|
-
executeHashlineSingle,
|
|
24
|
-
HashlineMismatchError,
|
|
25
|
-
type HashlineParams,
|
|
26
|
-
hashlineEditParamsSchema,
|
|
27
|
-
} from "./modes/hashline";
|
|
28
|
-
import hashlineGrammarTemplate from "./modes/hashline.lark" with { type: "text" };
|
|
29
29
|
import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
|
|
30
30
|
import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
|
|
31
31
|
import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
|
|
@@ -34,13 +34,12 @@ export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/ed
|
|
|
34
34
|
export * from "./apply-patch";
|
|
35
35
|
export * from "./diff";
|
|
36
36
|
export * from "./file-read-cache";
|
|
37
|
-
export * from "./line-hash";
|
|
38
37
|
|
|
39
38
|
// Resolve the `$HFMT$` and `$HSEP$` placeholders in the hashline Lark grammar.
|
|
40
39
|
const hashlineGrammar = resolveHashlineGrammarPlaceholders(hashlineGrammarTemplate);
|
|
41
40
|
|
|
41
|
+
export * from "../hashline";
|
|
42
42
|
export * from "./modes/apply-patch";
|
|
43
|
-
export * from "./modes/hashline";
|
|
44
43
|
export * from "./modes/patch";
|
|
45
44
|
export * from "./modes/replace";
|
|
46
45
|
export * from "./normalize";
|
package/src/edit/streaming.ts
CHANGED
|
@@ -12,17 +12,18 @@
|
|
|
12
12
|
* The shared renderer / `ToolExecutionComponent` consult the strategy via
|
|
13
13
|
* the injected `editMode` rather than probing argument shape.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
17
|
-
import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
|
|
18
|
-
import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
15
|
+
|
|
19
16
|
import {
|
|
20
17
|
computeHashlineDiff,
|
|
21
18
|
computeHashlineSectionDiff,
|
|
22
19
|
containsRecognizableHashlineOperations,
|
|
23
20
|
type HashlineInputSection,
|
|
24
21
|
splitHashlineInputs,
|
|
25
|
-
} from "
|
|
22
|
+
} from "../hashline";
|
|
23
|
+
import type { Theme } from "../modes/theme/theme";
|
|
24
|
+
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
25
|
+
import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
|
|
26
|
+
import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
26
27
|
import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
|
|
27
28
|
import type { ReplaceEditEntry } from "./modes/replace";
|
|
28
29
|
|
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import * as util from "node:util";
|
|
6
6
|
import * as vm from "node:vm";
|
|
7
7
|
|
|
8
|
+
import { parse as babelParse } from "@babel/parser";
|
|
8
9
|
import * as Diff from "diff";
|
|
9
10
|
import type { ToolSession } from "../../tools";
|
|
10
11
|
import { ToolError } from "../../tools/tool-errors";
|
|
@@ -488,65 +489,108 @@ function buildRequire(cwd: string): NodeJS.Require {
|
|
|
488
489
|
return createRequire(pathToFileURL(path.join(cwd, "[eval]")).href);
|
|
489
490
|
}
|
|
490
491
|
|
|
491
|
-
// Static `import ... from "x"` is not valid inside vm.runInContext
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
492
|
+
// Static `import ... from "x"` is not valid inside vm.runInContext (script-mode parsing).
|
|
493
|
+
// Rewrite top-level static imports to dynamic `await import(...)` so users can paste ESM
|
|
494
|
+
// source verbatim. We use a real parser instead of regex matching so imports embedded in
|
|
495
|
+
// string literals, template literals, or comments — common in codemods — stay intact.
|
|
496
|
+
|
|
497
|
+
type BabelImportDeclaration = {
|
|
498
|
+
type: "ImportDeclaration";
|
|
499
|
+
start: number;
|
|
500
|
+
end: number;
|
|
501
|
+
source: { value: string };
|
|
502
|
+
specifiers: ReadonlyArray<{
|
|
503
|
+
type: "ImportDefaultSpecifier" | "ImportNamespaceSpecifier" | "ImportSpecifier";
|
|
504
|
+
local: { name: string };
|
|
505
|
+
imported?: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
|
|
506
|
+
}>;
|
|
507
|
+
attributes?: ReadonlyArray<{
|
|
508
|
+
key: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
|
|
509
|
+
value: { value: string };
|
|
510
|
+
}>;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
function buildDynamicImportCall(sourceLiteral: string, withClause: string | undefined): string {
|
|
514
|
+
return withClause ? `import(${sourceLiteral}, { with: ${withClause} })` : `import(${sourceLiteral})`;
|
|
511
515
|
}
|
|
512
516
|
|
|
513
|
-
function
|
|
517
|
+
function buildWithClause(node: BabelImportDeclaration): string | undefined {
|
|
518
|
+
const attrs = node.attributes;
|
|
519
|
+
if (!attrs || attrs.length === 0) return undefined;
|
|
520
|
+
const pairs = attrs.map(attr => {
|
|
521
|
+
const key = attr.key.type === "Identifier" ? attr.key.name : JSON.stringify(attr.key.value);
|
|
522
|
+
return `${key}: ${JSON.stringify(attr.value.value)}`;
|
|
523
|
+
});
|
|
524
|
+
return `{ ${pairs.join(", ")} }`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function rewriteImportNode(node: BabelImportDeclaration): string {
|
|
528
|
+
const sourceLiteral = JSON.stringify(node.source.value);
|
|
529
|
+
const withClause = buildWithClause(node);
|
|
530
|
+
const importCall = buildDynamicImportCall(sourceLiteral, withClause);
|
|
531
|
+
|
|
514
532
|
let defaultName: string | undefined;
|
|
515
533
|
let namespaceName: string | undefined;
|
|
516
|
-
|
|
517
|
-
for (const
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
} else if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
defaultName = part;
|
|
526
|
-
} else {
|
|
527
|
-
return `await import(${sourceLiteral}); /* unrewritten import: ${clause} */`;
|
|
534
|
+
const namedPairs: Array<[string, string]> = [];
|
|
535
|
+
for (const spec of node.specifiers) {
|
|
536
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
537
|
+
defaultName = spec.local.name;
|
|
538
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
539
|
+
namespaceName = spec.local.name;
|
|
540
|
+
} else if (spec.type === "ImportSpecifier" && spec.imported) {
|
|
541
|
+
const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
542
|
+
namedPairs.push([imported, spec.local.name]);
|
|
528
543
|
}
|
|
529
544
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
const props = defaultName ? `default: ${defaultName}, ${
|
|
534
|
-
return `const { ${props} } = await
|
|
545
|
+
|
|
546
|
+
if (namedPairs.length > 0) {
|
|
547
|
+
const inner = namedPairs.map(([imp, loc]) => (imp === loc ? imp : `${imp}: ${loc}`)).join(", ");
|
|
548
|
+
const props = defaultName ? `default: ${defaultName}, ${inner}` : inner;
|
|
549
|
+
return `const { ${props} } = await ${importCall};`;
|
|
535
550
|
}
|
|
536
551
|
if (namespaceName && defaultName) {
|
|
537
|
-
return `const ${namespaceName} = await
|
|
552
|
+
return `const ${namespaceName} = await ${importCall}; const ${defaultName} = ${namespaceName}.default;`;
|
|
538
553
|
}
|
|
539
|
-
if (namespaceName) return `const ${namespaceName} = await
|
|
540
|
-
if (defaultName) return `const ${defaultName} = (await
|
|
541
|
-
return `await
|
|
554
|
+
if (namespaceName) return `const ${namespaceName} = await ${importCall};`;
|
|
555
|
+
if (defaultName) return `const ${defaultName} = (await ${importCall}).default;`;
|
|
556
|
+
return `await ${importCall};`;
|
|
542
557
|
}
|
|
543
558
|
|
|
544
559
|
export function rewriteStaticImports(code: string): string {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
560
|
+
if (!code.includes("import")) return code;
|
|
561
|
+
|
|
562
|
+
let ast: { program: { body: ReadonlyArray<{ type: string }> } };
|
|
563
|
+
try {
|
|
564
|
+
ast = babelParse(code, {
|
|
565
|
+
sourceType: "module",
|
|
566
|
+
allowAwaitOutsideFunction: true,
|
|
567
|
+
allowReturnOutsideFunction: true,
|
|
568
|
+
allowImportExportEverywhere: true,
|
|
569
|
+
allowNewTargetOutsideFunction: true,
|
|
570
|
+
allowSuperOutsideMethod: true,
|
|
571
|
+
allowUndeclaredExports: true,
|
|
572
|
+
errorRecovery: true,
|
|
573
|
+
}) as unknown as typeof ast;
|
|
574
|
+
} catch {
|
|
575
|
+
// Parser bailed entirely — let the VM surface the real syntax error.
|
|
576
|
+
return code;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Only rewrite top-level imports. Anything nested deeper is invalid JS anyway and the
|
|
580
|
+
// VM will report it.
|
|
581
|
+
const imports: BabelImportDeclaration[] = [];
|
|
582
|
+
for (const node of ast.program.body) {
|
|
583
|
+
if (node.type === "ImportDeclaration") imports.push(node as unknown as BabelImportDeclaration);
|
|
584
|
+
}
|
|
585
|
+
if (imports.length === 0) return code;
|
|
586
|
+
|
|
587
|
+
// Splice from the back so earlier offsets stay valid.
|
|
588
|
+
imports.sort((a, b) => b.start - a.start);
|
|
589
|
+
let result = code;
|
|
590
|
+
for (const node of imports) {
|
|
591
|
+
result = result.slice(0, node.start) + rewriteImportNode(node) + result.slice(node.end);
|
|
592
|
+
}
|
|
593
|
+
return result;
|
|
550
594
|
}
|
|
551
595
|
|
|
552
596
|
function wrapCode(code: string): { source: string; asyncWrapped: boolean } {
|
|
@@ -17,7 +17,7 @@ import type { ExecOptions } from "../../exec/exec";
|
|
|
17
17
|
import { execCommand } from "../../exec/exec";
|
|
18
18
|
import type { CustomMessage } from "../../session/messages";
|
|
19
19
|
import { EventBus } from "../../utils/event-bus";
|
|
20
|
-
import { installLegacyPiSpecifierShim } from "../plugins/legacy-pi-compat";
|
|
20
|
+
import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/legacy-pi-compat";
|
|
21
21
|
import { getAllPluginExtensionPaths } from "../plugins/loader";
|
|
22
22
|
|
|
23
23
|
import { resolvePath } from "../utils";
|
|
@@ -36,6 +36,12 @@ import type {
|
|
|
36
36
|
installLegacyPiSpecifierShim();
|
|
37
37
|
|
|
38
38
|
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
|
39
|
+
type LoadedExtensionModule = ExtensionFactory | { default?: ExtensionFactory };
|
|
40
|
+
|
|
41
|
+
function getExtensionFactory(module: LoadedExtensionModule): ExtensionFactory | null {
|
|
42
|
+
const candidate = typeof module === "function" ? module : module.default;
|
|
43
|
+
return typeof candidate === "function" ? candidate : null;
|
|
44
|
+
}
|
|
39
45
|
|
|
40
46
|
export class ExtensionRuntimeNotInitializedError extends Error {
|
|
41
47
|
constructor() {
|
|
@@ -272,8 +278,8 @@ async function loadExtension(
|
|
|
272
278
|
): Promise<{ extension: Extension | null; error: string | null }> {
|
|
273
279
|
const resolvedPath = resolvePath(extensionPath, cwd);
|
|
274
280
|
try {
|
|
275
|
-
const module = await
|
|
276
|
-
const factory = (module
|
|
281
|
+
const module = (await loadLegacyPiModule(resolvedPath)) as LoadedExtensionModule;
|
|
282
|
+
const factory = getExtensionFactory(module);
|
|
277
283
|
|
|
278
284
|
if (typeof factory !== "function") {
|
|
279
285
|
return {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
1
3
|
import * as path from "node:path";
|
|
4
|
+
import * as url from "node:url";
|
|
2
5
|
|
|
3
6
|
const LEGACY_PI_PACKAGE_MAP = {
|
|
4
7
|
"@mariozechner/pi-agent-core": "@oh-my-pi/pi-agent-core",
|
|
@@ -54,6 +57,10 @@ function getResolvedSpecifier(specifier: string): string {
|
|
|
54
57
|
return resolved;
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
function toImportSpecifier(resolvedPath: string): string {
|
|
61
|
+
return url.pathToFileURL(resolvedPath).href;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
function rewriteLegacyPiImports(source: string): string {
|
|
58
65
|
return source.replace(
|
|
59
66
|
LEGACY_PI_IMPORT_SPECIFIER_REGEX,
|
|
@@ -63,42 +70,118 @@ function rewriteLegacyPiImports(source: string): string {
|
|
|
63
70
|
return match;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
return `${prefix}${getResolvedSpecifier(remappedSpecifier)}${suffix}`;
|
|
73
|
+
return `${prefix}${toImportSpecifier(getResolvedSpecifier(remappedSpecifier))}${suffix}`;
|
|
67
74
|
},
|
|
68
75
|
);
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
// Match `from "..."
|
|
78
|
+
// Match static `from "..."` / `from '...'` import specifiers.
|
|
79
|
+
const STATIC_IMPORT_SPECIFIER_REGEX = /(from\s+["'])([^"']+)(["'])/g;
|
|
80
|
+
// Match static imports plus dynamic `import("...")` / `import('...')` specifiers.
|
|
72
81
|
const ANY_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s*\(\s*)["'])([^"']+)(["'])/g;
|
|
73
82
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
/** Resolve bare imports against the extension directory before loading mirrored legacy Pi files. */
|
|
84
|
+
function isUrlLikeSpecifier(specifier: string): boolean {
|
|
85
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shouldPreserveImportSpecifier(specifier: string): boolean {
|
|
89
|
+
return specifier.startsWith(".") || path.isAbsolute(specifier) || isUrlLikeSpecifier(specifier);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toRewrittenImportSpecifier(resolvedPath: string): string {
|
|
93
|
+
return isUrlLikeSpecifier(resolvedPath) ? resolvedPath : toImportSpecifier(resolvedPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
function rewriteBareImportsForLegacyExtension(source: string, importerPath: string): string {
|
|
82
97
|
const importerDir = path.dirname(importerPath);
|
|
83
98
|
return source.replace(ANY_IMPORT_SPECIFIER_REGEX, (match, prefix: string, specifier: string, suffix: string) => {
|
|
84
99
|
// Skip relative, absolute, URL-style, and already-resolved Node specifiers.
|
|
85
|
-
if (
|
|
86
|
-
specifier.startsWith(".") ||
|
|
87
|
-
specifier.startsWith("/") ||
|
|
88
|
-
specifier.startsWith("node:") ||
|
|
89
|
-
specifier.includes("://")
|
|
90
|
-
) {
|
|
100
|
+
if (shouldPreserveImportSpecifier(specifier)) {
|
|
91
101
|
return match;
|
|
92
102
|
}
|
|
93
103
|
try {
|
|
94
104
|
const resolved = Bun.resolveSync(specifier, importerDir);
|
|
95
|
-
return `${prefix}${resolved}${suffix}`;
|
|
105
|
+
return `${prefix}${toRewrittenImportSpecifier(resolved)}${suffix}`;
|
|
96
106
|
} catch {
|
|
97
107
|
return match;
|
|
98
108
|
}
|
|
99
109
|
});
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
interface LegacyPiMirrorState {
|
|
113
|
+
root: string;
|
|
114
|
+
seen: Map<string, string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getMirrorPath(sourcePath: string, state: LegacyPiMirrorState): string {
|
|
118
|
+
const extension = path.extname(sourcePath) || ".js";
|
|
119
|
+
const digest = Bun.hash(sourcePath).toString(36);
|
|
120
|
+
return path.join(state.root, `${digest}${extension}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function rewriteRelativeImportsForLegacyExtension(
|
|
124
|
+
source: string,
|
|
125
|
+
importerPath: string,
|
|
126
|
+
state: LegacyPiMirrorState,
|
|
127
|
+
): Promise<string> {
|
|
128
|
+
const replacements = new Map<string, string>();
|
|
129
|
+
|
|
130
|
+
for (const match of source.matchAll(STATIC_IMPORT_SPECIFIER_REGEX)) {
|
|
131
|
+
const specifier = match[2];
|
|
132
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolved = Bun.resolveSync(specifier, path.dirname(importerPath));
|
|
137
|
+
const mirrored = await mirrorLegacyPiFile(resolved, state);
|
|
138
|
+
replacements.set(specifier, toImportSpecifier(mirrored));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (replacements.size === 0) {
|
|
142
|
+
return source;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return source.replace(STATIC_IMPORT_SPECIFIER_REGEX, (match, prefix: string, specifier: string, suffix: string) => {
|
|
146
|
+
const replacement = replacements.get(specifier);
|
|
147
|
+
return replacement ? `${prefix}${replacement}${suffix}` : match;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function rewriteLegacyPiImportsForRuntime(
|
|
152
|
+
source: string,
|
|
153
|
+
importerPath: string,
|
|
154
|
+
state: LegacyPiMirrorState,
|
|
155
|
+
): Promise<string> {
|
|
156
|
+
const withRelativeResolved = await rewriteRelativeImportsForLegacyExtension(source, importerPath, state);
|
|
157
|
+
const withLegacyRemap = rewriteLegacyPiImports(withRelativeResolved);
|
|
158
|
+
return rewriteBareImportsForLegacyExtension(withLegacyRemap, importerPath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function mirrorLegacyPiFile(sourcePath: string, state: LegacyPiMirrorState): Promise<string> {
|
|
162
|
+
const resolvedPath = path.resolve(sourcePath);
|
|
163
|
+
const cached = state.seen.get(resolvedPath);
|
|
164
|
+
if (cached) {
|
|
165
|
+
return cached;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const mirrorPath = getMirrorPath(resolvedPath, state);
|
|
169
|
+
state.seen.set(resolvedPath, mirrorPath);
|
|
170
|
+
|
|
171
|
+
const raw = await Bun.file(resolvedPath).text();
|
|
172
|
+
const rewritten = await rewriteLegacyPiImportsForRuntime(raw, resolvedPath, state);
|
|
173
|
+
await Bun.write(mirrorPath, rewritten);
|
|
174
|
+
return mirrorPath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown> {
|
|
178
|
+
const root = path.join(os.tmpdir(), "omp-legacy-pi-file", Bun.hash(resolvedPath).toString(36));
|
|
179
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
180
|
+
const state: LegacyPiMirrorState = { root, seen: new Map() };
|
|
181
|
+
const mirroredEntry = await mirrorLegacyPiFile(resolvedPath, state);
|
|
182
|
+
return import(`${toImportSpecifier(mirroredEntry)}?mtime=${Date.now()}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
102
185
|
function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
|
|
103
186
|
if (path.endsWith(".tsx")) {
|
|
104
187
|
return "tsx";
|
|
@@ -150,10 +233,6 @@ export function installLegacyPiSpecifierShim(): void {
|
|
|
150
233
|
|
|
151
234
|
build.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: LEGACY_PI_FILE_NAMESPACE }, async args => {
|
|
152
235
|
const raw = await Bun.file(args.path).text();
|
|
153
|
-
// Bare specifiers (e.g. "lodash", "@scope/pkg/sub") imported from a legacy-namespaced
|
|
154
|
-
// extension file would otherwise bypass Node-style node_modules lookup because the
|
|
155
|
-
// importer lives in a custom namespace. Pre-resolve them to absolute paths so the
|
|
156
|
-
// extension's own node_modules are honored.
|
|
157
236
|
const withLegacyRemap = rewriteLegacyPiImports(raw);
|
|
158
237
|
const withBareResolved = rewriteBareImportsForLegacyExtension(withLegacyRemap, args.path);
|
|
159
238
|
return {
|