@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
* `xd://report_issue`, and the system prompt tells the model to write
|
|
6
6
|
* `<tool>: <concise description>` there when auto-QA is enabled.
|
|
7
7
|
*
|
|
8
|
-
* Enabled by default
|
|
9
|
-
*
|
|
8
|
+
* Enabled by default (`dev.autoqa` defaults to true); `PI_AUTO_QA=0` or an
|
|
9
|
+
* explicit `dev.autoqa: false` short-circuits injection entirely. When the
|
|
10
|
+
* user is only enabled by default (never configured `dev.autoqa` themselves),
|
|
11
|
+
* a persisted `dev.autoqaConsent: "denied"` also disables injection so a "No"
|
|
12
|
+
* in the consent dialog fully turns the feature off.
|
|
10
13
|
* Records grievances to a local SQLite database; never throws from the device
|
|
11
14
|
* dispatch path.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Nothing is written until consent resolves. If the user has never been asked
|
|
17
|
+
* (`dev.autoqaConsent === "unset"`) the process-global consent handler —
|
|
18
|
+
* wired by `InteractiveMode` to a Yes/No popup — is invoked exactly once and
|
|
19
|
+
* the decision is persisted; a denial (or dismissal) drops the pending report
|
|
20
|
+
* without touching the database. Subsequent calls (including from subagents)
|
|
21
|
+
* read the cached decision without prompting. `PI_AUTO_QA_PUSH=1` bypasses
|
|
22
|
+
* the dialog for headless environments.
|
|
18
23
|
*
|
|
19
24
|
* When the user grants consent, push is automatically active against the
|
|
20
25
|
* bundled endpoint (`dev.autoqaPush.endpoint`, default `qa.omp.sh`). Each
|
|
@@ -42,6 +47,14 @@ export declare function isReportIssueToolCall(toolCall: {
|
|
|
42
47
|
}): boolean;
|
|
43
48
|
/** Call preview for an `xd://report_issue` write. */
|
|
44
49
|
export declare function renderReportIssueDeviceCall(content: unknown, uiTheme: Theme): Component;
|
|
50
|
+
/**
|
|
51
|
+
* Whether Auto-QA is active for this session.
|
|
52
|
+
*
|
|
53
|
+
* Precedence: `PI_AUTO_QA` env flag > explicit `dev.autoqa` setting >
|
|
54
|
+
* default-on unless the user previously denied consent. The denial veto only
|
|
55
|
+
* applies to the default: explicitly configuring `dev.autoqa: true` re-enables
|
|
56
|
+
* injection (recording still no-ops until consent is granted).
|
|
57
|
+
*/
|
|
45
58
|
export declare function isAutoQaEnabled(settings?: Settings): boolean;
|
|
46
59
|
/**
|
|
47
60
|
* Resolver for the user's "share grievances?" consent.
|
|
@@ -82,8 +95,8 @@ export declare function __resetAutoQaConsentForTests(): void;
|
|
|
82
95
|
export declare function resolveAutoQaConsent(settings: Settings | undefined): Promise<boolean>;
|
|
83
96
|
/**
|
|
84
97
|
* Open (or return the cached handle for) the auto-QA SQLite database at
|
|
85
|
-
* `~/.omp/
|
|
86
|
-
*
|
|
98
|
+
* `~/.omp/autoqa.db` (XDG: `$XDG_DATA_HOME/omp/autoqa.db`), creating the
|
|
99
|
+
* schema lazily. Returns `null` when the path cannot be resolved or opened.
|
|
87
100
|
*/
|
|
88
101
|
export declare function openAutoQaDb(): Database | null;
|
|
89
102
|
export interface FlushResult {
|
|
@@ -127,6 +140,8 @@ export declare function __resetAutoQaFlushStateForTests(): void;
|
|
|
127
140
|
* Flush queued grievances to the configured backend.
|
|
128
141
|
*/
|
|
129
142
|
export declare function flushGrievances(db?: Database, settings?: Settings, options?: FlushOptions): Promise<FlushResult>;
|
|
143
|
+
/** Test-only: await the last consent → insert → flush pipeline. */
|
|
144
|
+
export declare function __awaitAutoQaRecordPipelineForTests(): Promise<void>;
|
|
130
145
|
/**
|
|
131
146
|
* Execute `write xd://report_issue`. `text` must be either:
|
|
132
147
|
* - `<tool>: <concise description>` on one line, or
|
|
@@ -46,6 +46,12 @@ export declare const TOOL_TIMEOUTS: {
|
|
|
46
46
|
export type ToolWithTimeout = keyof typeof TOOL_TIMEOUTS;
|
|
47
47
|
/**
|
|
48
48
|
* Clamp a raw timeout to the allowed range for a tool.
|
|
49
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* When `rawTimeout` is undefined the tool's `default` is used. A positive
|
|
51
|
+
* `maxTimeout` (the `tools.maxTimeout` global ceiling) caps the *resolved*
|
|
52
|
+
* value — including the default-fallback path — before the per-tool `min`/`max`
|
|
53
|
+
* floor and ceiling apply, so a configured global cap governs calls where the
|
|
54
|
+
* agent omits `timeout`, not only explicitly-passed values. `maxTimeout <= 0`
|
|
55
|
+
* means no global cap.
|
|
50
56
|
*/
|
|
51
|
-
export declare function clampTimeout(tool: ToolWithTimeout, rawTimeout?: number): number;
|
|
57
|
+
export declare function clampTimeout(tool: ToolWithTimeout, rawTimeout?: number, maxTimeout?: number): number;
|
|
@@ -23,13 +23,11 @@ export interface StartupChangelogSelection {
|
|
|
23
23
|
selectedEntries: number;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Parse changelog entries from
|
|
27
|
-
*
|
|
26
|
+
* Parse changelog entries from omp's package asset when available, falling back
|
|
27
|
+
* to the copy embedded in compiled binaries.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* synthesize a fallback path from the host project's cwd; doing so caused issue
|
|
32
|
-
* #1423 (the host project's `CHANGELOG.md` was rendered as omp's).
|
|
29
|
+
* The embedded fallback keeps standalone binaries self-contained without
|
|
30
|
+
* resolving relative to the host project's cwd, which caused issue #1423.
|
|
33
31
|
*/
|
|
34
32
|
export declare function parseChangelog(changelogPath: string | undefined): Promise<ChangelogEntry[]>;
|
|
35
33
|
/**
|
|
@@ -23,6 +23,15 @@ export declare function searchFirecrawl(params: SearchParams): Promise<SearchRes
|
|
|
23
23
|
export declare class FirecrawlProvider extends SearchProvider {
|
|
24
24
|
readonly id = "firecrawl";
|
|
25
25
|
readonly label = "Firecrawl";
|
|
26
|
+
/**
|
|
27
|
+
* Auto-chain admission: requires a credential so an unconfigured Firecrawl
|
|
28
|
+
* doesn't displace other providers that the user has set up with API keys.
|
|
29
|
+
*/
|
|
26
30
|
isAvailable(authStorage: AuthStorage): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Firecrawl supports keyless mode, so an explicit user selection
|
|
33
|
+
* (`webSearch: firecrawl`) works without any credential configured.
|
|
34
|
+
*/
|
|
35
|
+
isExplicitlyAvailable(_authStorage: AuthStorage): boolean;
|
|
27
36
|
search(params: SearchParams): Promise<SearchResponse>;
|
|
28
37
|
}
|
|
@@ -54,7 +54,7 @@ export declare const SEARCH_PROVIDER_OPTIONS: readonly [{
|
|
|
54
54
|
}, {
|
|
55
55
|
readonly value: "firecrawl";
|
|
56
56
|
readonly label: "Firecrawl";
|
|
57
|
-
readonly description: "
|
|
57
|
+
readonly description: "Uses Firecrawl API when FIRECRAWL_API_KEY is set; falls back to keyless mode";
|
|
58
58
|
}, {
|
|
59
59
|
readonly value: "brave";
|
|
60
60
|
readonly label: "Brave";
|
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.0.
|
|
4
|
+
"version": "17.0.9",
|
|
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",
|
|
@@ -51,18 +51,19 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
|
+
"@babel/traverse": "^7.29.7",
|
|
54
55
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.0.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.0.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.0.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.0.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.0.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.0.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.0.
|
|
56
|
+
"@oh-my-pi/hashline": "17.0.9",
|
|
57
|
+
"@oh-my-pi/omp-stats": "17.0.9",
|
|
58
|
+
"@oh-my-pi/pi-agent-core": "17.0.9",
|
|
59
|
+
"@oh-my-pi/pi-ai": "17.0.9",
|
|
60
|
+
"@oh-my-pi/pi-catalog": "17.0.9",
|
|
61
|
+
"@oh-my-pi/pi-mnemopi": "17.0.9",
|
|
62
|
+
"@oh-my-pi/pi-natives": "17.0.9",
|
|
63
|
+
"@oh-my-pi/pi-tui": "17.0.9",
|
|
64
|
+
"@oh-my-pi/pi-utils": "17.0.9",
|
|
65
|
+
"@oh-my-pi/pi-wire": "17.0.9",
|
|
66
|
+
"@oh-my-pi/snapcompact": "17.0.9",
|
|
66
67
|
"@opentelemetry/api": "^1.9.1",
|
|
67
68
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
69
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -79,7 +80,6 @@
|
|
|
79
80
|
"@xterm/headless": "^6.0.0",
|
|
80
81
|
"arktype": "2.2.3",
|
|
81
82
|
"chalk": "^5.6.2",
|
|
82
|
-
"diff": "^9.0.0",
|
|
83
83
|
"fast-xml-parser": "^5.9.3",
|
|
84
84
|
"handlebars": "^4.7.9",
|
|
85
85
|
"header-generator": "^2.1.82",
|
|
@@ -19,7 +19,7 @@ const BUNDLED_PACKAGES: readonly BundledPackage[] = [
|
|
|
19
19
|
{ dir: "ai", identifier: "PiAi", rootShim: "legacy-pi-ai-shim.ts" },
|
|
20
20
|
{ dir: "coding-agent", identifier: "PiCodingAgent", rootShim: "legacy-pi-coding-agent-shim.ts" },
|
|
21
21
|
{ dir: "natives", identifier: "PiNatives", rootShim: null },
|
|
22
|
-
{ dir: "tui", identifier: "PiTui", rootShim:
|
|
22
|
+
{ dir: "tui", identifier: "PiTui", rootShim: "legacy-pi-tui-shim.ts" },
|
|
23
23
|
{ dir: "utils", identifier: "PiUtils", rootShim: null },
|
|
24
24
|
];
|
|
25
25
|
|
|
@@ -41,7 +41,7 @@ export async function listGrievances(options: ListGrievancesOptions): Promise<vo
|
|
|
41
41
|
console.log("[]");
|
|
42
42
|
} else {
|
|
43
43
|
console.log(
|
|
44
|
-
chalk.dim("No grievances database found.
|
|
44
|
+
chalk.dim("No grievances database found. Auto-QA has not recorded any reports yet (or was disabled)."),
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
return;
|
|
@@ -110,7 +110,7 @@ export async function cleanGrievances(options: CleanGrievancesOptions): Promise<
|
|
|
110
110
|
console.log(JSON.stringify({ deleted: 0 }));
|
|
111
111
|
} else {
|
|
112
112
|
console.log(
|
|
113
|
-
chalk.dim("No grievances database found.
|
|
113
|
+
chalk.dim("No grievances database found. Auto-QA has not recorded any reports yet (or was disabled)."),
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
return;
|
package/src/cli/models-cli.ts
CHANGED
|
@@ -17,8 +17,14 @@ import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { ModelRegistry } from "../config/model-registry";
|
|
19
19
|
import { Settings } from "../config/settings";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
discoverAndLoadExtensions,
|
|
22
|
+
ExtensionRunner,
|
|
23
|
+
emitSessionShutdownEvent,
|
|
24
|
+
loadExtensions,
|
|
25
|
+
} from "../extensibility/extensions";
|
|
21
26
|
import { discoverAuthStorage } from "../sdk";
|
|
27
|
+
import { SessionManager } from "../session/session-manager";
|
|
22
28
|
import { EventBus } from "../utils/event-bus";
|
|
23
29
|
|
|
24
30
|
export type ModelsAction = "ls" | "find" | "refresh";
|
|
@@ -303,25 +309,39 @@ export async function runModelsListing(options: RunModelsListingOptions): Promis
|
|
|
303
309
|
eventBus,
|
|
304
310
|
disabledExtensionIds,
|
|
305
311
|
);
|
|
312
|
+
const extensionRunner =
|
|
313
|
+
extensionsResult.extensions.length > 0
|
|
314
|
+
? new ExtensionRunner(
|
|
315
|
+
extensionsResult.extensions,
|
|
316
|
+
extensionsResult.runtime,
|
|
317
|
+
cwd,
|
|
318
|
+
SessionManager.inMemory(cwd),
|
|
319
|
+
modelRegistry,
|
|
320
|
+
)
|
|
321
|
+
: undefined;
|
|
306
322
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
323
|
+
try {
|
|
324
|
+
for (const { path: extPath, error } of extensionsResult.errors) {
|
|
325
|
+
process.stderr.write(`Failed to load extension: ${extPath}: ${error}\n`);
|
|
326
|
+
}
|
|
310
327
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
// Mirror sdk.ts: drain pending provider registrations into the registry.
|
|
329
|
+
const activeSources = extensionsResult.extensions.map(extension => extension.path);
|
|
330
|
+
modelRegistry.syncExtensionSources(activeSources);
|
|
331
|
+
for (const sourceId of new Set(activeSources)) {
|
|
332
|
+
modelRegistry.clearSourceRegistrations(sourceId);
|
|
333
|
+
}
|
|
334
|
+
for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
|
|
335
|
+
modelRegistry.registerProvider(name, config, sourceId);
|
|
336
|
+
}
|
|
337
|
+
extensionsResult.runtime.pendingProviderRegistrations = [];
|
|
338
|
+
// Discover runtime (extension) provider catalogs now that they are registered.
|
|
339
|
+
await modelRegistry.refreshRuntimeProviders(action === "refresh" ? "online" : "online-if-uncached");
|
|
323
340
|
|
|
324
|
-
|
|
341
|
+
renderProviderModels(modelRegistry, action, pattern, json);
|
|
342
|
+
} finally {
|
|
343
|
+
await emitSessionShutdownEvent(extensionRunner);
|
|
344
|
+
}
|
|
325
345
|
}
|
|
326
346
|
|
|
327
347
|
/**
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -556,6 +556,19 @@ async function pruneBunCacheAfterGlobalInstall(): Promise<BunInstallCachePruneRe
|
|
|
556
556
|
return await pruneBunInstallCache(cacheDir, packageNames.size === 0 ? undefined : packageNames);
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Detect a musl-libc Linux host (Alpine, Void-musl) so self-update replaces a
|
|
561
|
+
* musl binary with the musl release asset instead of the glibc build, which
|
|
562
|
+
* would fail to start on the next run. Mirrors the detection in
|
|
563
|
+
* scripts/install.sh.
|
|
564
|
+
*/
|
|
565
|
+
function isMuslLinux(): boolean {
|
|
566
|
+
if (process.platform !== "linux") return false;
|
|
567
|
+
if (fs.existsSync("/etc/alpine-release")) return true;
|
|
568
|
+
const loaderArch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
|
569
|
+
return fs.existsSync(`/lib/ld-musl-${loaderArch}.so.1`);
|
|
570
|
+
}
|
|
571
|
+
|
|
559
572
|
/**
|
|
560
573
|
* Get the appropriate binary name for this platform.
|
|
561
574
|
*/
|
|
@@ -566,7 +579,7 @@ function getBinaryName(): string {
|
|
|
566
579
|
let os: string;
|
|
567
580
|
switch (platform) {
|
|
568
581
|
case "linux":
|
|
569
|
-
os = "linux";
|
|
582
|
+
os = isMuslLinux() ? "linux-musl" : "linux";
|
|
570
583
|
break;
|
|
571
584
|
case "darwin":
|
|
572
585
|
os = "darwin";
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -409,7 +409,7 @@ function formatLimitLine(limit: UsageLimit, labelWidth: number, nowMs: number):
|
|
|
409
409
|
const details: string[] = [describeAmount(limit)];
|
|
410
410
|
const resetsAt = limit.window?.resetsAt;
|
|
411
411
|
if (resetsAt !== undefined && resetsAt > nowMs) {
|
|
412
|
-
details.push(
|
|
412
|
+
details.push(`${limit.window?.resetLabel ?? "resets"} in ${formatDuration(resetsAt - nowMs)}`);
|
|
413
413
|
}
|
|
414
414
|
const lines = [
|
|
415
415
|
` ${STATUS_COLOR[status]("●")} ${padded} ${renderBar(limit)} ${chalk.dim(details.join(" · "))}`,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { ModelRegistry } from "../model-registry";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stub AuthStorage that satisfies the surface used by ModelRegistry's
|
|
10
|
+
* constructor (#loadModels → clearConfigApiKeys, constructor →
|
|
11
|
+
* setFallbackResolver). The awaiter under test never reaches auth-gated code
|
|
12
|
+
* paths, so no real credential store is required.
|
|
13
|
+
*/
|
|
14
|
+
function createStubAuthStorage(): AuthStorage {
|
|
15
|
+
const stub = {
|
|
16
|
+
setFallbackResolver: () => {},
|
|
17
|
+
clearConfigApiKeys: () => {},
|
|
18
|
+
hasAuth: () => false,
|
|
19
|
+
};
|
|
20
|
+
return stub as unknown as AuthStorage;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("ModelRegistry.awaitBackgroundRefresh", () => {
|
|
24
|
+
let tmpDir: string;
|
|
25
|
+
let registry: ModelRegistry;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmpDir = mkdtempSync(path.join(os.tmpdir(), "omp-reg-"));
|
|
29
|
+
// Construct with an explicit modelsPath inside the temp dir so the
|
|
30
|
+
// constructor's #loadModels read returns "not-found" rather than
|
|
31
|
+
// touching the host's ~/.omp/agent/models.yaml. isBunTestRuntime()
|
|
32
|
+
// auto-stubs #fetch in the constructor.
|
|
33
|
+
registry = new ModelRegistry(createStubAuthStorage(), path.join(tmpDir, "models.yaml"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolves immediately when no background refresh is in flight", async () => {
|
|
42
|
+
// No refreshInBackground() called → #backgroundRefresh is undefined.
|
|
43
|
+
// The awaiter must settle within a single microtask, never hanging.
|
|
44
|
+
let settled = false;
|
|
45
|
+
const p = registry.awaitBackgroundRefresh().then(() => {
|
|
46
|
+
settled = true;
|
|
47
|
+
});
|
|
48
|
+
await Promise.resolve();
|
|
49
|
+
await p;
|
|
50
|
+
expect(settled).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("blocks until the in-flight background refresh resolves, then resolves", async () => {
|
|
54
|
+
// Drive refreshInBackground with a controlled refresh() return value so
|
|
55
|
+
// #backgroundRefresh is captured but not yet settled.
|
|
56
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
57
|
+
const refreshSpy = vi.spyOn(registry, "refresh").mockReturnValue(promise);
|
|
58
|
+
|
|
59
|
+
registry.refreshInBackground();
|
|
60
|
+
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
|
61
|
+
|
|
62
|
+
let settled = false;
|
|
63
|
+
const awaitPromise = registry.awaitBackgroundRefresh().then(() => {
|
|
64
|
+
settled = true;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Yield to the microtask queue: the awaiter must still be pending.
|
|
68
|
+
for (let i = 0; i < 5; i++) await Promise.resolve();
|
|
69
|
+
expect(settled).toBe(false);
|
|
70
|
+
|
|
71
|
+
resolve();
|
|
72
|
+
|
|
73
|
+
await awaitPromise;
|
|
74
|
+
expect(settled).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("resolves even when the underlying refresh rejects (refreshInBackground swallows)", async () => {
|
|
78
|
+
// refreshInBackground wraps refresh() in .catch(...) so discovery errors
|
|
79
|
+
// never reach awaitBackgroundRefresh callers. The awaiter must resolve,
|
|
80
|
+
// not propagate the rejection.
|
|
81
|
+
const { promise, reject } = Promise.withResolvers<void>();
|
|
82
|
+
vi.spyOn(registry, "refresh").mockReturnValue(promise);
|
|
83
|
+
|
|
84
|
+
registry.refreshInBackground();
|
|
85
|
+
|
|
86
|
+
let settled = false;
|
|
87
|
+
const awaitPromise = registry.awaitBackgroundRefresh().then(() => {
|
|
88
|
+
settled = true;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
reject(new Error("synthetic discovery failure"));
|
|
92
|
+
|
|
93
|
+
await awaitPromise;
|
|
94
|
+
expect(settled).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("awaiter is a no-op after the in-flight refresh settles and clears #backgroundRefresh", async () => {
|
|
98
|
+
// Once refreshInBackground's promise resolves, #backgroundRefresh is
|
|
99
|
+
// cleared in the .finally. A subsequent awaitBackgroundRefresh must be
|
|
100
|
+
// an immediate no-op (microtask), not hang waiting for a stale promise
|
|
101
|
+
// or a second refresh that was never started.
|
|
102
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
103
|
+
vi.spyOn(registry, "refresh").mockReturnValue(promise);
|
|
104
|
+
|
|
105
|
+
registry.refreshInBackground();
|
|
106
|
+
resolve();
|
|
107
|
+
await registry.awaitBackgroundRefresh();
|
|
108
|
+
|
|
109
|
+
// Now #backgroundRefresh is cleared. A fresh await must resolve in a
|
|
110
|
+
// single microtask — measure by asserting it settles before a second
|
|
111
|
+
// microtask tick.
|
|
112
|
+
let settled = false;
|
|
113
|
+
const p = registry.awaitBackgroundRefresh().then(() => {
|
|
114
|
+
settled = true;
|
|
115
|
+
});
|
|
116
|
+
await Promise.resolve();
|
|
117
|
+
expect(settled).toBe(true);
|
|
118
|
+
await p;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("refreshInBackground deduplicates: a second call while in-flight starts no new refresh", async () => {
|
|
122
|
+
// The guard `if (this.#backgroundRefresh) return` at the top of
|
|
123
|
+
// refreshInBackground prevents concurrent refreshes. A second call
|
|
124
|
+
// while the first is still pending must not invoke refresh() again.
|
|
125
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
126
|
+
const refreshSpy = vi.spyOn(registry, "refresh").mockReturnValue(promise);
|
|
127
|
+
|
|
128
|
+
registry.refreshInBackground();
|
|
129
|
+
registry.refreshInBackground();
|
|
130
|
+
registry.refreshInBackground();
|
|
131
|
+
|
|
132
|
+
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
|
133
|
+
|
|
134
|
+
resolve();
|
|
135
|
+
await registry.awaitBackgroundRefresh();
|
|
136
|
+
|
|
137
|
+
// After settle, #backgroundRefresh is cleared — a new call DOES start
|
|
138
|
+
// a fresh refresh.
|
|
139
|
+
const { promise: secondPromise, resolve: secondResolve } = Promise.withResolvers<void>();
|
|
140
|
+
refreshSpy.mockReturnValue(secondPromise);
|
|
141
|
+
registry.refreshInBackground();
|
|
142
|
+
expect(refreshSpy).toHaveBeenCalledTimes(2);
|
|
143
|
+
|
|
144
|
+
secondResolve();
|
|
145
|
+
await registry.awaitBackgroundRefresh();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -23,6 +23,7 @@ import { getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/mode
|
|
|
23
23
|
import {
|
|
24
24
|
googleAntigravityModelManagerOptions,
|
|
25
25
|
googleGeminiCliModelManagerOptions,
|
|
26
|
+
type OpenAICodexAccount,
|
|
26
27
|
openaiCodexModelManagerOptions,
|
|
27
28
|
PROVIDER_DESCRIPTORS,
|
|
28
29
|
} from "@oh-my-pi/pi-catalog/provider-models";
|
|
@@ -157,7 +158,7 @@ interface ProviderOverride {
|
|
|
157
158
|
export function mergeDiscoveredModel<TApi extends Api>(
|
|
158
159
|
model: Model<TApi>,
|
|
159
160
|
existing: Model<Api> | undefined,
|
|
160
|
-
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "remoteCompaction" | "transport">,
|
|
161
|
+
providerOverride?: Pick<ProviderOverride, "baseUrl" | "compat" | "headers" | "remoteCompaction" | "transport">,
|
|
161
162
|
): Model<TApi> {
|
|
162
163
|
if (existing) {
|
|
163
164
|
const supportsTools = model.supportsTools ?? existing.supportsTools;
|
|
@@ -171,7 +172,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
171
172
|
providerOverride?.remoteCompaction,
|
|
172
173
|
),
|
|
173
174
|
...(supportsTools !== undefined ? { supportsTools } : {}),
|
|
174
|
-
compat: model.compatConfig,
|
|
175
|
+
compat: mergeCompat(model.compatConfig, providerOverride?.compat),
|
|
175
176
|
} as ModelSpec<TApi>);
|
|
176
177
|
}
|
|
177
178
|
if (providerOverride) {
|
|
@@ -184,7 +185,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
184
185
|
model.remoteCompaction,
|
|
185
186
|
providerOverride.remoteCompaction,
|
|
186
187
|
),
|
|
187
|
-
compat: model.compatConfig,
|
|
188
|
+
compat: mergeCompat(model.compatConfig, providerOverride.compat),
|
|
188
189
|
} as ModelSpec<TApi>);
|
|
189
190
|
}
|
|
190
191
|
return model;
|
|
@@ -427,20 +428,35 @@ function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: stri
|
|
|
427
428
|
return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Resolve every configured Codex OAuth account for catalog discovery, refreshing
|
|
433
|
+
* each credential exactly once. Codex `/models` is account-scoped, so discovery
|
|
434
|
+
* must fetch per account and union the results; resolving a single access token
|
|
435
|
+
* (as before) hid models available only through a sibling account (#6265).
|
|
436
|
+
*
|
|
437
|
+
* Returns `null` when any stored account fails to resolve (e.g. a transient
|
|
438
|
+
* refresh failure): the Codex manager is authoritative, so unioning only the
|
|
439
|
+
* accounts that resolved would cache a partial catalog and hide the failed
|
|
440
|
+
* account's models for the cache TTL. Aborting keeps the previous/bundled
|
|
441
|
+
* catalog instead.
|
|
442
|
+
*/
|
|
443
|
+
async function resolveCodexDiscoveryAccounts(
|
|
431
444
|
authStorage: AuthStorage,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
if (
|
|
441
|
-
|
|
445
|
+
resolvedAccessToken: string,
|
|
446
|
+
): Promise<OpenAICodexAccount[] | null> {
|
|
447
|
+
const accesses = await authStorage.getOAuthAccesses("openai-codex");
|
|
448
|
+
const accounts: OpenAICodexAccount[] = [];
|
|
449
|
+
for (const access of accesses) {
|
|
450
|
+
if (!access.ok) return null;
|
|
451
|
+
accounts.push({ accessToken: access.accessToken, accountId: access.accountId });
|
|
452
|
+
}
|
|
453
|
+
if (!accounts.some(account => account.accessToken === resolvedAccessToken)) {
|
|
454
|
+
const matchingCredential = getOAuthCredentialsForProvider(authStorage, "openai-codex").find(
|
|
455
|
+
credential => credential.access === resolvedAccessToken,
|
|
456
|
+
);
|
|
457
|
+
accounts.push({ accessToken: resolvedAccessToken, accountId: matchingCredential?.accountId });
|
|
442
458
|
}
|
|
443
|
-
return
|
|
459
|
+
return accounts;
|
|
444
460
|
}
|
|
445
461
|
|
|
446
462
|
function mergeCompat<TBase extends object, TOverride extends object>(
|
|
@@ -853,6 +869,28 @@ export class ModelRegistry {
|
|
|
853
869
|
this.#backgroundRefresh = refreshPromise;
|
|
854
870
|
}
|
|
855
871
|
|
|
872
|
+
/**
|
|
873
|
+
* Wait for any in-flight background model discovery to settle.
|
|
874
|
+
*
|
|
875
|
+
* Background discovery started by {@link refreshInBackground} is
|
|
876
|
+
* fire-and-forget; RPC consumers (e.g. `get_available_models`,
|
|
877
|
+
* `set_model`) and deferred `--model` resolution that read the registry
|
|
878
|
+
* immediately after session creation can otherwise observe a partial
|
|
879
|
+
* catalog before discovery-backed providers have populated `#models`.
|
|
880
|
+
* Awaiting the tracked promise ensures the response reflects every
|
|
881
|
+
* configured provider once the initial background refresh resolves.
|
|
882
|
+
*
|
|
883
|
+
* No-op when no refresh is in flight (`#backgroundRefresh` cleared in the
|
|
884
|
+
* `finally` of `refreshInBackground` on completion). Resolves immediately
|
|
885
|
+
* in that case so already-warm sessions are unaffected. Discovery errors
|
|
886
|
+
* remain swallowed by `refreshInBackground`'s existing `.catch`.
|
|
887
|
+
*/
|
|
888
|
+
async awaitBackgroundRefresh(): Promise<void> {
|
|
889
|
+
if (this.#backgroundRefresh) {
|
|
890
|
+
await this.#backgroundRefresh;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
856
894
|
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
|
857
895
|
this.#reloadStaticModels();
|
|
858
896
|
for (const selector of this.#suppressedSelectors.keys()) {
|
|
@@ -1143,8 +1181,18 @@ export class ModelRegistry {
|
|
|
1143
1181
|
models.push(spec);
|
|
1144
1182
|
continue;
|
|
1145
1183
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1184
|
+
// Current unrestorable markers prove that neither same-id nor
|
|
1185
|
+
// request-model bundled headers matched the live model. Only markers
|
|
1186
|
+
// from the old id-only writer may recover through `requestModelId`.
|
|
1187
|
+
const unrestorable = unrestorableHeaderIds.has(spec.id);
|
|
1188
|
+
const bundledHeaders = (
|
|
1189
|
+
unrestorable
|
|
1190
|
+
? cache.legacyHeaderRestoreMarkers && spec.requestModelId
|
|
1191
|
+
? bundledById?.get(spec.requestModelId)
|
|
1192
|
+
: undefined
|
|
1193
|
+
: (bundledById?.get(spec.id) ??
|
|
1194
|
+
(spec.requestModelId ? bundledById?.get(spec.requestModelId) : undefined))
|
|
1195
|
+
)?.headers;
|
|
1148
1196
|
if (!bundledHeaders) continue;
|
|
1149
1197
|
models.push({ ...spec, headers: bundledHeaders });
|
|
1150
1198
|
}
|
|
@@ -1723,14 +1771,11 @@ export class ModelRegistry {
|
|
|
1723
1771
|
providerId: "openai-codex",
|
|
1724
1772
|
authoritative: true,
|
|
1725
1773
|
resolveKey: value => value,
|
|
1726
|
-
createOptions: accessToken =>
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
accessToken,
|
|
1730
|
-
accountId,
|
|
1774
|
+
createOptions: accessToken =>
|
|
1775
|
+
openaiCodexModelManagerOptions({
|
|
1776
|
+
resolveAccounts: () => resolveCodexDiscoveryAccounts(this.authStorage, accessToken),
|
|
1731
1777
|
fetch: this.#fetch,
|
|
1732
|
-
})
|
|
1733
|
-
},
|
|
1778
|
+
}),
|
|
1734
1779
|
},
|
|
1735
1780
|
];
|
|
1736
1781
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
@@ -2078,6 +2123,24 @@ export class ModelRegistry {
|
|
|
2078
2123
|
.map(provider => provider.provider);
|
|
2079
2124
|
}
|
|
2080
2125
|
|
|
2126
|
+
/**
|
|
2127
|
+
* Whether `providerId` is known to the registry: it has at least one live
|
|
2128
|
+
* model, or it is configured for dynamic discovery (models.yml `discovery:`
|
|
2129
|
+
* or a runtime extension provider) and is not disabled. Discovery-only
|
|
2130
|
+
* providers can hold zero models at startup — cached rows never persist
|
|
2131
|
+
* live auth headers (#5780), so a provider whose discovered models all
|
|
2132
|
+
* carry config headers (`authHeader: true`) only materializes models after
|
|
2133
|
+
* the online refresh completes.
|
|
2134
|
+
*/
|
|
2135
|
+
hasProvider(providerId: string): boolean {
|
|
2136
|
+
if (this.#models.some(model => model.provider === providerId)) return true;
|
|
2137
|
+
if (getDisabledProviderIdsFromSettings().has(providerId)) return false;
|
|
2138
|
+
return (
|
|
2139
|
+
this.#discoverableProviders.some(provider => provider.provider === providerId) ||
|
|
2140
|
+
this.#runtimeModelManagers.has(providerId)
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2081
2144
|
getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
|
|
2082
2145
|
return this.#providerDiscoveryStates.get(provider);
|
|
2083
2146
|
}
|