@oh-my-pi/pi-coding-agent 17.3.7 → 17.3.8
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 +60 -0
- package/dist/{CHANGELOG-1hmwwt45.md → CHANGELOG-vr9cckb4.md} +60 -0
- package/dist/cli.js +2993 -3001
- package/dist/docs-index.generated.txt +1 -1
- package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
- package/dist/types/advisor/advise-tool.d.ts +4 -2
- package/dist/types/cli/auth-broker-cli.d.ts +15 -0
- package/dist/types/cli/update-cli.d.ts +8 -0
- package/dist/types/cli-commands.d.ts +10 -2
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/config/settings.d.ts +9 -0
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/launch/presence.d.ts +4 -1
- package/dist/types/mcp/oauth-credentials.d.ts +23 -0
- package/dist/types/mcp/oauth-flow.d.ts +11 -0
- package/dist/types/mnemopi/backend.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +25 -3
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/prewalk.d.ts +4 -0
- package/dist/types/session/session-entries.d.ts +0 -1
- package/dist/types/session/session-manager.d.ts +12 -0
- package/dist/types/session/session-stats.d.ts +13 -1
- package/dist/types/session/skill-title-input.d.ts +13 -0
- package/dist/types/subprocess/worker-client.d.ts +7 -4
- package/dist/types/task/label.d.ts +2 -0
- package/dist/types/task/render.d.ts +2 -0
- package/dist/types/tiny/completion-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +6 -4
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tiny/worker.d.ts +27 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/read-format.d.ts +6 -0
- package/dist/types/tools/read-summary.d.ts +7 -1
- package/dist/types/utils/block-context.d.ts +14 -0
- package/dist/types/utils/fetch-timeout.d.ts +15 -0
- package/dist/types/utils/git.d.ts +25 -1
- package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
- package/package.json +13 -13
- package/src/advisor/advise-tool.ts +5 -3
- package/src/cli/auth-broker-cli.ts +36 -1
- package/src/cli/profile-bootstrap.ts +2 -6
- package/src/cli/update-cli.ts +63 -11
- package/src/cli-commands.ts +61 -7
- package/src/commands/completions.ts +2 -1
- package/src/commit/agentic/index.ts +15 -2
- package/src/commit/git/diff.ts +6 -2
- package/src/config/model-resolver.ts +52 -6
- package/src/config/models-config.ts +2 -2
- package/src/config/settings-schema.ts +33 -0
- package/src/config/settings.ts +159 -30
- package/src/discovery/helpers.ts +45 -2
- package/src/discovery/omp-plugins.ts +2 -1
- package/src/discovery/opencode.ts +56 -3
- package/src/eval/js/process-entry.ts +4 -4
- package/src/export/html/tool-views.generated.js +19 -19
- package/src/extensibility/extensions/runner.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
- package/src/launch/client.ts +9 -4
- package/src/launch/presence.ts +19 -4
- package/src/lsp/defaults.json +1 -1
- package/src/mcp/manager.ts +41 -20
- package/src/mcp/oauth-credentials.ts +38 -0
- package/src/mcp/oauth-flow.ts +21 -0
- package/src/mcp/tool-bridge.ts +32 -16
- package/src/mnemopi/backend.ts +35 -3
- package/src/modes/components/model-hub.ts +37 -4
- package/src/modes/components/settings-selector.ts +17 -11
- package/src/modes/components/tool-execution.ts +97 -29
- package/src/modes/components/tree-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +12 -2
- package/src/modes/controllers/input-controller.ts +64 -27
- package/src/modes/controllers/mcp-command-controller.ts +13 -4
- package/src/modes/interactive-mode.ts +79 -11
- package/src/modes/types.ts +11 -0
- package/src/prompts/system/memory-extraction-system.md +5 -22
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/session/agent-session.ts +52 -6
- package/src/session/messages.ts +6 -0
- package/src/session/prewalk.ts +25 -7
- package/src/session/session-entries.ts +0 -1
- package/src/session/session-maintenance.ts +10 -1
- package/src/session/session-manager.ts +15 -0
- package/src/session/session-stats.ts +24 -3
- package/src/session/settings-stream-fn.ts +7 -0
- package/src/session/skill-title-input.ts +32 -0
- package/src/session/turn-recovery.ts +23 -18
- package/src/subprocess/worker-client.ts +8 -5
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/task/label.ts +14 -1
- package/src/task/persisted-revive.ts +13 -0
- package/src/task/render.ts +1 -1
- package/src/task/structured-subagent.ts +5 -2
- package/src/tiny/completion-prompt.ts +16 -0
- package/src/tiny/title-client.ts +15 -6
- package/src/tiny/title-protocol.ts +8 -1
- package/src/tiny/worker.ts +21 -19
- package/src/tools/bash.ts +7 -1
- package/src/tools/read-format.ts +16 -2
- package/src/tools/read-summary.ts +9 -4
- package/src/tools/read.ts +306 -72
- package/src/utils/block-context.ts +15 -1
- package/src/utils/fetch-timeout.ts +33 -0
- package/src/utils/git.ts +54 -11
- package/src/web/search/providers/browser-page.ts +21 -3
- package/src/web/search/providers/tinyfish.ts +26 -0
|
@@ -3,6 +3,8 @@ export interface GitCommandResult {
|
|
|
3
3
|
exitCode: number;
|
|
4
4
|
stdout: string;
|
|
5
5
|
stderr: string;
|
|
6
|
+
/** True when stdout or stderr hit {@link GIT_COMMAND_OUTPUT_LIMIT_BYTES} and the captured text is incomplete. */
|
|
7
|
+
truncated: boolean;
|
|
6
8
|
}
|
|
7
9
|
export interface GitRepository {
|
|
8
10
|
commonDir: string;
|
|
@@ -55,6 +57,7 @@ export interface DiffOptions {
|
|
|
55
57
|
readonly numstat?: boolean;
|
|
56
58
|
readonly signal?: AbortSignal;
|
|
57
59
|
readonly stat?: boolean;
|
|
60
|
+
readonly requireComplete?: boolean;
|
|
58
61
|
}
|
|
59
62
|
export interface StatusOptions {
|
|
60
63
|
readonly pathspecs?: readonly string[];
|
|
@@ -136,6 +139,19 @@ export declare class GitCommandError extends Error {
|
|
|
136
139
|
readonly result: GitCommandResult;
|
|
137
140
|
constructor(args: readonly string[], result: GitCommandResult);
|
|
138
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* A git subprocess produced more output than {@link GIT_COMMAND_OUTPUT_LIMIT_BYTES}
|
|
144
|
+
* and its captured stdout was truncated. Thrown only for callers that opt into
|
|
145
|
+
* completeness via `diff({ requireComplete: true })`, where operating on a partial
|
|
146
|
+
* diff would silently corrupt downstream parsing — e.g. the split-commit builder,
|
|
147
|
+
* which would otherwise throw a misleading "No diff found" for files sorting after
|
|
148
|
+
* a large binary blob whose base85 payload pushed the diff past the cap.
|
|
149
|
+
*/
|
|
150
|
+
export declare class GitOutputTruncatedError extends Error {
|
|
151
|
+
readonly args: readonly string[];
|
|
152
|
+
readonly result: GitCommandResult;
|
|
153
|
+
constructor(args: readonly string[], result: GitCommandResult);
|
|
154
|
+
}
|
|
139
155
|
/** Default deadline for git and gh subprocesses spawned by the coding agent. */
|
|
140
156
|
export declare const GIT_COMMAND_TIMEOUT_MS: number;
|
|
141
157
|
/**
|
|
@@ -367,7 +383,15 @@ export declare const patch: {
|
|
|
367
383
|
canApply(cwd: string, patchPath: string, options?: Omit<PatchOptions, "check">): Promise<boolean>;
|
|
368
384
|
/** Check if a patch string can be applied cleanly. */
|
|
369
385
|
canApplyText(cwd: string, patchText: string, options?: Omit<PatchOptions, "check">): Promise<boolean>;
|
|
370
|
-
/**
|
|
386
|
+
/**
|
|
387
|
+
* Join patch parts into a single patch string.
|
|
388
|
+
*
|
|
389
|
+
* Each part is terminated with a single `\n` if it lacks one, then parts are
|
|
390
|
+
* concatenated verbatim — matching git's native multi-file diff layout. Parts
|
|
391
|
+
* are NOT separated by an extra blank line and trailing newlines are NOT
|
|
392
|
+
* stripped: a `GIT binary patch` block ends in a blank line that
|
|
393
|
+
* `git apply --binary` requires, and stripping it corrupts the patch (#8899).
|
|
394
|
+
*/
|
|
371
395
|
join(parts: string[]): string;
|
|
372
396
|
};
|
|
373
397
|
export declare const cherryPick: ((cwd: string, revision: string, signal?: AbortSignal) => Promise<void>) & {
|
|
@@ -15,6 +15,10 @@ export interface TinyFishSearchParams {
|
|
|
15
15
|
page?: number;
|
|
16
16
|
include_domains?: string[];
|
|
17
17
|
exclude_domains?: string[];
|
|
18
|
+
/** ISO 3166-1 alpha-2 region, e.g. `IT`. Geolocates results. */
|
|
19
|
+
location?: string;
|
|
20
|
+
/** ISO 639-1 language, e.g. `it`. */
|
|
21
|
+
language?: string;
|
|
18
22
|
signal?: AbortSignal;
|
|
19
23
|
timeoutMs?: number;
|
|
20
24
|
fetch?: FetchImpl;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.8",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,18 +48,18 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@babel/parser": "^7.29.7",
|
|
51
|
-
"@oh-my-pi/hashline": "17.3.
|
|
52
|
-
"@oh-my-pi/omp-stats": "17.3.
|
|
53
|
-
"@oh-my-pi/omptype": "17.3.
|
|
54
|
-
"@oh-my-pi/pi-agent-core": "17.3.
|
|
55
|
-
"@oh-my-pi/pi-ai": "17.3.
|
|
56
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
57
|
-
"@oh-my-pi/pi-mnemopi": "17.3.
|
|
58
|
-
"@oh-my-pi/pi-natives": "17.3.
|
|
59
|
-
"@oh-my-pi/pi-tui": "17.3.
|
|
60
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
61
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
62
|
-
"@oh-my-pi/snapcompact": "17.3.
|
|
51
|
+
"@oh-my-pi/hashline": "17.3.8",
|
|
52
|
+
"@oh-my-pi/omp-stats": "17.3.8",
|
|
53
|
+
"@oh-my-pi/omptype": "17.3.8",
|
|
54
|
+
"@oh-my-pi/pi-agent-core": "17.3.8",
|
|
55
|
+
"@oh-my-pi/pi-ai": "17.3.8",
|
|
56
|
+
"@oh-my-pi/pi-catalog": "17.3.8",
|
|
57
|
+
"@oh-my-pi/pi-mnemopi": "17.3.8",
|
|
58
|
+
"@oh-my-pi/pi-natives": "17.3.8",
|
|
59
|
+
"@oh-my-pi/pi-tui": "17.3.8",
|
|
60
|
+
"@oh-my-pi/pi-utils": "17.3.8",
|
|
61
|
+
"@oh-my-pi/pi-wire": "17.3.8",
|
|
62
|
+
"@oh-my-pi/snapcompact": "17.3.8",
|
|
63
63
|
"@opentelemetry/api": "^1.9.1",
|
|
64
64
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
65
65
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -110,8 +110,10 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
110
110
|
* auto-resume anything, so it is delivered live. Parking it during an active
|
|
111
111
|
* run instead strands it (it never reaches the running agent) and the withheld
|
|
112
112
|
* notes dump as one burst at the next user prompt — the bug this guards.
|
|
113
|
-
* - During the post-interrupt immune-turn window, further `concern
|
|
114
|
-
*
|
|
113
|
+
* - During the post-interrupt immune-turn window, further `concern` notes are
|
|
114
|
+
* downgraded to asides; preservation still wins. A `blocker` is exempt: it
|
|
115
|
+
* means the agent handed off broken or unexercised work, so it still steers a
|
|
116
|
+
* triggered turn even right after a prior interrupt (#5628).
|
|
115
117
|
*/
|
|
116
118
|
export function resolveAdvisorDeliveryChannel(opts: {
|
|
117
119
|
severity: AdvisorSeverity | undefined;
|
|
@@ -127,7 +129,7 @@ export function resolveAdvisorDeliveryChannel(opts: {
|
|
|
127
129
|
if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
|
|
128
130
|
if (opts.terminalAnswerNoQueuedWork && opts.severity !== "blocker" && !opts.streaming && !opts.aborting)
|
|
129
131
|
return "preserve";
|
|
130
|
-
if (opts.interruptImmuneTurnActive) return "aside";
|
|
132
|
+
if (opts.interruptImmuneTurnActive && opts.severity !== "blocker") return "aside";
|
|
131
133
|
return "steer";
|
|
132
134
|
}
|
|
133
135
|
|
|
@@ -33,10 +33,14 @@ import {
|
|
|
33
33
|
SqliteAuthCredentialStore,
|
|
34
34
|
} from "@oh-my-pi/pi-ai";
|
|
35
35
|
import { AuthBrokerClient, DEFAULT_AUTH_BROKER_BIND, startAuthBroker } from "@oh-my-pi/pi-ai/auth-broker";
|
|
36
|
+
import { refreshOAuthToken } from "@oh-my-pi/pi-ai/oauth";
|
|
37
|
+
import type { OAuthCredentials } from "@oh-my-pi/pi-ai/oauth/types";
|
|
36
38
|
import { $which, APP_NAME, getAgentDbPath, getConfigRootDir, isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
37
39
|
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
38
40
|
import { setTransports as setLoggerTransports } from "@oh-my-pi/pi-utils/logger";
|
|
39
41
|
import { $ } from "bun";
|
|
42
|
+
import { refreshManagedMcpOAuthCredential } from "../mcp/oauth-credentials";
|
|
43
|
+
import { isManagedMCPOAuthCredentialId, mcpOAuthServerUrlFromCredentialId } from "../mcp/oauth-flow";
|
|
40
44
|
import { resolveAuthBrokerConfig } from "../session/auth-broker-config";
|
|
41
45
|
|
|
42
46
|
export type AuthBrokerAction = "serve" | "token" | "login" | "logout" | "status" | "import" | "migrate" | "list";
|
|
@@ -119,6 +123,34 @@ async function ensureToken(): Promise<string> {
|
|
|
119
123
|
return token;
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
/**
|
|
127
|
+
* OAuth refresh handler for `omp auth-broker serve`'s {@link AuthStorage}.
|
|
128
|
+
*
|
|
129
|
+
* The vault holds provider OAuth rows AND OMP-managed `mcp_oauth:*` rows.
|
|
130
|
+
* Provider rows refresh through the per-provider registry. MCP rows are
|
|
131
|
+
* self-describing — the embedded token endpoint and client credentials are the
|
|
132
|
+
* only refresh material — so they refresh with a generic `refresh_token` grant.
|
|
133
|
+
* The serve process never loads the MCP manager, so this is the only place that
|
|
134
|
+
* teaches the broker to refresh MCP tokens; without it
|
|
135
|
+
* `POST /v1/credential/:id/refresh` fails with "Unknown OAuth provider" and the
|
|
136
|
+
* background refresher lets MCP access tokens expire (issue #8933).
|
|
137
|
+
*/
|
|
138
|
+
export function refreshBrokerOAuthCredential(
|
|
139
|
+
provider: string,
|
|
140
|
+
credential: OAuthCredential,
|
|
141
|
+
signal?: AbortSignal,
|
|
142
|
+
): Promise<OAuthCredentials> {
|
|
143
|
+
if (isManagedMCPOAuthCredentialId(provider)) {
|
|
144
|
+
return refreshManagedMcpOAuthCredential(credential, {
|
|
145
|
+
serverUrl: mcpOAuthServerUrlFromCredentialId(provider),
|
|
146
|
+
signal,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Non-MCP rows: same per-provider path AuthStorage would take by default
|
|
150
|
+
// (the serve process registers no custom OAuth providers).
|
|
151
|
+
return refreshOAuthToken(provider as OAuthProvider, credential);
|
|
152
|
+
}
|
|
153
|
+
|
|
122
154
|
async function runServe(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
|
123
155
|
// The broker is a long-running headless service: route structured logs to
|
|
124
156
|
// stdout so a process supervisor (pm2, journald, k8s) captures them, and
|
|
@@ -129,7 +161,10 @@ async function runServe(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
|
|
|
129
161
|
const token = await ensureToken();
|
|
130
162
|
const dbPath = getAgentDbPath();
|
|
131
163
|
const store = await SqliteAuthCredentialStore.open(dbPath);
|
|
132
|
-
const storage = new AuthStorage(store
|
|
164
|
+
const storage = new AuthStorage(store, {
|
|
165
|
+
refreshOAuthCredential: (provider, _credentialId, credential, signal) =>
|
|
166
|
+
refreshBrokerOAuthCredential(provider, credential, signal),
|
|
167
|
+
});
|
|
133
168
|
await storage.reload();
|
|
134
169
|
const handle = startAuthBroker({
|
|
135
170
|
storage,
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* them also activates (`omp --print --profile work`).
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { isSubcommand } from "../cli-commands";
|
|
36
|
+
import { isSubcommand, LAUNCH_FLAG_COMMANDS } from "../cli-commands";
|
|
37
37
|
import {
|
|
38
38
|
EXTENSION_SHADOWABLE_STRING_FLAGS,
|
|
39
39
|
isUnknownLongValueCandidate,
|
|
@@ -43,10 +43,6 @@ import {
|
|
|
43
43
|
STRING_VALUE_FLAGS,
|
|
44
44
|
} from "./flag-tables";
|
|
45
45
|
|
|
46
|
-
function isProfileBootstrapSubcommand(arg: string): boolean {
|
|
47
|
-
return arg === "launch" || arg === "acp";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
function needsBoundaryAfterGlobalStrip(stripped: readonly string[]): boolean {
|
|
51
47
|
const previous = stripped[stripped.length - 1];
|
|
52
48
|
return (
|
|
@@ -222,7 +218,7 @@ export function extractProfileFlags(argv: readonly string[]): ProfileBootstrapRe
|
|
|
222
218
|
// any other token has been forwarded, later subcommand names are launch text.
|
|
223
219
|
// `launch` and `acp` are explicit spellings of launch-shaped commands, so
|
|
224
220
|
// global launch flags that follow them must still be extracted.
|
|
225
|
-
if (canDispatchSubcommand && isSubcommand(arg) &&
|
|
221
|
+
if (canDispatchSubcommand && isSubcommand(arg) && LAUNCH_FLAG_COMMANDS[arg] !== true) {
|
|
226
222
|
sawSubcommand = true;
|
|
227
223
|
}
|
|
228
224
|
canDispatchSubcommand = false;
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -15,7 +15,12 @@ import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
|
15
15
|
import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
|
|
16
16
|
import { $ } from "bun";
|
|
17
17
|
import { theme } from "../modes/theme/theme";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isTimeoutError,
|
|
20
|
+
isUnsupportedProxyError,
|
|
21
|
+
unsupportedProxyMessage,
|
|
22
|
+
withTimeoutSignal,
|
|
23
|
+
} from "../utils/fetch-timeout";
|
|
19
24
|
|
|
20
25
|
const REPO = "can1357/oh-my-pi";
|
|
21
26
|
const PACKAGE = "@oh-my-pi/pi-coding-agent";
|
|
@@ -248,6 +253,7 @@ async function getReleaseBinaryAsset(
|
|
|
248
253
|
if (isTimeoutError(err)) {
|
|
249
254
|
throw new Error("Timed out fetching GitHub release metadata after 30s", { cause: err });
|
|
250
255
|
}
|
|
256
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
251
257
|
throw err;
|
|
252
258
|
}
|
|
253
259
|
if ((response.status === 403 && !githubToken) || response.status === 429) {
|
|
@@ -287,6 +293,7 @@ export async function downloadVerifiedBinary(options: VerifiedBinaryDownloadOpti
|
|
|
287
293
|
if (isTimeoutError(err)) {
|
|
288
294
|
throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
|
|
289
295
|
}
|
|
296
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
290
297
|
throw err;
|
|
291
298
|
}
|
|
292
299
|
if (!response.ok || !response.body) {
|
|
@@ -326,6 +333,7 @@ export async function downloadVerifiedBinary(options: VerifiedBinaryDownloadOpti
|
|
|
326
333
|
if (isTimeoutError(err)) {
|
|
327
334
|
throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
|
|
328
335
|
}
|
|
336
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
329
337
|
throw err;
|
|
330
338
|
}
|
|
331
339
|
}
|
|
@@ -441,6 +449,14 @@ function tryRealpath(p: string): string | undefined {
|
|
|
441
449
|
}
|
|
442
450
|
}
|
|
443
451
|
|
|
452
|
+
function isSymlinkPath(p: string): boolean {
|
|
453
|
+
try {
|
|
454
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
455
|
+
} catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
444
460
|
function isPathInDirectoryLexical(filePath: string, directoryPath: string): boolean {
|
|
445
461
|
const normalizedPath = normalizePathForComparison(path.resolve(filePath));
|
|
446
462
|
const normalizedDirectory = normalizePathForComparison(path.resolve(directoryPath));
|
|
@@ -509,6 +525,14 @@ interface UpdateMethodResolutionOptions {
|
|
|
509
525
|
* preserves a global package symlink instead of resolving into its checkout.
|
|
510
526
|
*/
|
|
511
527
|
ompLinkTarget?: string;
|
|
528
|
+
/**
|
|
529
|
+
* Whether package-manager routing (bun/npm) is permitted. Binary-only
|
|
530
|
+
* releases pass `false`: a manager launcher then resolves to `"binary"` and
|
|
531
|
+
* is taken over in place rather than reinstalled through its manager. Defaults
|
|
532
|
+
* to `true` in {@link resolveUpdateMethod} so callers that only classify need
|
|
533
|
+
* not set it.
|
|
534
|
+
*/
|
|
535
|
+
allowPackageManagers?: boolean;
|
|
512
536
|
}
|
|
513
537
|
|
|
514
538
|
type UpdateTarget =
|
|
@@ -525,6 +549,7 @@ function resolveUpdateMethod(
|
|
|
525
549
|
options: UpdateMethodResolutionOptions = {},
|
|
526
550
|
): UpdateMethod {
|
|
527
551
|
const {
|
|
552
|
+
allowPackageManagers = true,
|
|
528
553
|
bunGlobalDir,
|
|
529
554
|
homebrewPrefix,
|
|
530
555
|
miseBinDirs = [],
|
|
@@ -555,6 +580,7 @@ function resolveUpdateMethod(
|
|
|
555
580
|
globalBinDir: bunBinDir,
|
|
556
581
|
});
|
|
557
582
|
if (
|
|
583
|
+
allowPackageManagers &&
|
|
558
584
|
bunBinDir &&
|
|
559
585
|
isPathInDirectory(ompPath, bunBinDir) &&
|
|
560
586
|
!isStandaloneRegularFile &&
|
|
@@ -564,6 +590,7 @@ function resolveUpdateMethod(
|
|
|
564
590
|
}
|
|
565
591
|
const npmNodeModulesDir = resolveNpmGlobalNodeModulesDir(npmBinDir);
|
|
566
592
|
if (
|
|
593
|
+
allowPackageManagers &&
|
|
567
594
|
npmBinDir &&
|
|
568
595
|
isPathInDirectory(ompPath, npmBinDir) &&
|
|
569
596
|
!isStandaloneRegularFile &&
|
|
@@ -611,10 +638,25 @@ export function resolveUpdateTargetFromPath(
|
|
|
611
638
|
ompLinkTarget,
|
|
612
639
|
});
|
|
613
640
|
if (method === "binary") {
|
|
614
|
-
// A
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
|
|
641
|
+
// A symlinked launcher created by bun/npm is taken over in place on a
|
|
642
|
+
// binary-only release: routing through the manager is impossible, so the
|
|
643
|
+
// standalone binary replaces the launcher and keeps the PATH entry live.
|
|
644
|
+
// Every other symlink — a foreign alias, or an admin symlink into a
|
|
645
|
+
// shared install — is self-healing: update the real binary it resolves
|
|
646
|
+
// to and leave the launcher untouched, in every distribution channel.
|
|
647
|
+
// The old channel gate clobbered these foreign launchers on binary-only
|
|
648
|
+
// releases (EACCES on a root-owned link dir, or a stale split-brain copy
|
|
649
|
+
// of the binary shadowing the shared install).
|
|
650
|
+
const managerLauncher =
|
|
651
|
+
ompIsSymlink &&
|
|
652
|
+
!options.allowPackageManagers &&
|
|
653
|
+
resolveUpdateMethod(ompPath, bunBinDir, {
|
|
654
|
+
...options,
|
|
655
|
+
allowPackageManagers: true,
|
|
656
|
+
ompIsRegularFile,
|
|
657
|
+
ompLinkTarget,
|
|
658
|
+
}) !== "binary";
|
|
659
|
+
const binaryPath = ompIsSymlink && !managerLauncher ? (ompRealpath ?? ompPath) : ompPath;
|
|
618
660
|
return { method, path: binaryPath, replacesSymlink: ompIsSymlink && binaryPath === ompPath };
|
|
619
661
|
}
|
|
620
662
|
if (method === "bun" || method === "npm") return { method, path: ompPath };
|
|
@@ -623,25 +665,34 @@ export function resolveUpdateTargetFromPath(
|
|
|
623
665
|
/**
|
|
624
666
|
* Resolve how the running install should be updated.
|
|
625
667
|
*
|
|
626
|
-
* `allowPackageManagers: false`
|
|
627
|
-
*
|
|
628
|
-
*
|
|
668
|
+
* `allowPackageManagers: false` disables bun/npm routing — used for
|
|
669
|
+
* binary-only releases, where reinstalling through a package manager is never
|
|
670
|
+
* valid. The `bun pm bin -g` / `npm prefix -g` probes are then skipped unless
|
|
671
|
+
* the launcher is a symlink, whose bin dirs distinguish a manager launcher
|
|
672
|
+
* (taken over in place) from a foreign symlink (resolved to its real binary).
|
|
629
673
|
* Homebrew/mise detection always runs: both managers install GitHub release
|
|
630
674
|
* binaries and stay valid regardless of how the release is distributed.
|
|
631
675
|
*/
|
|
632
676
|
async function resolveUpdateTarget(options: { allowPackageManagers: boolean }): Promise<UpdateTarget> {
|
|
633
|
-
const bunBinDir = options.allowPackageManagers ? await getBunGlobalBinDir() : undefined;
|
|
634
|
-
const npmBinDir = options.allowPackageManagers ? await getNpmGlobalBinDir() : undefined;
|
|
635
677
|
const homebrewPrefix = await getHomebrewFormulaPrefix();
|
|
636
678
|
const miseAvailable = $which("mise") !== undefined;
|
|
637
679
|
const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
|
|
638
680
|
const miseDataDir = miseAvailable ? getMiseDataDir() : undefined;
|
|
639
681
|
const ompPath = resolveOmpPath();
|
|
640
682
|
|
|
683
|
+
// Binary-only releases skip package-manager routing, but a symlinked
|
|
684
|
+
// launcher still needs the manager bin dirs to tell a bun/npm launcher
|
|
685
|
+
// (taken over in place) from a foreign symlink (resolved to its real
|
|
686
|
+
// binary). A plain-file install never needs the distinction, so the common
|
|
687
|
+
// case stays probe-free.
|
|
688
|
+
const probeManagers = options.allowPackageManagers || (ompPath !== undefined && isSymlinkPath(ompPath));
|
|
689
|
+
const bunBinDir = probeManagers ? await getBunGlobalBinDir() : undefined;
|
|
690
|
+
const npmBinDir = probeManagers ? await getNpmGlobalBinDir() : undefined;
|
|
691
|
+
|
|
641
692
|
if (ompPath) {
|
|
642
693
|
return resolveUpdateTargetFromPath(ompPath, bunBinDir, {
|
|
643
694
|
allowPackageManagers: options.allowPackageManagers,
|
|
644
|
-
bunGlobalDir:
|
|
695
|
+
bunGlobalDir: probeManagers ? process.env.BUN_INSTALL_GLOBAL_DIR : undefined,
|
|
645
696
|
homebrewPrefix,
|
|
646
697
|
miseBinDirs,
|
|
647
698
|
miseDataDir,
|
|
@@ -672,6 +723,7 @@ async function fetchLatestManifest(
|
|
|
672
723
|
cause: err,
|
|
673
724
|
});
|
|
674
725
|
}
|
|
726
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
675
727
|
throw err;
|
|
676
728
|
}
|
|
677
729
|
if (!response.ok) {
|
package/src/cli-commands.ts
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { CommandEntry } from "@oh-my-pi/pi-utils/cli";
|
|
12
12
|
import * as commandHelp from "./cli/command-help";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
EXTENSION_SHADOWABLE_STRING_FLAGS,
|
|
15
|
+
flagConsumesValue,
|
|
16
|
+
OPTIONAL_VALUE_FLAGS,
|
|
17
|
+
STRING_VALUE_FLAGS,
|
|
18
|
+
VALUELESS_FLAGS,
|
|
19
|
+
} from "./cli/flag-tables";
|
|
14
20
|
import { launchHelp } from "./commands/launch-help";
|
|
15
21
|
|
|
16
22
|
export const commands: CommandEntry[] = [
|
|
@@ -285,12 +291,54 @@ function leadingSubcommandIndex(argv: string[]): number {
|
|
|
285
291
|
return -1;
|
|
286
292
|
}
|
|
287
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Subcommands that share the launch flag surface, so leading global flags
|
|
296
|
+
* (`--cwd`, `--model`, `--approval-mode`, …) placed before them are meaningful
|
|
297
|
+
* and must be forwarded ({@link resolveCliArgv}, #2970). Every other subcommand
|
|
298
|
+
* parses only its own flags.
|
|
299
|
+
*/
|
|
300
|
+
export const LAUNCH_FLAG_COMMANDS: Record<string, true> = { launch: true, acp: true };
|
|
301
|
+
|
|
302
|
+
/** Whether `arg` names a flag from the launch surface (bare or `--flag=value`). */
|
|
303
|
+
function isLaunchGlobalFlag(arg: string): boolean {
|
|
304
|
+
const eq = arg.indexOf("=");
|
|
305
|
+
const name = arg.startsWith("--") && eq !== -1 ? arg.slice(0, eq) : arg;
|
|
306
|
+
return (
|
|
307
|
+
STRING_VALUE_FLAGS.has(name) ||
|
|
308
|
+
OPTIONAL_VALUE_FLAGS.has(name) ||
|
|
309
|
+
VALUELESS_FLAGS.has(name) ||
|
|
310
|
+
EXTENSION_SHADOWABLE_STRING_FLAGS.has(name)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Drop recognized launch-global flags (and any value they consume) from the
|
|
316
|
+
* leading segment before a hoisted non-launch subcommand. `--cwd` and friends
|
|
317
|
+
* belong to the launch surface and mean nothing to a subcommand like `update`,
|
|
318
|
+
* whose strict parser would otherwise reject them with a cryptic
|
|
319
|
+
* `node:util.parseArgs` error (#8891). Tokens the launch tables don't recognize
|
|
320
|
+
* are kept, so a subcommand's own leading flags still reach it.
|
|
321
|
+
*/
|
|
322
|
+
function stripLaunchGlobalFlags(leading: readonly string[]): string[] {
|
|
323
|
+
const kept: string[] = [];
|
|
324
|
+
for (let index = 0; index < leading.length; index += 1) {
|
|
325
|
+
const arg = leading[index];
|
|
326
|
+
if (isLaunchGlobalFlag(arg)) {
|
|
327
|
+
if (flagConsumesValue(arg, leading[index + 1])) index += 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
kept.push(arg);
|
|
331
|
+
}
|
|
332
|
+
return kept;
|
|
333
|
+
}
|
|
334
|
+
|
|
288
335
|
/**
|
|
289
336
|
* Decide what the CLI runner should do with raw argv: reject bare reserved
|
|
290
337
|
* management words, pass help/version through untouched, route a recognized
|
|
291
338
|
* subcommand (even behind leading global flags like `--approval-mode=yolo`) to
|
|
292
|
-
* that command
|
|
293
|
-
*
|
|
339
|
+
* that command, and forward everything else to `launch` (#2970). Leading
|
|
340
|
+
* launch-global flags are forwarded to launch-shaped commands but stripped for
|
|
341
|
+
* other subcommands that cannot parse them (#8891).
|
|
294
342
|
*/
|
|
295
343
|
export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
|
296
344
|
const first = argv[0];
|
|
@@ -302,12 +350,18 @@ export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
|
|
302
350
|
if (isSubcommand(first)) return { argv };
|
|
303
351
|
// A subcommand can hide behind leading global option flags
|
|
304
352
|
// (`omp --approval-mode=yolo acp`). `run` dispatches strictly on argv[0], so
|
|
305
|
-
// hoist the subcommand to the front
|
|
306
|
-
//
|
|
307
|
-
//
|
|
353
|
+
// hoist the subcommand to the front. Launch-shaped commands share the launch
|
|
354
|
+
// flag surface, so their leading flags are forwarded and applied; every other
|
|
355
|
+
// subcommand parses only its own flags, so launch-global flags placed before
|
|
356
|
+
// it (`omp --cwd <dir> update`) are stripped rather than forwarded into a
|
|
357
|
+
// crash (#8891). Genuine launch prompts (no trailing subcommand) are untouched.
|
|
308
358
|
const subIndex = leadingSubcommandIndex(argv);
|
|
309
359
|
if (subIndex >= 0) {
|
|
310
|
-
|
|
360
|
+
const sub = argv[subIndex];
|
|
361
|
+
const leading = argv.slice(0, subIndex);
|
|
362
|
+
const trailing = argv.slice(subIndex + 1);
|
|
363
|
+
const forwardedLeading = LAUNCH_FLAG_COMMANDS[sub] === true ? leading : stripLaunchGlobalFlags(leading);
|
|
364
|
+
return { argv: [sub, ...forwardedLeading, ...trailing] };
|
|
311
365
|
}
|
|
312
366
|
return { argv: ["launch", ...argv] };
|
|
313
367
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (see `cli/completion-gen.ts`), so it never drifts from the actual CLI surface.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { APP_NAME, postmortem, VERSION } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { Args, type CliConfig, Command, type CommandCtor } from "@oh-my-pi/pi-utils/cli";
|
|
10
10
|
import { completionsHelp as commandHelp } from "../cli/command-help";
|
|
11
11
|
import { buildSpec, generateCompletion, type Shell } from "../cli/completion-gen";
|
|
@@ -55,6 +55,7 @@ export default class Completions extends Command {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
await Bun.write(Bun.stdout, await generateLiveCompletion(shell));
|
|
58
|
+
await postmortem.quit(0);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -11,7 +11,7 @@ import { ModelRegistry } from "../../config/model-registry";
|
|
|
11
11
|
import { Settings } from "../../config/settings";
|
|
12
12
|
import { discoverAuthStorage, discoverContextFiles, loadCliExtensionProviders } from "../../sdk";
|
|
13
13
|
import * as git from "../../utils/git";
|
|
14
|
-
import { abortOnGitFailure, pushOrAbort } from "../execute";
|
|
14
|
+
import { abortOnGitFailure, CommitAbortedError, pushOrAbort } from "../execute";
|
|
15
15
|
import { type ExistingChangelogEntries, runCommitAgentSession } from "./agent";
|
|
16
16
|
import { generateFallbackProposal } from "./fallback";
|
|
17
17
|
import { assignLockFilesToPlan } from "./lock-files";
|
|
@@ -303,7 +303,20 @@ async function runSplitCommit(
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
process.stdout.write("● Creating split commits...\n");
|
|
306
|
-
|
|
306
|
+
let stagedDiff: string;
|
|
307
|
+
try {
|
|
308
|
+
stagedDiff = await git.diff(ctx.cwd, { cached: true, binary: true, requireComplete: true });
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof git.GitOutputTruncatedError) {
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
`✗ Cannot create split commits: ${error.message}\n` +
|
|
313
|
+
" A large or binary file makes the staged diff too big to slice safely.\n" +
|
|
314
|
+
" Commit the large file(s) on their own, then re-run for the rest.\n",
|
|
315
|
+
);
|
|
316
|
+
throw new CommitAbortedError();
|
|
317
|
+
}
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
307
320
|
await git.stage.reset(ctx.cwd);
|
|
308
321
|
for (const [position, commitIndex] of order.entries()) {
|
|
309
322
|
const commit = plan.commits[commitIndex];
|
package/src/commit/git/diff.ts
CHANGED
|
@@ -21,9 +21,13 @@ export function parseNumstat(output: string): NumstatEntry[] {
|
|
|
21
21
|
|
|
22
22
|
export function parseFileDiffs(diff: string): FileDiff[] {
|
|
23
23
|
const sections: FileDiff[] = [];
|
|
24
|
-
|
|
24
|
+
// Split on a line-start lookahead so each block keeps its terminating
|
|
25
|
+
// newline(s) verbatim. Consuming the delimiter would drop the blank line
|
|
26
|
+
// that terminates a `GIT binary patch` block, corrupting binary diffs on
|
|
27
|
+
// rebuild (issue #8899).
|
|
28
|
+
const parts = diff.split(/^(?=diff --git )/m);
|
|
25
29
|
for (let index = 0; index < parts.length; index += 1) {
|
|
26
|
-
const part =
|
|
30
|
+
const part = parts[index];
|
|
27
31
|
if (!part.trim()) continue;
|
|
28
32
|
const lines = part.split("\n");
|
|
29
33
|
const header = lines[0] ?? "";
|
|
@@ -22,7 +22,7 @@ import { modelMatchesHost } from "@oh-my-pi/pi-catalog/hosts";
|
|
|
22
22
|
import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
|
|
23
23
|
import { stripThinkingVariantToken } from "@oh-my-pi/pi-catalog/identity/family";
|
|
24
24
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
25
|
-
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
25
|
+
import { type GeneratedProvider, getBundledModels, modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
26
26
|
import { DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models";
|
|
27
27
|
import { resolveBareVariantAlias, resolveVariantAlias } from "@oh-my-pi/pi-catalog/variant-collapse";
|
|
28
28
|
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
@@ -595,6 +595,41 @@ function includeSyntheticAllowedModels(available: Model<Api>[], allowedModels: I
|
|
|
595
595
|
return result;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Provider-lock a raw-id cross match.
|
|
600
|
+
*
|
|
601
|
+
* A slash-prefixed selector like `anthropic/claude-opus-5` is ambiguous: it is
|
|
602
|
+
* both the `anthropic` provider's canonical selector and — verbatim — an
|
|
603
|
+
* OpenRouter aggregator model id. When the named provider genuinely carries
|
|
604
|
+
* that model id in the bundled catalog but the model is missing from the
|
|
605
|
+
* candidate set (provider disabled, no credentials, or filtered out), letting
|
|
606
|
+
* the raw-id fallback re-bind the request onto a different provider's
|
|
607
|
+
* same-named model is a silent, expensive surprise — typically the aggregator's
|
|
608
|
+
* copy (OpenRouter bills published per-token Claude prices). Such a reference
|
|
609
|
+
* is provider-locked: it must fail rather than shadow.
|
|
610
|
+
*
|
|
611
|
+
* The lock only applies when the named provider carries the exact id in the
|
|
612
|
+
* bundled catalog. An aggregator raw id the named provider does NOT carry
|
|
613
|
+
* (e.g. `openai/gpt-4o:extended` — `openai` bundles `gpt-4o`, not the
|
|
614
|
+
* `:extended` variant) is legitimately an aggregator id and keeps resolving
|
|
615
|
+
* through the raw-id fallback, so bare aggregator selectors keep working.
|
|
616
|
+
*/
|
|
617
|
+
function isProviderLockedCrossMatch(pattern: string, matchedModel: Model<Api>): boolean {
|
|
618
|
+
const slashIdx = pattern.indexOf("/");
|
|
619
|
+
if (slashIdx <= 0) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const provider = pattern.slice(0, slashIdx).toLowerCase();
|
|
623
|
+
const modelId = pattern.slice(slashIdx + 1).toLowerCase();
|
|
624
|
+
if (matchedModel.provider.toLowerCase() === provider) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
// Case-insensitive on both halves: the surrounding matcher lowercases the
|
|
628
|
+
// selector before comparing ids, so the lock must not evaporate on case
|
|
629
|
+
// variance (catalog provider keys are lowercase; model ids may not be).
|
|
630
|
+
return getBundledModels(provider as GeneratedProvider).some(m => m.id.toLowerCase() === modelId);
|
|
631
|
+
}
|
|
632
|
+
|
|
598
633
|
/**
|
|
599
634
|
* Find an exact explicit provider/model match.
|
|
600
635
|
*/
|
|
@@ -644,11 +679,17 @@ function matchModel(
|
|
|
644
679
|
// Exact ID match (case-insensitive) — this must happen before provider-scoped
|
|
645
680
|
// fuzzy matching so raw IDs that contain slashes (for example OpenRouter model
|
|
646
681
|
// IDs like "openai/gpt-4o:extended") still resolve as IDs instead of being
|
|
647
|
-
// misread as a provider-qualified selector.
|
|
682
|
+
// misread as a provider-qualified selector. A provider-qualified pattern
|
|
683
|
+
// whose named provider carries the id stays locked to that provider when its
|
|
684
|
+
// only exact-id matches live on a different provider (isProviderLockedCrossMatch).
|
|
648
685
|
const lowerPattern = modelPattern.toLowerCase();
|
|
649
686
|
const exactMatches = availableModels.filter(m => m.id.toLowerCase() === lowerPattern);
|
|
650
687
|
if (exactMatches.length > 0) {
|
|
651
|
-
|
|
688
|
+
const unlockedMatches = exactMatches.filter(m => !isProviderLockedCrossMatch(modelPattern, m));
|
|
689
|
+
if (unlockedMatches.length > 0) {
|
|
690
|
+
return pickPreferredModel(unlockedMatches, context);
|
|
691
|
+
}
|
|
692
|
+
return undefined;
|
|
652
693
|
}
|
|
653
694
|
|
|
654
695
|
const bedrockInferenceProfile = resolveBedrockInferenceProfileModelId(modelPattern, availableModels);
|
|
@@ -1710,11 +1751,14 @@ function findExactCliModel(
|
|
|
1710
1751
|
// Flat-id (or full-selector-string) matches prefer authenticated providers,
|
|
1711
1752
|
// then fall back to catalog order. This covers aggregator-style flat ids
|
|
1712
1753
|
// that merely look provider-qualified (e.g. "openai/gpt-oss-120b" hosted on
|
|
1713
|
-
// OpenRouter), where the provider/id decomposition above found nothing.
|
|
1754
|
+
// OpenRouter), where the provider/id decomposition above found nothing. A
|
|
1755
|
+
// provider-qualified selector whose named provider carries the id must not
|
|
1756
|
+
// re-bind onto another provider's same-named flat id
|
|
1757
|
+
// (isProviderLockedCrossMatch); it stays provider-locked and fails instead.
|
|
1714
1758
|
const lower = selector.toLowerCase();
|
|
1715
1759
|
const isFlatMatch = (model: Model<Api>) =>
|
|
1716
1760
|
model.id.toLowerCase() === lower || formatModelString(model).toLowerCase() === lower;
|
|
1717
|
-
const preferred = availableModels.find(isFlatMatch);
|
|
1761
|
+
const preferred = availableModels.find(m => isFlatMatch(m) && !isProviderLockedCrossMatch(selector, m));
|
|
1718
1762
|
if (preferred) return preferred;
|
|
1719
1763
|
// The unauthenticated catalog fallback is a weak match: a bare id like
|
|
1720
1764
|
// `default` collides with the bundled `cursor/default` model, which must not
|
|
@@ -1723,7 +1767,9 @@ function findExactCliModel(
|
|
|
1723
1767
|
// role gets a chance first; the deferred fuzzy fallback below still recovers
|
|
1724
1768
|
// the catalog id when no role matches.
|
|
1725
1769
|
if (options?.catalogFallback === false) return undefined;
|
|
1726
|
-
return availableModels === allModels
|
|
1770
|
+
return availableModels === allModels
|
|
1771
|
+
? undefined
|
|
1772
|
+
: allModels.find(m => isFlatMatch(m) && !isProviderLockedCrossMatch(selector, m));
|
|
1727
1773
|
}
|
|
1728
1774
|
|
|
1729
1775
|
export interface ResolveCliModelResult {
|