@oh-my-pi/pi-coding-agent 15.10.8 → 15.10.10
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 +41 -1
- package/dist/types/config/model-registry.d.ts +13 -0
- package/dist/types/config/settings-schema.d.ts +0 -9
- package/dist/types/debug/terminal-info.d.ts +0 -1
- package/dist/types/extensibility/custom-tools/loader.d.ts +22 -3
- package/dist/types/extensibility/extensions/index.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +17 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/modes/components/transcript-container.d.ts +12 -26
- package/dist/types/sdk.d.ts +42 -2
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/index.d.ts +17 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tui/hyperlink.d.ts +8 -0
- package/package.json +9 -9
- package/src/cli/list-models.ts +5 -11
- package/src/config/model-registry.ts +91 -20
- package/src/config/settings-schema.ts +0 -10
- package/src/debug/terminal-info.ts +0 -3
- package/src/edit/diff.ts +48 -15
- package/src/eval/js/shared/rewrite-imports.ts +9 -1
- package/src/extensibility/custom-tools/loader.ts +43 -19
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/loader.ts +29 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +30 -6
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/mcp/transports/stdio.ts +139 -3
- package/src/modes/components/custom-editor.ts +69 -9
- package/src/modes/components/model-selector.ts +62 -52
- package/src/modes/components/transcript-container.ts +204 -125
- package/src/modes/controllers/event-controller.ts +0 -45
- package/src/modes/controllers/input-controller.ts +5 -5
- package/src/modes/controllers/mcp-command-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +0 -4
- package/src/modes/interactive-mode.ts +2 -10
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/tools/bash.md +3 -3
- package/src/prompts/tools/todo.md +5 -1
- package/src/sdk.ts +138 -56
- package/src/ssh/ssh-executor.ts +60 -4
- package/src/task/discovery.ts +17 -24
- package/src/task/executor.ts +19 -0
- package/src/task/index.ts +4 -0
- package/src/tiny/title-client.ts +6 -3
- package/src/tools/index.ts +17 -0
- package/src/tools/todo.ts +16 -7
- package/src/tui/hyperlink.ts +27 -3
- package/src/web/search/providers/anthropic.ts +8 -2
|
@@ -428,6 +428,12 @@ export interface CanonicalModelQueryOptions {
|
|
|
428
428
|
candidates?: readonly Model<Api>[];
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/** A canonical record (with query-filtered variants) plus the variant model selected for it. */
|
|
432
|
+
export interface CanonicalModelSelection {
|
|
433
|
+
record: CanonicalModelRecord;
|
|
434
|
+
model: Model<Api>;
|
|
435
|
+
}
|
|
436
|
+
|
|
431
437
|
/** Result of loading custom models from models.json */
|
|
432
438
|
interface CustomModelsResult {
|
|
433
439
|
models?: CustomModelOverlay[];
|
|
@@ -2217,48 +2223,81 @@ export class ModelRegistry {
|
|
|
2217
2223
|
return this.#models;
|
|
2218
2224
|
}
|
|
2219
2225
|
|
|
2220
|
-
|
|
2226
|
+
/**
|
|
2227
|
+
* Availability predicate with per-provider memoization. Auth lookups
|
|
2228
|
+
* (`authStorage.hasAuth`) and the disabled-provider set are resolved once
|
|
2229
|
+
* per provider instead of once per model, which matters when filtering the
|
|
2230
|
+
* full bundled catalog (thousands of models, ~50 providers).
|
|
2231
|
+
*/
|
|
2232
|
+
#createAvailabilityCheck(): (model: Model<Api>) => boolean {
|
|
2221
2233
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2234
|
+
const byProvider = new Map<string, boolean>();
|
|
2235
|
+
return model => {
|
|
2236
|
+
let available = byProvider.get(model.provider);
|
|
2237
|
+
if (available === undefined) {
|
|
2238
|
+
available =
|
|
2239
|
+
!disabledProviders.has(model.provider) &&
|
|
2240
|
+
(this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider));
|
|
2241
|
+
byProvider.set(model.provider, available);
|
|
2242
|
+
}
|
|
2243
|
+
return available;
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
/**
|
|
2248
|
+
* Build the shared per-query filter state for canonical model queries.
|
|
2249
|
+
* Hoisted out of the per-record loop: building the candidate-selector set
|
|
2250
|
+
* and availability memo once per query instead of once per record is what
|
|
2251
|
+
* keeps `getCanonicalModelSelections` linear instead of O(records × candidates).
|
|
2252
|
+
*/
|
|
2253
|
+
#canonicalQueryFilters(options: CanonicalModelQueryOptions | undefined): {
|
|
2254
|
+
candidateKeys: Set<string> | undefined;
|
|
2255
|
+
isAvailable: ((model: Model<Api>) => boolean) | undefined;
|
|
2256
|
+
} {
|
|
2257
|
+
return {
|
|
2258
|
+
candidateKeys: options?.candidates
|
|
2259
|
+
? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
|
|
2260
|
+
: undefined,
|
|
2261
|
+
isAvailable: options?.availableOnly ? this.#createAvailabilityCheck() : undefined,
|
|
2262
|
+
};
|
|
2226
2263
|
}
|
|
2227
2264
|
|
|
2228
2265
|
#filterCanonicalVariants(
|
|
2229
2266
|
record: CanonicalModelRecord,
|
|
2230
|
-
|
|
2267
|
+
candidateKeys: ReadonlySet<string> | undefined,
|
|
2268
|
+
isAvailable: ((model: Model<Api>) => boolean) | undefined,
|
|
2231
2269
|
): CanonicalModelVariant[] {
|
|
2232
|
-
const candidateKeys = options?.candidates
|
|
2233
|
-
? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
|
|
2234
|
-
: undefined;
|
|
2235
2270
|
return record.variants.filter(variant => {
|
|
2236
2271
|
if (candidateKeys && !candidateKeys.has(variant.selector)) {
|
|
2237
2272
|
return false;
|
|
2238
2273
|
}
|
|
2239
|
-
if (
|
|
2274
|
+
if (isAvailable && !isAvailable(variant.model)) {
|
|
2240
2275
|
return false;
|
|
2241
2276
|
}
|
|
2242
2277
|
return true;
|
|
2243
2278
|
});
|
|
2244
2279
|
}
|
|
2245
2280
|
|
|
2281
|
+
#buildModelOrder(candidates: readonly Model<Api>[]): Map<string, number> {
|
|
2282
|
+
const modelOrder = new Map<string, number>();
|
|
2283
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
2284
|
+
modelOrder.set(formatCanonicalVariantSelector(candidates[index]!), index);
|
|
2285
|
+
}
|
|
2286
|
+
return modelOrder;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2246
2289
|
#providerRank(): Map<string, number> {
|
|
2247
2290
|
return buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings());
|
|
2248
2291
|
}
|
|
2249
2292
|
|
|
2250
2293
|
#resolveCanonicalVariant(
|
|
2251
2294
|
variants: readonly CanonicalModelVariant[],
|
|
2252
|
-
|
|
2295
|
+
modelOrder: ReadonlyMap<string, number>,
|
|
2296
|
+
providerRank: ReadonlyMap<string, number>,
|
|
2253
2297
|
): CanonicalModelVariant | undefined {
|
|
2254
2298
|
if (variants.length === 0) {
|
|
2255
2299
|
return undefined;
|
|
2256
2300
|
}
|
|
2257
|
-
const providerRank = this.#providerRank();
|
|
2258
|
-
const modelOrder = new Map<string, number>();
|
|
2259
|
-
for (let index = 0; index < allCandidates.length; index += 1) {
|
|
2260
|
-
modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
|
|
2261
|
-
}
|
|
2262
2301
|
const sourceRank: Record<CanonicalModelVariant["source"], number> = {
|
|
2263
2302
|
override: 1,
|
|
2264
2303
|
bundled: 1,
|
|
@@ -2289,9 +2328,10 @@ export class ModelRegistry {
|
|
|
2289
2328
|
}
|
|
2290
2329
|
|
|
2291
2330
|
getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
|
|
2331
|
+
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
2292
2332
|
const records: CanonicalModelRecord[] = [];
|
|
2293
2333
|
for (const record of this.#canonicalIndex.records) {
|
|
2294
|
-
const variants = this.#filterCanonicalVariants(record,
|
|
2334
|
+
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
2295
2335
|
if (variants.length === 0) {
|
|
2296
2336
|
continue;
|
|
2297
2337
|
}
|
|
@@ -2304,12 +2344,43 @@ export class ModelRegistry {
|
|
|
2304
2344
|
return records;
|
|
2305
2345
|
}
|
|
2306
2346
|
|
|
2347
|
+
/**
|
|
2348
|
+
* One-pass equivalent of `getCanonicalModels` + `resolveCanonicalModel` per
|
|
2349
|
+
* record. The per-query state (candidate-selector set, availability memo,
|
|
2350
|
+
* provider rank, candidate order) is built once, so the whole catalog
|
|
2351
|
+
* resolves in O(records + candidates) instead of O(records × candidates).
|
|
2352
|
+
* This is the path the model selector hydrates from synchronously on open.
|
|
2353
|
+
*/
|
|
2354
|
+
getCanonicalModelSelections(options?: CanonicalModelQueryOptions): CanonicalModelSelection[] {
|
|
2355
|
+
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
2356
|
+
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
2357
|
+
const modelOrder = this.#buildModelOrder(candidates);
|
|
2358
|
+
const providerRank = this.#providerRank();
|
|
2359
|
+
const selections: CanonicalModelSelection[] = [];
|
|
2360
|
+
for (const record of this.#canonicalIndex.records) {
|
|
2361
|
+
const variants = this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
2362
|
+
if (variants.length === 0) {
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
const resolved = this.#resolveCanonicalVariant(variants, modelOrder, providerRank);
|
|
2366
|
+
if (!resolved) {
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
selections.push({
|
|
2370
|
+
record: { id: record.id, name: record.name, variants },
|
|
2371
|
+
model: resolved.model,
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
return selections;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2307
2377
|
getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
|
|
2308
2378
|
const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
|
|
2309
2379
|
if (!record) {
|
|
2310
2380
|
return [];
|
|
2311
2381
|
}
|
|
2312
|
-
|
|
2382
|
+
const { candidateKeys, isAvailable } = this.#canonicalQueryFilters(options);
|
|
2383
|
+
return this.#filterCanonicalVariants(record, candidateKeys, isAvailable);
|
|
2313
2384
|
}
|
|
2314
2385
|
|
|
2315
2386
|
resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
|
|
@@ -2318,7 +2389,7 @@ export class ModelRegistry {
|
|
|
2318
2389
|
return undefined;
|
|
2319
2390
|
}
|
|
2320
2391
|
const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
|
|
2321
|
-
return this.#resolveCanonicalVariant(variants, candidates)?.model;
|
|
2392
|
+
return this.#resolveCanonicalVariant(variants, this.#buildModelOrder(candidates), this.#providerRank())?.model;
|
|
2322
2393
|
}
|
|
2323
2394
|
|
|
2324
2395
|
getCanonicalId(model: Model<Api>): string | undefined {
|
|
@@ -2330,7 +2401,7 @@ export class ModelRegistry {
|
|
|
2330
2401
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
2331
2402
|
*/
|
|
2332
2403
|
getAvailable(): Model<Api>[] {
|
|
2333
|
-
return this.#models.filter(
|
|
2404
|
+
return this.#models.filter(this.#createAvailabilityCheck());
|
|
2334
2405
|
}
|
|
2335
2406
|
|
|
2336
2407
|
/**
|
|
@@ -686,16 +686,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
686
686
|
ui: { tab: "appearance", label: "Show Hardware Cursor", description: "Show terminal cursor for IME support" },
|
|
687
687
|
},
|
|
688
688
|
|
|
689
|
-
clearOnShrink: {
|
|
690
|
-
type: "boolean",
|
|
691
|
-
default: false,
|
|
692
|
-
ui: {
|
|
693
|
-
tab: "appearance",
|
|
694
|
-
label: "Clear on Shrink",
|
|
695
|
-
description: "Clear empty rows when content shrinks (may cause flicker)",
|
|
696
|
-
},
|
|
697
|
-
},
|
|
698
|
-
|
|
699
689
|
// ────────────────────────────────────────────────────────────────────────
|
|
700
690
|
// Model
|
|
701
691
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -36,7 +36,6 @@ export interface TerminalStateInfo {
|
|
|
36
36
|
hyperlinks: boolean;
|
|
37
37
|
deccara: boolean;
|
|
38
38
|
screenToScrollback: boolean;
|
|
39
|
-
eagerEraseScrollbackRisk: boolean;
|
|
40
39
|
synchronizedOutput: boolean;
|
|
41
40
|
multiplexer: string | null;
|
|
42
41
|
env: { TERM?: string; TERM_PROGRAM?: string; TERM_PROGRAM_VERSION?: string; COLORTERM?: string };
|
|
@@ -82,7 +81,6 @@ export function collectTerminalState(runtime: TerminalRuntimeState): TerminalSta
|
|
|
82
81
|
hyperlinks: TERMINAL.hyperlinks,
|
|
83
82
|
deccara: TERMINAL.deccara,
|
|
84
83
|
screenToScrollback: TERMINAL.supportsScreenToScrollback,
|
|
85
|
-
eagerEraseScrollbackRisk: TERMINAL.eagerEraseScrollbackRisk,
|
|
86
84
|
synchronizedOutput: runtime.synchronizedOutput,
|
|
87
85
|
multiplexer: detectMultiplexer(env),
|
|
88
86
|
env: {
|
|
@@ -115,7 +113,6 @@ export function formatTerminalState(info: TerminalStateInfo): string {
|
|
|
115
113
|
"",
|
|
116
114
|
"Scrollback",
|
|
117
115
|
` Screen->history clear: ${info.screenToScrollback ? "CSI 22 J" : "CSI 2 J (redraw)"}`,
|
|
118
|
-
` Eager-erase risk: ${yesNo(info.eagerEraseScrollbackRisk)} (ED3 may yank scrolled readers)`,
|
|
119
116
|
"",
|
|
120
117
|
"Detection signals",
|
|
121
118
|
` TERM: ${info.env.TERM ?? "(unset)"}`,
|
package/src/edit/diff.ts
CHANGED
|
@@ -55,13 +55,10 @@ function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, conten
|
|
|
55
55
|
return `${prefix}${lineNum}|${content}`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
type DiffSource = "old" | "new";
|
|
59
|
-
|
|
60
58
|
interface ParsedNumberedDiffRow {
|
|
61
59
|
prefix: "+" | "-" | " ";
|
|
62
60
|
lineNumber: number;
|
|
63
61
|
content: string;
|
|
64
|
-
source: DiffSource;
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
function parseNumberedDiffRow(row: string): ParsedNumberedDiffRow | undefined {
|
|
@@ -70,12 +67,7 @@ function parseNumberedDiffRow(row: string): ParsedNumberedDiffRow | undefined {
|
|
|
70
67
|
const prefix = match[1] as "+" | "-" | " ";
|
|
71
68
|
const lineNumber = Number.parseInt(match[2], 10);
|
|
72
69
|
if (!Number.isFinite(lineNumber)) return undefined;
|
|
73
|
-
return {
|
|
74
|
-
prefix,
|
|
75
|
-
lineNumber,
|
|
76
|
-
content: match[3] ?? "",
|
|
77
|
-
source: prefix === "+" ? "new" : "old",
|
|
78
|
-
};
|
|
70
|
+
return { prefix, lineNumber, content: match[3] ?? "" };
|
|
79
71
|
}
|
|
80
72
|
|
|
81
73
|
function isDiffChangeRow(row: string | undefined): boolean {
|
|
@@ -92,7 +84,6 @@ function adjustedContextInsertIndex(rows: readonly string[], index: number): num
|
|
|
92
84
|
|
|
93
85
|
function insertBracketContextRows(
|
|
94
86
|
rows: string[],
|
|
95
|
-
source: DiffSource,
|
|
96
87
|
contextLines: ReadonlyMap<number, string>,
|
|
97
88
|
seenRows: Set<string>,
|
|
98
89
|
): void {
|
|
@@ -106,7 +97,7 @@ function insertBracketContextRows(
|
|
|
106
97
|
let nextSourceLine: number | undefined;
|
|
107
98
|
for (let i = 0; i < rows.length; i++) {
|
|
108
99
|
const parsed = parseNumberedDiffRow(rows[i]);
|
|
109
|
-
if (!parsed || parsed.
|
|
100
|
+
if (!parsed || parsed.prefix === "+") continue;
|
|
110
101
|
if (parsed.lineNumber < lineNumber) {
|
|
111
102
|
previousSourceLine = parsed.lineNumber;
|
|
112
103
|
continue;
|
|
@@ -127,6 +118,16 @@ function insertBracketContextRows(
|
|
|
127
118
|
}
|
|
128
119
|
}
|
|
129
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Insert off-window block-boundary rows (enclosing header, matching closing
|
|
123
|
+
* bracket, …) into a numbered diff. Context rows carry pre-edit line numbers —
|
|
124
|
+
* the renumbering contract of `buildCompactDiffPreview` — so boundary lines
|
|
125
|
+
* discovered in the new file are translated back to their pre-edit numbers
|
|
126
|
+
* and merged with the old-file pass before a single insertion sweep. Without
|
|
127
|
+
* the translation, a context line sitting in a net-offset region would be
|
|
128
|
+
* re-inserted under its post-edit number: duplicated, out of order, and
|
|
129
|
+
* renumbered incorrectly by the preview.
|
|
130
|
+
*/
|
|
130
131
|
function addMatchingBracketContextRows(
|
|
131
132
|
rows: string[],
|
|
132
133
|
oldLines: readonly string[],
|
|
@@ -136,16 +137,48 @@ function addMatchingBracketContextRows(
|
|
|
136
137
|
const oldVisible: number[] = [];
|
|
137
138
|
const newVisible: number[] = [];
|
|
138
139
|
const seenRows = new Set(rows);
|
|
140
|
+
// Change positions in new-file coordinates, used to translate an unchanged
|
|
141
|
+
// new-file line number back to its pre-edit equivalent.
|
|
142
|
+
const changes: { newPos: number; delta: 1 | -1 }[] = [];
|
|
143
|
+
let offset = 0;
|
|
139
144
|
|
|
140
145
|
for (const row of rows) {
|
|
141
146
|
const parsed = parseNumberedDiffRow(row);
|
|
142
147
|
if (!parsed) continue;
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
switch (parsed.prefix) {
|
|
149
|
+
case "-":
|
|
150
|
+
oldVisible.push(parsed.lineNumber);
|
|
151
|
+
changes.push({ newPos: parsed.lineNumber + offset, delta: -1 });
|
|
152
|
+
offset--;
|
|
153
|
+
break;
|
|
154
|
+
case "+":
|
|
155
|
+
newVisible.push(parsed.lineNumber);
|
|
156
|
+
changes.push({ newPos: parsed.lineNumber, delta: 1 });
|
|
157
|
+
offset++;
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
// Context rows are visible in BOTH files: pre-edit number as
|
|
161
|
+
// written, post-edit number shifted by the net change so far.
|
|
162
|
+
oldVisible.push(parsed.lineNumber);
|
|
163
|
+
newVisible.push(parsed.lineNumber + offset);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
const toOldLineNumber = (newLineNumber: number): number => {
|
|
169
|
+
let shift = 0;
|
|
170
|
+
for (const change of changes) {
|
|
171
|
+
if (change.newPos <= newLineNumber) shift += change.delta;
|
|
172
|
+
}
|
|
173
|
+
return newLineNumber - shift;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const contextRows = findBlockContextLines(oldLines, oldVisible, source);
|
|
177
|
+
for (const [lineNumber, text] of findBlockContextLines(newLines, newVisible, source)) {
|
|
178
|
+
const oldLineNumber = toOldLineNumber(lineNumber);
|
|
179
|
+
if (!contextRows.has(oldLineNumber)) contextRows.set(oldLineNumber, text);
|
|
180
|
+
}
|
|
181
|
+
insertBracketContextRows(rows, contextRows, seenRows);
|
|
149
182
|
}
|
|
150
183
|
|
|
151
184
|
/**
|
|
@@ -82,6 +82,14 @@ function parseProgram(code: string): { program: { body: ReadonlyArray<BabelProgr
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Callee substituted for dynamic `import(...)` calls. Functions handed to puppeteer
|
|
86
|
+
// (`tab.evaluate`, `page.evaluate`, `waitForFunction`, `$$eval`, ...) are serialized with
|
|
87
|
+
// `Function.prototype.toString()` and re-evaluated inside the browser page, where the
|
|
88
|
+
// worker-injected `__omp_import__` global does not exist. The swap therefore guards on the
|
|
89
|
+
// helper's presence and falls back to native dynamic import, so serialized code keeps
|
|
90
|
+
// working in foreign realms while in-worker code still resolves against the session cwd.
|
|
91
|
+
const DYNAMIC_IMPORT_CALLEE = '(typeof __omp_import__ === "function" ? __omp_import__ : (s, o) => import(s, o))';
|
|
92
|
+
|
|
85
93
|
function buildOmpImportCall(sourceLiteral: string, optionsLiteral: string | undefined): string {
|
|
86
94
|
// Route every static import through the worker-injected `__omp_import__` helper so the
|
|
87
95
|
// specifier resolves against the session cwd (and `with`-attribute imports keep working).
|
|
@@ -180,7 +188,7 @@ export function rewriteImports(code: string): string {
|
|
|
180
188
|
const call = node as unknown as { callee?: { type?: string; start?: number; end?: number } };
|
|
181
189
|
const callee = call.callee;
|
|
182
190
|
if (callee?.type !== "Import" || typeof callee.start !== "number" || typeof callee.end !== "number") return;
|
|
183
|
-
edits.push({ start: callee.start, end: callee.end, text:
|
|
191
|
+
edits.push({ start: callee.start, end: callee.end, text: DYNAMIC_IMPORT_CALLEE });
|
|
184
192
|
});
|
|
185
193
|
|
|
186
194
|
if (edits.length === 0) return code;
|
|
@@ -66,8 +66,10 @@ async function loadTool(
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Tool path with optional source metadata
|
|
70
|
-
|
|
69
|
+
/** Tool path with optional source metadata, suitable for forwarding from a
|
|
70
|
+
* parent session to a subagent so the subagent can re-bind tools to its own
|
|
71
|
+
* `CustomToolAPI` without redoing the filesystem scan. */
|
|
72
|
+
export interface ToolPathWithSource {
|
|
71
73
|
path: string;
|
|
72
74
|
source?: { provider: string; providerName: string; level: "user" | "project" };
|
|
73
75
|
}
|
|
@@ -189,26 +191,19 @@ export async function loadCustomTools(
|
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
194
|
+
* Collect the absolute tool-source paths to load, without importing or
|
|
195
|
+
* binding factories. Hot path on session startup — the scan walks
|
|
196
|
+
* `.omp/tools/`, `.claude/tools/`, the plugin tree, and any configured paths.
|
|
197
|
+
*
|
|
198
|
+
* Subagents reuse the parent's collected paths via the SDK's
|
|
199
|
+
* `preloadedCustomToolPaths` option, then call `loadCustomTools` themselves
|
|
200
|
+
* so each session re-binds factories with its own session-scoped
|
|
201
|
+
* `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
196
202
|
*
|
|
197
203
|
* @param configuredPaths - Explicit paths from settings.json and CLI --tool flags
|
|
198
204
|
* @param cwd - Current working directory
|
|
199
|
-
* @param builtInToolNames - Names of built-in tools to check for conflicts
|
|
200
205
|
*/
|
|
201
|
-
export async function
|
|
202
|
-
configuredPaths: string[],
|
|
203
|
-
cwd: string,
|
|
204
|
-
builtInToolNames: string[],
|
|
205
|
-
pushPendingAction?: (action: {
|
|
206
|
-
label: string;
|
|
207
|
-
sourceToolName: string;
|
|
208
|
-
apply(reason: string): Promise<AgentToolResult<unknown>>;
|
|
209
|
-
reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
|
|
210
|
-
}) => void,
|
|
211
|
-
) {
|
|
206
|
+
export async function discoverCustomToolPaths(configuredPaths: string[], cwd: string): Promise<ToolPathWithSource[]> {
|
|
212
207
|
const allPathsWithSources: ToolPathWithSource[] = [];
|
|
213
208
|
const seen = new Set<string>();
|
|
214
209
|
|
|
@@ -241,5 +236,34 @@ export async function discoverAndLoadCustomTools(
|
|
|
241
236
|
addPath(resolvePath(configPath, cwd), { provider: "config", providerName: "Config", level: "project" });
|
|
242
237
|
}
|
|
243
238
|
|
|
244
|
-
return
|
|
239
|
+
return allPathsWithSources;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Discover and load tools from standard locations via capability system:
|
|
244
|
+
* 1. User and project tools discovered by capability providers
|
|
245
|
+
* 2. Installed plugins (~/.omp/plugins/node_modules/*)
|
|
246
|
+
* 3. Explicitly configured paths from settings or CLI
|
|
247
|
+
*
|
|
248
|
+
* Composed of {@link discoverCustomToolPaths} (FS scan) + {@link loadCustomTools}
|
|
249
|
+
* (per-session binding). Subagents skip the first step and just call
|
|
250
|
+
* `loadCustomTools` against the parent's collected paths.
|
|
251
|
+
*
|
|
252
|
+
* @param configuredPaths - Explicit paths from settings.json and CLI --tool flags
|
|
253
|
+
* @param cwd - Current working directory
|
|
254
|
+
* @param builtInToolNames - Names of built-in tools to check for conflicts
|
|
255
|
+
*/
|
|
256
|
+
export async function discoverAndLoadCustomTools(
|
|
257
|
+
configuredPaths: string[],
|
|
258
|
+
cwd: string,
|
|
259
|
+
builtInToolNames: string[],
|
|
260
|
+
pushPendingAction?: (action: {
|
|
261
|
+
label: string;
|
|
262
|
+
sourceToolName: string;
|
|
263
|
+
apply(reason: string): Promise<AgentToolResult<unknown>>;
|
|
264
|
+
reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
|
|
265
|
+
}) => void,
|
|
266
|
+
) {
|
|
267
|
+
const pathsWithSources = await discoverCustomToolPaths(configuredPaths, cwd);
|
|
268
|
+
return loadCustomTools(pathsWithSources, cwd, builtInToolNames, pushPendingAction);
|
|
245
269
|
}
|
|
@@ -475,16 +475,24 @@ async function discoverExtensionsInDir(dir: string): Promise<string[]> {
|
|
|
475
475
|
|
|
476
476
|
return discovered;
|
|
477
477
|
}
|
|
478
|
-
|
|
479
478
|
/**
|
|
480
|
-
* Discover
|
|
479
|
+
* Discover absolute paths of extensions to load, without importing or
|
|
480
|
+
* binding factories. Hot path on session startup — the scan walks native
|
|
481
|
+
* `.omp`/`.pi` extension capabilities, the installed-plugin tree, and any
|
|
482
|
+
* configured paths.
|
|
483
|
+
*
|
|
484
|
+
* Subagents reuse the parent's collected paths via the SDK's
|
|
485
|
+
* `preloadedExtensionPaths` option, then call {@link loadExtensions} themselves
|
|
486
|
+
* so each session rebuilds Extension instances bound to its OWN
|
|
487
|
+
* `ExtensionAPI` (cwd, eventBus, runtime). Forwarding the parent's
|
|
488
|
+
* `LoadExtensionsResult` directly would reuse handlers/tools/commands that
|
|
489
|
+
* closed over the parent's `cwd` and event bus.
|
|
481
490
|
*/
|
|
482
|
-
export async function
|
|
491
|
+
export async function discoverExtensionPaths(
|
|
483
492
|
configuredPaths: string[],
|
|
484
493
|
cwd: string,
|
|
485
|
-
eventBus?: EventBus,
|
|
486
494
|
disabledExtensionIds: string[] = [],
|
|
487
|
-
): Promise<
|
|
495
|
+
): Promise<string[]> {
|
|
488
496
|
const allPaths: string[] = [];
|
|
489
497
|
const seen = new Set<string>();
|
|
490
498
|
const disabled = new Set(disabledExtensionIds);
|
|
@@ -545,5 +553,20 @@ export async function discoverAndLoadExtensions(
|
|
|
545
553
|
addPath(resolved);
|
|
546
554
|
}
|
|
547
555
|
|
|
548
|
-
return
|
|
556
|
+
return allPaths;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Discover and load extensions from standard locations. Composed of
|
|
561
|
+
* {@link discoverExtensionPaths} (FS scan) + {@link loadExtensions}
|
|
562
|
+
* (per-session binding).
|
|
563
|
+
*/
|
|
564
|
+
export async function discoverAndLoadExtensions(
|
|
565
|
+
configuredPaths: string[],
|
|
566
|
+
cwd: string,
|
|
567
|
+
eventBus?: EventBus,
|
|
568
|
+
disabledExtensionIds: string[] = [],
|
|
569
|
+
): Promise<LoadExtensionsResult> {
|
|
570
|
+
const paths = await discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds);
|
|
571
|
+
return loadExtensions(paths, cwd, eventBus);
|
|
549
572
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as fs from "node:fs
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as url from "node:url";
|
|
4
4
|
import { isCompiledBinary } from "@oh-my-pi/pi-utils";
|
|
@@ -142,7 +142,31 @@ const LEGACY_PI_CODING_AGENT_SHIM_PATH = BUNFS_PACKAGE_ROOT
|
|
|
142
142
|
// `Bun.resolveSync`, and hardcoding a relative source-tree path would break
|
|
143
143
|
// installs where the bundled packages live at `node_modules/@oh-my-pi/pi-*`
|
|
144
144
|
// rather than `packages/*`.
|
|
145
|
-
|
|
145
|
+
//
|
|
146
|
+
// Every override target is validated against the on-disk filesystem at module
|
|
147
|
+
// init: any entry whose file is missing (e.g. a compiled binary where Bun's
|
|
148
|
+
// `--compile` quietly dropped an additional entrypoint — issue #2168) is left
|
|
149
|
+
// out so `resolveCanonicalPiSpecifier` falls through to `getResolvedSpecifier`,
|
|
150
|
+
// which throws under bunfs and triggers the catch in `rewriteLegacyPiImports`.
|
|
151
|
+
// That catch leaves the specifier untouched so Bun resolves the canonical
|
|
152
|
+
// `@oh-my-pi/pi-*` import from the extension's own `node_modules` instead of
|
|
153
|
+
// emitting a bunfs `file://` URL to a module that isn't actually present.
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Drop overrides whose targets are missing on disk so they can fall through to
|
|
157
|
+
* the canonical-resolution path. Exported for the test seam in #2168.
|
|
158
|
+
*
|
|
159
|
+
* `pathExistsSync` defaults to `fs.existsSync`; the tests inject a stub to
|
|
160
|
+
* simulate the missing-entrypoint failure mode without touching the real FS.
|
|
161
|
+
*/
|
|
162
|
+
export function __validateLegacyPiPackageRootOverrides(
|
|
163
|
+
candidates: Record<string, string>,
|
|
164
|
+
pathExistsSync: (p: string) => boolean = fs.existsSync,
|
|
165
|
+
): Record<string, string> {
|
|
166
|
+
return Object.fromEntries(Object.entries(candidates).filter(([, candidate]) => pathExistsSync(candidate)));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const LEGACY_PI_PACKAGE_ROOT_OVERRIDES = __validateLegacyPiPackageRootOverrides({
|
|
146
170
|
[`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
|
|
147
171
|
[`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
|
|
148
172
|
...(BUNFS_PACKAGE_ROOT
|
|
@@ -153,7 +177,7 @@ const LEGACY_PI_PACKAGE_ROOT_OVERRIDES: Record<string, string> = {
|
|
|
153
177
|
[`${CANONICAL_PI_SCOPE}/pi-utils`]: bunfsPath("utils", "src", "index.js"),
|
|
154
178
|
}
|
|
155
179
|
: {}),
|
|
156
|
-
};
|
|
180
|
+
});
|
|
157
181
|
|
|
158
182
|
let isLegacyPiSpecifierShimInstalled = false;
|
|
159
183
|
|
|
@@ -253,7 +277,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
253
277
|
|
|
254
278
|
async function pathExists(p: string): Promise<boolean> {
|
|
255
279
|
try {
|
|
256
|
-
await fs.stat(p);
|
|
280
|
+
await fs.promises.stat(p);
|
|
257
281
|
return true;
|
|
258
282
|
} catch {
|
|
259
283
|
return false;
|
|
@@ -267,7 +291,7 @@ function hasSourceModuleExtension(p: string): boolean {
|
|
|
267
291
|
|
|
268
292
|
async function resolveSourceModuleFile(basePath: string): Promise<string | null> {
|
|
269
293
|
try {
|
|
270
|
-
const stats = await fs.stat(basePath);
|
|
294
|
+
const stats = await fs.promises.stat(basePath);
|
|
271
295
|
if (stats.isFile()) {
|
|
272
296
|
// Non-source files (JSON, WASM, text assets, etc.) bypass the on-load
|
|
273
297
|
// rewrite hook so Bun's native loaders handle them; our hook would
|
|
@@ -475,7 +499,7 @@ const hookedExtensionEntries = new Set<string>();
|
|
|
475
499
|
/** Resolve symlinks in a path, falling back to the input if realpath fails. */
|
|
476
500
|
async function realpathOrSelf(p: string): Promise<string> {
|
|
477
501
|
try {
|
|
478
|
-
return await fs.realpath(p);
|
|
502
|
+
return await fs.promises.realpath(p);
|
|
479
503
|
} catch {
|
|
480
504
|
return p;
|
|
481
505
|
}
|