@oh-my-pi/pi-coding-agent 16.1.16 → 16.1.18
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 +49 -0
- package/dist/cli.js +16646 -16608
- package/dist/types/auto-thinking/classifier.d.ts +4 -2
- package/dist/types/cli/usage-cli.d.ts +14 -0
- package/dist/types/commands/token.d.ts +9 -0
- package/dist/types/config/model-discovery.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +7 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +34 -15
- package/dist/types/extensibility/plugins/loader.d.ts +16 -9
- package/dist/types/extensibility/tool-event-input.d.ts +2 -0
- package/dist/types/hindsight/content.d.ts +2 -10
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +26 -10
- package/dist/types/modes/components/extensions/extension-list.d.ts +13 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +11 -0
- package/dist/types/session/agent-session.d.ts +6 -6
- package/dist/types/session/provider-image-budget.d.ts +3 -0
- package/dist/types/session/session-context.d.ts +6 -5
- package/dist/types/task/parallel.d.ts +4 -0
- package/dist/types/thinking.d.ts +8 -1
- package/dist/types/tiny/title-client.d.ts +2 -2
- package/dist/types/utils/tools-manager.d.ts +2 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +9 -16
- package/src/auto-thinking/classifier.ts +7 -2
- package/src/cli/profile-alias.ts +38 -7
- package/src/cli/usage-cli.ts +40 -5
- package/src/commands/token.ts +54 -0
- package/src/config/model-discovery.ts +59 -8
- package/src/config/model-registry.ts +74 -3
- package/src/discovery/omp-extension-roots.ts +1 -3
- package/src/extensibility/extensions/wrapper.ts +3 -2
- package/src/extensibility/hooks/tool-wrapper.ts +4 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +40 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +240 -90
- package/src/extensibility/plugins/loader.ts +71 -23
- package/src/extensibility/plugins/marketplace/manager.ts +134 -0
- package/src/extensibility/tool-event-input.ts +57 -0
- package/src/hindsight/content.ts +21 -12
- package/src/mcp/tool-bridge.ts +27 -2
- package/src/mnemopi/state.ts +5 -2
- package/src/modes/components/agent-transcript-viewer.ts +193 -41
- package/src/modes/components/chat-transcript-builder.ts +6 -0
- package/src/modes/components/custom-editor.test.ts +18 -1
- package/src/modes/components/custom-editor.ts +77 -45
- package/src/modes/components/extensions/extension-dashboard.ts +221 -165
- package/src/modes/components/extensions/extension-list.ts +66 -33
- package/src/modes/components/hook-editor.ts +15 -2
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +52 -8
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/welcome.ts +12 -14
- package/src/modes/controllers/command-controller.ts +16 -5
- package/src/modes/controllers/input-controller.ts +115 -3
- package/src/modes/controllers/selector-controller.ts +23 -1
- package/src/modes/interactive-mode.ts +3 -3
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/sdk.ts +8 -10
- package/src/session/agent-session.ts +193 -49
- package/src/session/provider-image-budget.ts +86 -0
- package/src/session/session-context.ts +14 -7
- package/src/session/session-storage.ts +24 -2
- package/src/session/snapcompact-inline.ts +19 -3
- package/src/slash-commands/builtin-registry.ts +0 -22
- package/src/slash-commands/helpers/usage-report.ts +9 -1
- package/src/task/parallel.ts +6 -1
- package/src/thinking.ts +9 -2
- package/src/tiny/title-client.ts +75 -21
- package/src/utils/tools-manager.ts +67 -10
package/src/cli/profile-alias.ts
CHANGED
|
@@ -198,37 +198,68 @@ export function resolveProfileAliasCommandFromProcess(
|
|
|
198
198
|
if (!runtime || !script || !/\.[cm]?[jt]s$/.test(script)) return DEFAULT_ALIAS_COMMAND;
|
|
199
199
|
|
|
200
200
|
const scriptPath = path.resolve(cwd, script);
|
|
201
|
-
|
|
201
|
+
// Normalize to forward slashes for POSIX shell fields — bash/zsh/fish
|
|
202
|
+
// can't resolve backslash-separated paths, even on Windows (Git Bash, WSL).
|
|
203
|
+
const posixScriptPath = scriptPath.replace(/\\/g, "/");
|
|
204
|
+
const posixRuntime = runtime.replace(/\\/g, "/");
|
|
205
|
+
const posix = `${quoteForShell(posixRuntime)} ${quoteForShell(posixScriptPath)}`;
|
|
202
206
|
return {
|
|
203
|
-
display: `${
|
|
207
|
+
display: `${posixRuntime} ${posixScriptPath}`,
|
|
204
208
|
posix,
|
|
205
209
|
fish: posix,
|
|
206
210
|
powerShell: `${quoteForPowerShell(runtime)} ${quoteForPowerShell(scriptPath)}`,
|
|
207
211
|
};
|
|
208
212
|
}
|
|
209
213
|
|
|
214
|
+
/** Normalize backslashes to forward slashes for POSIX-shell paths.
|
|
215
|
+
* path.posix.join only adds / separators — it preserves existing backslashes
|
|
216
|
+
* in input segments like homeDir ("C:\Users\me"), producing mixed paths.
|
|
217
|
+
* Windows UNC paths (\\server\share) become //server/share — path.posix.join
|
|
218
|
+
* would collapse the leading // to /, so we restore it after joining. */
|
|
219
|
+
function toPosix(p: string): string {
|
|
220
|
+
return p.replace(/\\/g, "/");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Like path.posix.join, but preserves leading // (UNC roots) which
|
|
224
|
+
* path.posix.join collapses to a single /. */
|
|
225
|
+
function posixJoinUnc(...segments: string[]): string {
|
|
226
|
+
const joined = path.posix.join(...segments);
|
|
227
|
+
// path.posix.join normalizes // at the start to /, breaking UNC roots.
|
|
228
|
+
// Restore it if any input segment started with // (a toPosix'd UNC path).
|
|
229
|
+
if (segments.some(s => s.startsWith("//") && !s.startsWith("///"))) {
|
|
230
|
+
return `/${joined}`;
|
|
231
|
+
}
|
|
232
|
+
return joined;
|
|
233
|
+
}
|
|
234
|
+
|
|
210
235
|
function resolveShellConfigPath(
|
|
211
236
|
shell: ProfileAliasShell,
|
|
212
237
|
homeDir: string,
|
|
213
238
|
platform: NodeJS.Platform,
|
|
214
239
|
env: NodeJS.ProcessEnv,
|
|
215
240
|
): string {
|
|
241
|
+
// POSIX shells (bash/zsh/fish) always need forward-slash config paths,
|
|
242
|
+
// even on Windows. path.posix.join adds / separators but preserves existing
|
|
243
|
+
// backslashes in input segments, so we normalize each component with toPosix.
|
|
244
|
+
// PowerShell profiles use the platform-native path.join (backslashes on
|
|
245
|
+
// Windows, forward slashes elsewhere).
|
|
246
|
+
const posixHome = toPosix(homeDir);
|
|
216
247
|
switch (shell) {
|
|
217
248
|
case "zsh":
|
|
218
|
-
return
|
|
249
|
+
return posixJoinUnc(env.ZDOTDIR ? toPosix(env.ZDOTDIR) : posixHome, ".zshrc");
|
|
219
250
|
case "bash":
|
|
220
|
-
return platform === "darwin" ?
|
|
251
|
+
return platform === "darwin" ? posixJoinUnc(posixHome, ".bash_profile") : posixJoinUnc(posixHome, ".bashrc");
|
|
221
252
|
case "fish": {
|
|
222
253
|
// fish sources conf.d from $XDG_CONFIG_HOME/fish (default ~/.config/fish);
|
|
223
254
|
// a hard-coded ~/.config would be silently ignored when the user relocates
|
|
224
255
|
// their XDG config root, leaving the alias unsourced after a restart.
|
|
225
|
-
const configHome = env.XDG_CONFIG_HOME
|
|
226
|
-
return
|
|
256
|
+
const configHome = env.XDG_CONFIG_HOME ? toPosix(env.XDG_CONFIG_HOME) : posixJoinUnc(posixHome, ".config");
|
|
257
|
+
return posixJoinUnc(configHome, "fish", "conf.d", "omp-profiles.fish");
|
|
227
258
|
}
|
|
228
259
|
case "pwsh":
|
|
229
260
|
return platform === "win32"
|
|
230
261
|
? path.join(homeDir, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
|
|
231
|
-
:
|
|
262
|
+
: posixJoinUnc(posixHome, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
|
|
232
263
|
case "powershell":
|
|
233
264
|
return path.join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
234
265
|
}
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type UsageReport,
|
|
16
16
|
type UsageUnit,
|
|
17
17
|
} from "@oh-my-pi/pi-ai";
|
|
18
|
-
import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { formatDuration, formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
19
19
|
import chalk from "chalk";
|
|
20
20
|
import { ModelRegistry } from "../config/model-registry";
|
|
21
21
|
import { discoverAuthStorage } from "../sdk";
|
|
@@ -450,6 +450,10 @@ export function formatUsageBreakdown(
|
|
|
450
450
|
lines.push(
|
|
451
451
|
`${chalk.bold.cyan(formatProviderName(provider))} ${chalk.dim(`— ${accountCount} ${accountCount === 1 ? "account" : "accounts"}`)}`,
|
|
452
452
|
);
|
|
453
|
+
// Provider-wide disclaimers render once per provider, not per limit.
|
|
454
|
+
const providerNotes = [...new Set(providerReports.flatMap(report => report.notes ?? []))];
|
|
455
|
+
for (const note of providerNotes)
|
|
456
|
+
lines.push(` ${chalk.dim(sanitizeText(note.replace(/[\r\n]+/g, " ").replace(/\t/g, " ")))}`);
|
|
453
457
|
|
|
454
458
|
const labelWidth = providerReports
|
|
455
459
|
.flatMap(report => report.limits)
|
|
@@ -646,6 +650,28 @@ function collectStoredAccounts(authStorage: AuthStorage): UsageAccountIdentity[]
|
|
|
646
650
|
return accounts;
|
|
647
651
|
}
|
|
648
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Keep only accounts worth a usage row: those whose provider has a usage
|
|
655
|
+
* provider, so a missing report is a real gap rather than the absence of any
|
|
656
|
+
* usage concept. Providers with no usage endpoint (web-search keys, local /
|
|
657
|
+
* keyless servers, inference providers without a usage API) would only ever
|
|
658
|
+
* render as noise, so they are dropped.
|
|
659
|
+
*
|
|
660
|
+
* `hasUsageProvider` is injected (in practice {@link AuthStorage.usageProviderFor})
|
|
661
|
+
* so custom/broker resolvers stay authoritative — no provider list is duplicated
|
|
662
|
+
* here. An explicit `--provider` request bypasses the cull, so
|
|
663
|
+
* `omp usage --provider xai` can still confirm the stored credential has no
|
|
664
|
+
* usage endpoint.
|
|
665
|
+
*/
|
|
666
|
+
export function selectReportableAccounts(
|
|
667
|
+
accounts: UsageAccountIdentity[],
|
|
668
|
+
hasUsageProvider: (provider: string) => boolean,
|
|
669
|
+
explicitProvider?: string,
|
|
670
|
+
): UsageAccountIdentity[] {
|
|
671
|
+
if (explicitProvider) return accounts;
|
|
672
|
+
return accounts.filter(account => hasUsageProvider(account.provider));
|
|
673
|
+
}
|
|
674
|
+
|
|
649
675
|
/** Apply a redaction mask to an optional identity field. */
|
|
650
676
|
function maskIdentity(redaction: Map<string, string>, value: string | undefined): string | undefined {
|
|
651
677
|
return value === undefined ? undefined : (redaction.get(value) ?? value);
|
|
@@ -717,7 +743,12 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
|
717
743
|
(await authStorage.fetchUsageReports({
|
|
718
744
|
baseUrlResolver: provider => modelRegistry.getProviderBaseUrl(provider),
|
|
719
745
|
})) ?? [];
|
|
720
|
-
|
|
746
|
+
const storedAccounts = collectStoredAccounts(authStorage);
|
|
747
|
+
let accounts = selectReportableAccounts(
|
|
748
|
+
storedAccounts,
|
|
749
|
+
provider => authStorage.usageProviderFor(provider) !== undefined,
|
|
750
|
+
cmd.provider,
|
|
751
|
+
);
|
|
721
752
|
let filteredReports = reports;
|
|
722
753
|
if (cmd.provider) {
|
|
723
754
|
const wanted = cmd.provider.toLowerCase();
|
|
@@ -760,9 +791,13 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
|
760
791
|
|
|
761
792
|
if (filteredReports.length === 0 && accounts.length === 0) {
|
|
762
793
|
const scope = cmd.provider ? ` for provider "${cmd.provider}"` : "";
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
794
|
+
// Credentials exist but every one is for a provider without a usage
|
|
795
|
+
// endpoint — say so rather than implying nothing is logged in.
|
|
796
|
+
const message =
|
|
797
|
+
storedAccounts.length > 0
|
|
798
|
+
? `No usage data${scope}. Stored credentials are for providers without a usage endpoint.\n`
|
|
799
|
+
: `No credentials found${scope}. Run \`omp\` and use /login to add accounts.\n`;
|
|
800
|
+
process.stderr.write(chalk.yellow(message));
|
|
766
801
|
process.exitCode = 1;
|
|
767
802
|
return;
|
|
768
803
|
}
|
package/src/commands/token.ts
CHANGED
|
@@ -28,12 +28,23 @@ export default class Token extends Command {
|
|
|
28
28
|
description: "Force refresh the OAuth token even if it has not expired",
|
|
29
29
|
default: false,
|
|
30
30
|
}),
|
|
31
|
+
account: Flags.integer({
|
|
32
|
+
char: "a",
|
|
33
|
+
description: "Select the Nth OAuth account (1-based) in stored order instead of the round-robin default",
|
|
34
|
+
}),
|
|
35
|
+
list: Flags.boolean({
|
|
36
|
+
char: "l",
|
|
37
|
+
description: "List the provider's OAuth accounts (index + identity) and exit",
|
|
38
|
+
default: false,
|
|
39
|
+
}),
|
|
31
40
|
};
|
|
32
41
|
|
|
33
42
|
static examples = [
|
|
34
43
|
"# Get API key for Anthropic\n omp token anthropic",
|
|
35
44
|
"# Get raw Copilot credential JSON\n omp token github-copilot --raw",
|
|
36
45
|
"# Force refresh and get Gemini CLI token\n omp token google-gemini-cli --force-refresh",
|
|
46
|
+
"# List Anthropic OAuth accounts\n omp token anthropic --list",
|
|
47
|
+
"# Get the 2nd Anthropic OAuth account's token\n omp token anthropic --account 2",
|
|
37
48
|
];
|
|
38
49
|
|
|
39
50
|
async run(): Promise<void> {
|
|
@@ -43,6 +54,49 @@ export default class Token extends Command {
|
|
|
43
54
|
|
|
44
55
|
const authStorage = await discoverAuthStorage();
|
|
45
56
|
try {
|
|
57
|
+
if (flags.list || flags.account !== undefined) {
|
|
58
|
+
const accounts = authStorage.listOAuthAccounts(provider);
|
|
59
|
+
if (accounts.length === 0) {
|
|
60
|
+
process.stderr.write(`${chalk.red(`No OAuth accounts found for provider "${providerName}".`)}\n`);
|
|
61
|
+
process.stderr.write("--account/--list select among OAuth accounts; this provider has none stored.\n");
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (flags.list) {
|
|
66
|
+
for (const acct of accounts) {
|
|
67
|
+
const label =
|
|
68
|
+
acct.email ??
|
|
69
|
+
acct.accountId ??
|
|
70
|
+
acct.projectId ??
|
|
71
|
+
acct.enterpriseUrl ??
|
|
72
|
+
`credential #${acct.credentialId}`;
|
|
73
|
+
process.stdout.write(`${acct.position + 1}. ${label}\n`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const n = flags.account;
|
|
78
|
+
if (n === undefined || n < 1 || n > accounts.length) {
|
|
79
|
+
process.stderr.write(
|
|
80
|
+
`${chalk.red(`Invalid --account ${n ?? "(missing)"}.`)} Provider "${providerName}" has ${accounts.length} OAuth account(s) (1-${accounts.length}).\n`,
|
|
81
|
+
);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const resolution = await authStorage.getOAuthAccessAt(provider, n - 1, {
|
|
86
|
+
forceRefresh: flags["force-refresh"],
|
|
87
|
+
});
|
|
88
|
+
if (!resolution?.ok) {
|
|
89
|
+
const reason = resolution && !resolution.ok ? resolution.error : "no OAuth credential available";
|
|
90
|
+
process.stderr.write(
|
|
91
|
+
`${chalk.red(`Could not get token for account ${n} of "${providerName}": ${reason}`)}\n`,
|
|
92
|
+
);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write(`${resolution.accessToken}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
46
100
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
47
101
|
|
|
48
102
|
// Resolve the API key / token
|
|
@@ -117,6 +117,11 @@ type LlamaCppDiscoveredServerMetadata = {
|
|
|
117
117
|
input?: ("text" | "image")[];
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
type LlamaCppModelListEntry = {
|
|
121
|
+
id: string;
|
|
122
|
+
contextWindow?: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
120
125
|
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
121
126
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
122
127
|
return value;
|
|
@@ -162,6 +167,26 @@ function extractLlamaCppContextWindow(payload: Record<string, unknown>): number
|
|
|
162
167
|
return toPositiveNumberOrUndefined(payload.n_ctx);
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
function extractLlamaCppModelContextWindow(item: Record<string, unknown>): number | undefined {
|
|
171
|
+
const meta = item.meta;
|
|
172
|
+
if (!isRecord(meta)) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return toPositiveNumberOrUndefined(meta.n_ctx) ?? toPositiveNumberOrUndefined(meta.n_ctx_train);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseLlamaCppModelList(payload: unknown): LlamaCppModelListEntry[] {
|
|
179
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
return payload.data.flatMap(item => {
|
|
183
|
+
if (!isRecord(item) || typeof item.id !== "string" || !item.id) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
return [{ id: item.id, contextWindow: extractLlamaCppModelContextWindow(item) }];
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
165
190
|
function extractLlamaCppInputCapabilities(payload: Record<string, unknown>): ("text" | "image")[] | undefined {
|
|
166
191
|
const modalities = payload.modalities;
|
|
167
192
|
if (!isRecord(modalities)) {
|
|
@@ -338,12 +363,13 @@ export async function discoverLlamaCppModels(
|
|
|
338
363
|
const [response, serverMetadata] = apiKey
|
|
339
364
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
340
365
|
: await attempt(baseHeaders);
|
|
341
|
-
const payload = (await response.json()) as
|
|
342
|
-
const models = payload
|
|
366
|
+
const payload = (await response.json()) as unknown;
|
|
367
|
+
const models = parseLlamaCppModelList(payload);
|
|
343
368
|
const discovered: Model<Api>[] = [];
|
|
344
369
|
for (const item of models) {
|
|
345
|
-
const id = item
|
|
370
|
+
const { id } = item;
|
|
346
371
|
if (!id) continue;
|
|
372
|
+
const contextWindow = item.contextWindow ?? serverMetadata?.contextWindow ?? 128000;
|
|
347
373
|
discovered.push(
|
|
348
374
|
buildModel({
|
|
349
375
|
id,
|
|
@@ -355,11 +381,8 @@ export async function discoverLlamaCppModels(
|
|
|
355
381
|
input: serverMetadata?.input ?? ["text"],
|
|
356
382
|
imageInputDecoder: "stb",
|
|
357
383
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
358
|
-
contextWindow
|
|
359
|
-
maxTokens: Math.min(
|
|
360
|
-
serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY,
|
|
361
|
-
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
362
|
-
),
|
|
384
|
+
contextWindow,
|
|
385
|
+
maxTokens: Math.min(contextWindow, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
363
386
|
headers,
|
|
364
387
|
compat: {
|
|
365
388
|
supportsStore: false,
|
|
@@ -372,6 +395,34 @@ export async function discoverLlamaCppModels(
|
|
|
372
395
|
return discovered;
|
|
373
396
|
}
|
|
374
397
|
|
|
398
|
+
export async function discoverLlamaCppModelContextWindow(
|
|
399
|
+
model: Pick<Model<Api>, "provider" | "id" | "baseUrl" | "headers">,
|
|
400
|
+
ctx: DiscoveryContext,
|
|
401
|
+
): Promise<number | undefined> {
|
|
402
|
+
const baseUrl = normalizeLlamaCppBaseUrl(model.baseUrl);
|
|
403
|
+
const modelsUrl = `${baseUrl}/models`;
|
|
404
|
+
const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
|
|
405
|
+
const attempt = async (headers: Record<string, string>) => {
|
|
406
|
+
const response = await ctx.fetch(modelsUrl, {
|
|
407
|
+
headers,
|
|
408
|
+
signal: AbortSignal.timeout(250),
|
|
409
|
+
});
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
const entries = parseLlamaCppModelList(await response.json());
|
|
414
|
+
return entries.find(entry => entry.id === model.id)?.contextWindow;
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
const apiKey = await ctx.getBearerApiKeyResolver(model.provider);
|
|
418
|
+
return apiKey
|
|
419
|
+
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
420
|
+
: await attempt(baseHeaders);
|
|
421
|
+
} catch {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
375
426
|
export async function discoverOpenAIModelsList(
|
|
376
427
|
providerConfig: DiscoveryProviderConfig,
|
|
377
428
|
ctx: DiscoveryContext,
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
68
68
|
type DiscoveryContext,
|
|
69
69
|
type DiscoveryProviderConfig,
|
|
70
|
+
discoverLlamaCppModelContextWindow,
|
|
70
71
|
discoverModelsByProviderType,
|
|
71
72
|
getImplicitOllamaBaseUrl,
|
|
72
73
|
getOllamaContextLengthOverride,
|
|
@@ -759,6 +760,50 @@ export class ModelRegistry {
|
|
|
759
760
|
}
|
|
760
761
|
}
|
|
761
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Refresh dynamic metadata that can appear only after a local model loads.
|
|
765
|
+
*/
|
|
766
|
+
async refreshSelectedModelMetadata(model: Model<Api>): Promise<Model<Api>> {
|
|
767
|
+
const isLlamaCppDiscovery = this.#discoverableProviders.some(
|
|
768
|
+
providerConfig => providerConfig.provider === model.provider && providerConfig.discovery.type === "llama.cpp",
|
|
769
|
+
);
|
|
770
|
+
if (!isLlamaCppDiscovery) {
|
|
771
|
+
return model;
|
|
772
|
+
}
|
|
773
|
+
const contextWindow = await discoverLlamaCppModelContextWindow(model, this.#nonResolvingDiscoveryContext());
|
|
774
|
+
if (contextWindow === undefined) {
|
|
775
|
+
return this.find(model.provider, model.id) ?? model;
|
|
776
|
+
}
|
|
777
|
+
const current = this.find(model.provider, model.id) ?? model;
|
|
778
|
+
const override = this.#resolveLiveModelOverride(current);
|
|
779
|
+
const customModel = this.#resolveLiveCustomModelOverlay(current);
|
|
780
|
+
const patch: ModelPatch = {};
|
|
781
|
+
if (
|
|
782
|
+
override?.contextWindow === undefined &&
|
|
783
|
+
customModel?.contextWindow === undefined &&
|
|
784
|
+
current.contextWindow !== contextWindow
|
|
785
|
+
) {
|
|
786
|
+
patch.contextWindow = contextWindow;
|
|
787
|
+
}
|
|
788
|
+
const maxTokens = Math.min(contextWindow, DISCOVERY_DEFAULT_MAX_TOKENS);
|
|
789
|
+
if (
|
|
790
|
+
override?.maxTokens === undefined &&
|
|
791
|
+
customModel?.maxTokens === undefined &&
|
|
792
|
+
current.maxTokens !== maxTokens
|
|
793
|
+
) {
|
|
794
|
+
patch.maxTokens = maxTokens;
|
|
795
|
+
}
|
|
796
|
+
if (patch.contextWindow === undefined && patch.maxTokens === undefined) {
|
|
797
|
+
return current;
|
|
798
|
+
}
|
|
799
|
+
const patched = applyModelPatch(current, patch, "merge");
|
|
800
|
+
this.#models = this.#models.map(candidate =>
|
|
801
|
+
candidate.provider === current.provider && candidate.id === current.id ? patched : candidate,
|
|
802
|
+
);
|
|
803
|
+
this.#rebuildCanonicalIndex();
|
|
804
|
+
return patched;
|
|
805
|
+
}
|
|
806
|
+
|
|
762
807
|
/**
|
|
763
808
|
* Discover models for providers registered at runtime via `fetchDynamicModels`
|
|
764
809
|
* (extension providers). Merges the discovered catalog into the existing model
|
|
@@ -1008,7 +1053,7 @@ export class ModelRegistry {
|
|
|
1008
1053
|
provider: providerConfig.provider,
|
|
1009
1054
|
status: "cached",
|
|
1010
1055
|
optional: providerConfig.optional ?? false,
|
|
1011
|
-
stale: !cache.fresh || !cache.authoritative || configStale,
|
|
1056
|
+
stale: providerConfig.discovery.type === "llama.cpp" || !cache.fresh || !cache.authoritative || configStale,
|
|
1012
1057
|
fetchedAt: cache.updatedAt,
|
|
1013
1058
|
models: models.map(model => model.id),
|
|
1014
1059
|
});
|
|
@@ -1272,7 +1317,9 @@ export class ModelRegistry {
|
|
|
1272
1317
|
const cacheProviderId = this.#configuredDiscoveryCacheProviderId(providerConfig);
|
|
1273
1318
|
const cached = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1274
1319
|
const cacheOlderThanConfig = cached !== null && this.#isDiscoveryCacheOlderThanModelsConfig(cached.updatedAt);
|
|
1275
|
-
const
|
|
1320
|
+
const bypassFreshCache = providerConfig.discovery.type === "llama.cpp" && strategy === "online-if-uncached";
|
|
1321
|
+
const effectiveStrategy =
|
|
1322
|
+
strategy === "online-if-uncached" && (cacheOlderThanConfig || bypassFreshCache) ? "online" : strategy;
|
|
1276
1323
|
const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
|
|
1277
1324
|
if (requiresAuth) {
|
|
1278
1325
|
const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
|
|
@@ -1335,7 +1382,7 @@ export class ModelRegistry {
|
|
|
1335
1382
|
provider: providerId,
|
|
1336
1383
|
status,
|
|
1337
1384
|
optional: providerConfig.optional ?? false,
|
|
1338
|
-
stale: result.stale || status === "cached" || (cacheOlderThanConfig && status !== "ok"),
|
|
1385
|
+
stale: result.stale || status === "cached" || ((cacheOlderThanConfig || bypassFreshCache) && status !== "ok"),
|
|
1339
1386
|
fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
|
|
1340
1387
|
models: result.models.map(model => model.id),
|
|
1341
1388
|
error: discoveryError,
|
|
@@ -1365,6 +1412,13 @@ export class ModelRegistry {
|
|
|
1365
1412
|
};
|
|
1366
1413
|
}
|
|
1367
1414
|
|
|
1415
|
+
#nonResolvingDiscoveryContext(): DiscoveryContext {
|
|
1416
|
+
return {
|
|
1417
|
+
fetch: this.#fetch,
|
|
1418
|
+
getBearerApiKeyResolver: async () => undefined,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1368
1422
|
#warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
|
|
1369
1423
|
const previous = this.#lastDiscoveryWarnings.get(providerConfig.provider);
|
|
1370
1424
|
if (previous === error) {
|
|
@@ -1574,6 +1628,23 @@ export class ModelRegistry {
|
|
|
1574
1628
|
return this.#applyProviderTransportOverride(model, override);
|
|
1575
1629
|
});
|
|
1576
1630
|
}
|
|
1631
|
+
#resolveLiveModelOverride(model: Model<Api>): ModelOverride | undefined {
|
|
1632
|
+
const providerOverrides = this.#modelOverrides.get(model.provider);
|
|
1633
|
+
if (!providerOverrides) return undefined;
|
|
1634
|
+
return resolveModelOverrideWithAliases(
|
|
1635
|
+
providerOverrides,
|
|
1636
|
+
model,
|
|
1637
|
+
(provider, id) => this.find(provider, id) !== undefined,
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
#resolveLiveCustomModelOverlay(model: Model<Api>): CustomModelOverlay | undefined {
|
|
1642
|
+
return (
|
|
1643
|
+
this.#customModelOverlays.find(overlay => overlay.provider === model.provider && overlay.id === model.id) ??
|
|
1644
|
+
this.#runtimeModelOverlays.find(overlay => overlay.provider === model.provider && overlay.id === model.id)
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1577
1648
|
#applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
|
|
1578
1649
|
if (overrides.size === 0) return models;
|
|
1579
1650
|
let liveKeys: Set<string> | null = null;
|
|
@@ -180,9 +180,7 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
|
|
|
180
180
|
async function listInstalledPluginRoots(ctx: LoadContext): Promise<InjectedRoot[]> {
|
|
181
181
|
try {
|
|
182
182
|
const plugins = await getEnabledPlugins(ctx.cwd, { home: ctx.home });
|
|
183
|
-
|
|
184
|
-
// honored by `getEnabledPlugins` via `loadProjectOverrides`.
|
|
185
|
-
return plugins.map(({ path: p }) => ({ path: p, level: "user" }));
|
|
183
|
+
return plugins.map(({ path: p, scope }) => ({ path: p, level: scope }));
|
|
186
184
|
} catch (err) {
|
|
187
185
|
logger.debug("listInstalledPluginRoots: enumeration failed", { error: String(err) });
|
|
188
186
|
return [];
|
|
@@ -6,6 +6,7 @@ import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai
|
|
|
6
6
|
import type { Settings } from "../../config/settings";
|
|
7
7
|
import type { Theme } from "../../modes/theme/theme";
|
|
8
8
|
import { type ApprovalMode, formatApprovalPrompt, requiresApproval } from "../../tools/approval";
|
|
9
|
+
import { normalizeToolEventInput } from "../tool-event-input";
|
|
9
10
|
import { applyToolProxy } from "../tool-proxy";
|
|
10
11
|
import type { ExtensionRunner } from "./runner";
|
|
11
12
|
import type { RegisteredTool, ToolCallEventResult } from "./types";
|
|
@@ -185,7 +186,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
185
186
|
type: "tool_call",
|
|
186
187
|
toolName: this.tool.name,
|
|
187
188
|
toolCallId,
|
|
188
|
-
input: params as Record<string, unknown
|
|
189
|
+
input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
|
|
189
190
|
})) as ToolCallEventResult | undefined;
|
|
190
191
|
|
|
191
192
|
if (callResult?.block) {
|
|
@@ -220,7 +221,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
220
221
|
type: "tool_result",
|
|
221
222
|
toolName: this.tool.name,
|
|
222
223
|
toolCallId,
|
|
223
|
-
input: params as Record<string, unknown
|
|
224
|
+
input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
|
|
224
225
|
content: result.content,
|
|
225
226
|
details: result.details,
|
|
226
227
|
isError: !!executionError,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { Static, TSchema } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { normalizeToolEventInput } from "../tool-event-input";
|
|
6
7
|
import { applyToolProxy } from "../tool-proxy";
|
|
7
8
|
import type { HookRunner } from "./runner";
|
|
8
9
|
import type { ToolCallEventResult, ToolResultEventResult } from "./types";
|
|
@@ -46,7 +47,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
46
47
|
type: "tool_call",
|
|
47
48
|
toolName: this.tool.name,
|
|
48
49
|
toolCallId,
|
|
49
|
-
input: params as Record<string, unknown
|
|
50
|
+
input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
|
|
50
51
|
})) as ToolCallEventResult | undefined;
|
|
51
52
|
|
|
52
53
|
if (callResult?.block) {
|
|
@@ -72,7 +73,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
72
73
|
type: "tool_result",
|
|
73
74
|
toolName: this.tool.name,
|
|
74
75
|
toolCallId,
|
|
75
|
-
input: params as Record<string, unknown
|
|
76
|
+
input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
|
|
76
77
|
content: result.content,
|
|
77
78
|
details: result.details,
|
|
78
79
|
isError: false,
|
|
@@ -95,7 +96,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
95
96
|
type: "tool_result",
|
|
96
97
|
toolName: this.tool.name,
|
|
97
98
|
toolCallId,
|
|
98
|
-
input: params as Record<string, unknown
|
|
99
|
+
input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
|
|
99
100
|
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
100
101
|
details: undefined,
|
|
101
102
|
isError: true,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static handles on every bundled `@oh-my-pi/pi-*` surface a legacy extension
|
|
3
|
+
* may import. Loaded lazily by `legacy-pi-compat.ts` in compiled-binary mode
|
|
4
|
+
* (issue #3423) and re-exported through the `omp-legacy-pi-bundled:` virtual
|
|
5
|
+
* namespace — bunfs paths cannot be resolved at runtime on Bun 1.3.14+, so
|
|
6
|
+
* the only way to re-route extension imports onto the host's in-process copy
|
|
7
|
+
* is via live module references captured at compile time.
|
|
8
|
+
*
|
|
9
|
+
* This module is split out from `legacy-pi-compat.ts` so dev/test runs that
|
|
10
|
+
* touch the compat layer never trigger the cascade through
|
|
11
|
+
* `legacy-pi-coding-agent-shim.ts → ../index → export/html/...` (which
|
|
12
|
+
* requires generated artifacts that only exist after a `bun run build`).
|
|
13
|
+
*
|
|
14
|
+
* The bundler reaches every entry below via standard static-import analysis,
|
|
15
|
+
* so the matching `--compile` extras can be dropped from
|
|
16
|
+
* `scripts/build-binary.ts`.
|
|
17
|
+
*/
|
|
18
|
+
import * as bundledPiAgentCore from "@oh-my-pi/pi-agent-core";
|
|
19
|
+
import * as bundledPiNatives from "@oh-my-pi/pi-natives";
|
|
20
|
+
import * as bundledPiTui from "@oh-my-pi/pi-tui";
|
|
21
|
+
import * as bundledPiUtils from "@oh-my-pi/pi-utils";
|
|
22
|
+
import * as bundledLegacyPiAiShim from "../legacy-pi-ai-shim";
|
|
23
|
+
import * as bundledLegacyPiCodingAgentShim from "../legacy-pi-coding-agent-shim";
|
|
24
|
+
import * as bundledTypeBoxShim from "../typebox";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canonical specifier → live module namespace. Keys MUST match the right-hand
|
|
28
|
+
* side of `bundledRegistryVirtualSpecifier(...)` calls in
|
|
29
|
+
* `legacy-pi-compat.ts`; the synthesizer enumerates each namespace's own
|
|
30
|
+
* enumerable exports at extension load time.
|
|
31
|
+
*/
|
|
32
|
+
export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
|
33
|
+
"@oh-my-pi/pi-agent-core": bundledPiAgentCore,
|
|
34
|
+
"@oh-my-pi/pi-ai": bundledLegacyPiAiShim,
|
|
35
|
+
"@oh-my-pi/pi-coding-agent": bundledLegacyPiCodingAgentShim,
|
|
36
|
+
"@oh-my-pi/pi-natives": bundledPiNatives,
|
|
37
|
+
"@oh-my-pi/pi-tui": bundledPiTui,
|
|
38
|
+
"@oh-my-pi/pi-utils": bundledPiUtils,
|
|
39
|
+
typebox: bundledTypeBoxShim,
|
|
40
|
+
};
|