@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/cli.js +3305 -3302
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +9 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +9 -1
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +22 -12
- package/src/advisor/advise-tool.ts +7 -3
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/config/model-registry.ts +9 -0
- package/src/config/model-resolver.ts +43 -5
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +2 -1
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/export/html/template.js +2 -0
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/lib/xai-http.ts +28 -2
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +17 -2
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +6 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +19 -17
- package/src/modes/interactive-mode.ts +66 -11
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +8 -3
- package/src/modes/rpc/rpc-mode.ts +12 -3
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/sdk.ts +6 -2
- package/src/session/agent-session.ts +79 -34
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +2 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/eval.ts +3 -0
- package/src/tools/image-gen.ts +561 -512
- package/src/tools/index.ts +3 -1
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/xai.ts +40 -9
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.1",
|
|
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",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@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.
|
|
55
|
+
"@oh-my-pi/hashline": "17.0.1",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.0.1",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.0.1",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.0.1",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.0.1",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.0.1",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.0.1",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.0.1",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.0.1",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.0.1",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.0.1",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
68
68
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.220.0",
|
|
@@ -2568,18 +2568,28 @@ describe("advisor", () => {
|
|
|
2568
2568
|
}
|
|
2569
2569
|
});
|
|
2570
2570
|
|
|
2571
|
-
it("preserves a late
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2571
|
+
it("preserves a late concern when the primary already ended with a terminal answer", () => {
|
|
2572
|
+
expect(
|
|
2573
|
+
resolveAdvisorDeliveryChannel({
|
|
2574
|
+
severity: "concern",
|
|
2575
|
+
autoResumeSuppressed: false,
|
|
2576
|
+
streaming: false,
|
|
2577
|
+
aborting: false,
|
|
2578
|
+
terminalAnswerNoQueuedWork: true,
|
|
2579
|
+
}),
|
|
2580
|
+
).toBe("preserve");
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
it("steers a late blocker after a terminal answer so the primary continues and acknowledges it (#5628)", () => {
|
|
2584
|
+
expect(
|
|
2585
|
+
resolveAdvisorDeliveryChannel({
|
|
2586
|
+
severity: "blocker",
|
|
2587
|
+
autoResumeSuppressed: false,
|
|
2588
|
+
streaming: false,
|
|
2589
|
+
aborting: false,
|
|
2590
|
+
terminalAnswerNoQueuedWork: true,
|
|
2591
|
+
}),
|
|
2592
|
+
).toBe("steer");
|
|
2583
2593
|
});
|
|
2584
2594
|
|
|
2585
2595
|
it("routes interrupting notes to the aside queue during immune turns without overriding preservation", () => {
|
|
@@ -106,8 +106,11 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
106
106
|
* the live turn while one is streaming, or (when idle) a triggered turn so the
|
|
107
107
|
* advice is acted on immediately.
|
|
108
108
|
* - If the primary tail is already a terminal text answer and there is no queued
|
|
109
|
-
* work, late
|
|
110
|
-
*
|
|
109
|
+
* work, a late `concern` is preserved as a visible card instead of waking the
|
|
110
|
+
* primary to restate completion. A `blocker` is the exception: it means the
|
|
111
|
+
* agent handed off broken or unexercised work, so it still steers a triggered
|
|
112
|
+
* turn to force the primary to acknowledge and continue before the turn is
|
|
113
|
+
* considered done (#5628) — deferring it to the next user turn is the bug.
|
|
111
114
|
* - After a deliberate user interrupt (`autoResumeSuppressed`) the advisor must
|
|
112
115
|
* not auto-resume the stopped run. While the agent is idle — or still tearing
|
|
113
116
|
* the interrupted turn down (`aborting`) — the note is preserved as a visible
|
|
@@ -129,7 +132,8 @@ export function resolveAdvisorDeliveryChannel(opts: {
|
|
|
129
132
|
}): AdvisorDeliveryChannel {
|
|
130
133
|
if (!isInterruptingSeverity(opts.severity)) return "aside";
|
|
131
134
|
if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
|
|
132
|
-
if (opts.terminalAnswerNoQueuedWork && !opts.streaming && !opts.aborting)
|
|
135
|
+
if (opts.terminalAnswerNoQueuedWork && opts.severity !== "blocker" && !opts.streaming && !opts.aborting)
|
|
136
|
+
return "preserve";
|
|
133
137
|
if (opts.interruptImmuneTurnActive) return "aside";
|
|
134
138
|
return "steer";
|
|
135
139
|
}
|
package/src/cli/bench-cli.ts
CHANGED
|
@@ -159,12 +159,14 @@ function isFirstTokenEvent(event: AssistantMessageEvent): boolean {
|
|
|
159
159
|
case "text_end":
|
|
160
160
|
case "thinking_end":
|
|
161
161
|
return event.content.length > 0;
|
|
162
|
+
case "image_end":
|
|
163
|
+
return true;
|
|
162
164
|
default:
|
|
163
165
|
return false;
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
/** Final message carries visible output — non-empty text/thinking or a tool call. */
|
|
169
|
+
/** Final message carries visible output — non-empty text/thinking, an image, or a tool call. */
|
|
168
170
|
function hasVisibleFinalContent(message: AssistantMessage): boolean {
|
|
169
171
|
return message.content.some(block => {
|
|
170
172
|
switch (block.type) {
|
|
@@ -172,6 +174,7 @@ function hasVisibleFinalContent(message: AssistantMessage): boolean {
|
|
|
172
174
|
return block.text.length > 0;
|
|
173
175
|
case "thinking":
|
|
174
176
|
return block.thinking.length > 0;
|
|
177
|
+
case "image":
|
|
175
178
|
case "redactedThinking":
|
|
176
179
|
case "toolCall":
|
|
177
180
|
return true;
|
package/src/cli/grep-cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import { GrepOutputMode, grep } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import chalk from "chalk";
|
|
10
|
+
import { expandPath } from "../tools/path-utils";
|
|
10
11
|
|
|
11
12
|
export interface GrepCommandArgs {
|
|
12
13
|
pattern: string;
|
|
@@ -73,7 +74,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
|
|
|
73
74
|
process.exit(1);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const searchPath = path.resolve(cmd.path);
|
|
77
|
+
const searchPath = path.resolve(expandPath(cmd.path));
|
|
77
78
|
console.log(chalk.dim(`Searching in: ${searchPath}`));
|
|
78
79
|
console.log(chalk.dim(`Pattern: ${cmd.pattern}`));
|
|
79
80
|
console.log(
|
package/src/cli-commands.ts
CHANGED
|
@@ -46,31 +46,65 @@ export const commands: CommandEntry[] = [
|
|
|
46
46
|
{ name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
-
// Documented-looking plugin
|
|
50
|
-
// commands. Without a guard `resolveCliArgv` rewrites e.g. `omp
|
|
51
|
-
// `omp launch
|
|
52
|
-
// instead of managing plugins (#
|
|
53
|
-
//
|
|
54
|
-
// `omp plugin <action
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
// Documented-looking plugin/marketplace verbs that are NOT registered top-level
|
|
50
|
+
// commands. Without a guard `resolveCliArgv` rewrites e.g. `omp marketplace add
|
|
51
|
+
// xyz` to `omp launch marketplace add xyz`, silently forwarding the argv to the
|
|
52
|
+
// model as a prompt instead of managing plugins (#4845; same class as the
|
|
53
|
+
// `list`/`remove` leak fixed in #2935 and the `install` leak in #1496/#1498).
|
|
54
|
+
// The real commands live under `omp plugin <action>`; each entry maps a verb to
|
|
55
|
+
// a hint pointing there. See {@link reservedTopLevelWordMessage} for when a hint
|
|
56
|
+
// fires vs. when the argv still falls through to `launch`.
|
|
57
|
+
const RESERVED_TOP_LEVEL_WORDS: Record<string, string> = {
|
|
58
|
+
extensions:
|
|
59
59
|
'`omp extensions` is not a management command. Use `omp plugin list` / `omp plugin install`, or run `omp launch extensions` if you meant to send "extensions" as a prompt.',
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"list",
|
|
63
|
-
'`omp list` is not a top-level command. Use `omp plugin list` to list installed plugins, or run `omp launch list` if you meant to send "list" as a prompt.',
|
|
64
|
-
],
|
|
65
|
-
[
|
|
66
|
-
"remove",
|
|
60
|
+
list: '`omp list` is not a top-level command. Use `omp plugin list` to list installed plugins, or run `omp launch list` if you meant to send "list" as a prompt.',
|
|
61
|
+
remove:
|
|
67
62
|
'`omp remove` is not a top-level command. Use `omp plugin uninstall <name>` to remove a plugin, or run `omp launch remove` if you meant to send "remove" as a prompt.',
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
uninstall:
|
|
64
|
+
'`omp uninstall` is not a top-level command. Use `omp plugin uninstall <name@marketplace>` to remove a plugin, or run `omp launch uninstall` if you meant to send "uninstall" as a prompt.',
|
|
65
|
+
marketplace:
|
|
66
|
+
'`omp marketplace` is not a top-level command. Use `omp plugin marketplace <add|remove|update|list>` to manage marketplaces, or run `omp launch marketplace` if you meant to send "marketplace" as a prompt.',
|
|
67
|
+
discover:
|
|
68
|
+
'`omp discover` is not a top-level command. Use `omp plugin discover [marketplace]` to browse available plugins, or run `omp launch discover` if you meant to send "discover" as a prompt.',
|
|
69
|
+
upgrade:
|
|
70
|
+
'`omp upgrade` is not a top-level command. Use `omp plugin upgrade [name@marketplace]` to upgrade plugins, or run `omp launch upgrade` if you meant to send "upgrade" as a prompt.',
|
|
71
|
+
enable:
|
|
72
|
+
'`omp enable` is not a top-level command. Use `omp plugin enable <name@marketplace>` to enable a plugin, or run `omp launch enable` if you meant to send "enable" as a prompt.',
|
|
73
|
+
disable:
|
|
74
|
+
'`omp disable` is not a top-level command. Use `omp plugin disable <name@marketplace>` to disable a plugin, or run `omp launch disable` if you meant to send "disable" as a prompt.',
|
|
75
|
+
};
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
// Sub-actions that make `omp marketplace <sub>` unambiguously a management
|
|
78
|
+
// command even when multi-word (the reporter's `omp marketplace add xyz`,
|
|
79
|
+
// #4845). Mirrors the switch in `handleMarketplace` (cli/plugin-cli.ts).
|
|
80
|
+
const MARKETPLACE_SUBCOMMANDS: Record<string, true> = { add: true, remove: true, rm: true, update: true, list: true };
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hint for a reserved plugin/marketplace verb used as a top-level command, or
|
|
84
|
+
* `undefined` when the argv should fall through to `launch`.
|
|
85
|
+
*
|
|
86
|
+
* A bare verb (`omp marketplace`) always hints. A multi-word invocation only
|
|
87
|
+
* hints when the arguments follow the documented plugin grammar — a marketplace
|
|
88
|
+
* sub-action (`omp marketplace add …`) or a `name@marketplace` plugin id
|
|
89
|
+
* (`omp uninstall foo@bar`) — so genuine prompts that merely begin with one of
|
|
90
|
+
* these words (`omp list all my files`, `omp upgrade the deps`) still launch.
|
|
91
|
+
*
|
|
92
|
+
* Flags (`-…`) and `@file` arguments in the verb slot are never management
|
|
93
|
+
* commands; those fall through to the default `launch` command.
|
|
94
|
+
*/
|
|
95
|
+
export function reservedTopLevelWordMessage(argv: readonly string[]): string | undefined {
|
|
96
|
+
const first = argv[0];
|
|
97
|
+
if (!first || first.startsWith("-") || first.startsWith("@")) return undefined;
|
|
98
|
+
const hint = RESERVED_TOP_LEVEL_WORDS[first];
|
|
99
|
+
if (!hint) return undefined;
|
|
100
|
+
const second = argv[1];
|
|
101
|
+
if (second === undefined) return hint;
|
|
102
|
+
if (first === "marketplace" && MARKETPLACE_SUBCOMMANDS[second]) return hint;
|
|
103
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
104
|
+
const arg = argv[index];
|
|
105
|
+
if (!arg.startsWith("-") && arg.includes("@")) return hint;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
/**
|
|
@@ -112,7 +146,7 @@ function leadingSubcommandIndex(argv: string[]): number {
|
|
|
112
146
|
*/
|
|
113
147
|
export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
|
114
148
|
const first = argv[0];
|
|
115
|
-
const reservedMessage = reservedTopLevelWordMessage(
|
|
149
|
+
const reservedMessage = reservedTopLevelWordMessage(argv);
|
|
116
150
|
if (reservedMessage) return { error: reservedMessage };
|
|
117
151
|
if (first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help") {
|
|
118
152
|
return { argv };
|
|
@@ -1993,6 +1993,15 @@ export class ModelRegistry {
|
|
|
1993
1993
|
getProviderBaseUrl(provider: string): string | undefined {
|
|
1994
1994
|
return this.#models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
|
|
1995
1995
|
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Get provider-level headers without including per-model overrides.
|
|
1998
|
+
*/
|
|
1999
|
+
getProviderHeaders(provider: string): Record<string, string> | undefined {
|
|
2000
|
+
return createLiveConfigHeaders([
|
|
2001
|
+
this.#providerOverrides.get(provider)?.headers,
|
|
2002
|
+
this.#runtimeProviderOverrides.get(provider)?.headers,
|
|
2003
|
+
]);
|
|
2004
|
+
}
|
|
1996
2005
|
|
|
1997
2006
|
/**
|
|
1998
2007
|
* Get API key for a model.
|
|
@@ -623,11 +623,18 @@ function findExactModelReferenceMatch(modelReference: string, availableModels: M
|
|
|
623
623
|
* 4. provider-scoped fuzzy match,
|
|
624
624
|
* 5. substring match with the alias-vs-dated pick.
|
|
625
625
|
* Returns the matched model or undefined if no match found.
|
|
626
|
+
*
|
|
627
|
+
* `exactOnly` stops after the exact phases (1-3), skipping the fuzzy/substring
|
|
628
|
+
* fallbacks (4-5). Callers use it to resolve the full selector exactly before
|
|
629
|
+
* a trailing `:<level>` thinking suffix is split off, so the suffix can never
|
|
630
|
+
* be fuzzily absorbed into a longer sibling id (e.g. `kimi-for-coding:high`
|
|
631
|
+
* must not match `kimi-for-coding-highspeed`).
|
|
626
632
|
*/
|
|
627
633
|
function matchModel(
|
|
628
634
|
modelPattern: string,
|
|
629
635
|
availableModels: Model<Api>[],
|
|
630
636
|
context: ModelPreferenceContext,
|
|
637
|
+
options?: { exactOnly?: boolean },
|
|
631
638
|
): Model<Api> | undefined {
|
|
632
639
|
const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
|
|
633
640
|
if (exactRefMatch) {
|
|
@@ -663,6 +670,14 @@ function matchModel(
|
|
|
663
670
|
return pickPreferredModel(preferred.length > 0 ? preferred : aliasMatches, context);
|
|
664
671
|
}
|
|
665
672
|
}
|
|
673
|
+
|
|
674
|
+
// Exact phases exhausted. Fuzzy/substring fallbacks (below) subsequence-match
|
|
675
|
+
// the whole pattern and would let a trailing `:<level>` thinking suffix bleed
|
|
676
|
+
// into a longer sibling id; callers that still hold an unstripped suffix ask
|
|
677
|
+
// for exact-only so the suffix is split off before any fuzzy attempt.
|
|
678
|
+
if (options?.exactOnly) {
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
666
681
|
// Check for provider/modelId format — fuzzy match within provider only.
|
|
667
682
|
const slashIndex = modelPattern.indexOf("/");
|
|
668
683
|
if (slashIndex !== -1) {
|
|
@@ -766,17 +781,33 @@ function parseModelPatternWithContext(
|
|
|
766
781
|
context: ModelPreferenceContext,
|
|
767
782
|
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
768
783
|
): ParsedModelResult {
|
|
769
|
-
//
|
|
770
|
-
|
|
784
|
+
// Exact match on the full pattern first (no fuzzy): a literal id that
|
|
785
|
+
// contains a colon (`coding-router:max`) wins over any suffix split.
|
|
786
|
+
const exactMatch = matchModel(pattern, availableModels, context, { exactOnly: true });
|
|
771
787
|
if (exactMatch) {
|
|
772
788
|
return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
773
789
|
}
|
|
774
790
|
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
791
|
+
// Prefer a fuzzy match whose actual id ends in the suffix, preserving
|
|
792
|
+
// shorthand selectors for literal tier models such as `router:low`. Other
|
|
793
|
+
// fuzzy results (e.g. `kimi-for-coding-highspeed`) cannot absorb the suffix.
|
|
778
794
|
const { base, level } = splitThinkingSuffix(pattern, -1, MAX_THINKING_SUFFIX_OPTIONS);
|
|
779
795
|
if (level) {
|
|
796
|
+
const literalSuffixMatch = matchModel(pattern, availableModels, context);
|
|
797
|
+
if (literalSuffixMatch?.id.toLowerCase().endsWith(`:${level}`)) {
|
|
798
|
+
return {
|
|
799
|
+
model: literalSuffixMatch,
|
|
800
|
+
thinkingLevel: undefined,
|
|
801
|
+
warning: undefined,
|
|
802
|
+
explicitThinkingLevel: false,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Strip a valid thinking suffix and recurse before accepting any other
|
|
807
|
+
// fuzzy match, so `:<level>` cannot be absorbed into a longer sibling
|
|
808
|
+
// id (e.g. `kimi-for-coding:high` must not match
|
|
809
|
+
// `kimi-for-coding-highspeed`). `max` is accepted only after the exact
|
|
810
|
+
// match above failed, so literal model IDs ending in `:max` keep winning.
|
|
780
811
|
const result = parseModelPatternWithContext(base, availableModels, context, options);
|
|
781
812
|
if (result.model) {
|
|
782
813
|
// Only use this thinking level if no warning from inner recursion
|
|
@@ -791,6 +822,13 @@ function parseModelPatternWithContext(
|
|
|
791
822
|
return result;
|
|
792
823
|
}
|
|
793
824
|
|
|
825
|
+
// No valid thinking suffix: fall back to fuzzy/substring matching on the
|
|
826
|
+
// whole pattern.
|
|
827
|
+
const fallbackMatch = matchModel(pattern, availableModels, context);
|
|
828
|
+
if (fallbackMatch) {
|
|
829
|
+
return { model: fallbackMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
830
|
+
}
|
|
831
|
+
|
|
794
832
|
const lastColonIndex = pattern.lastIndexOf(":");
|
|
795
833
|
if (lastColonIndex === -1) {
|
|
796
834
|
// No colons, pattern simply doesn't match any model
|
|
@@ -60,6 +60,7 @@ const OpenAICompatFields = {
|
|
|
60
60
|
"strictResponsesPairing?": "boolean",
|
|
61
61
|
"supportsImageDetailOriginal?": "boolean",
|
|
62
62
|
// anthropic-messages compat flags (same `compat` slot, per-api interpretation)
|
|
63
|
+
"supportsEagerToolInputStreaming?": "boolean",
|
|
63
64
|
"requiresToolResultId?": "boolean",
|
|
64
65
|
"replayUnsignedThinking?": "boolean",
|
|
65
66
|
} as const;
|
|
@@ -4479,7 +4479,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
4479
4479
|
{
|
|
4480
4480
|
value: "auto",
|
|
4481
4481
|
label: "Auto",
|
|
4482
|
-
description:
|
|
4482
|
+
description:
|
|
4483
|
+
"Priority: active session provider > GPT model image tool > Antigravity > xAI > OpenRouter > Gemini",
|
|
4483
4484
|
},
|
|
4484
4485
|
{ value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
|
|
4485
4486
|
{
|
package/src/discovery/codex.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
SOURCE_PATHS,
|
|
38
38
|
scanSkillsFromDir,
|
|
39
39
|
} from "./helpers";
|
|
40
|
+
import { resolvePluginStdioPaths } from "./substitute-plugin-root";
|
|
40
41
|
|
|
41
42
|
const PROVIDER_ID = "codex";
|
|
42
43
|
const DISPLAY_NAME = "OpenAI Codex";
|
|
@@ -87,7 +88,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
87
88
|
|
|
88
89
|
const items: MCPServer[] = [];
|
|
89
90
|
if (userConfig) {
|
|
90
|
-
const servers = extractMCPServersFromToml(userConfig);
|
|
91
|
+
const servers = extractMCPServersFromToml(userConfig, path.dirname(userConfigPath));
|
|
91
92
|
for (const [name, config] of Object.entries(servers)) {
|
|
92
93
|
items.push({
|
|
93
94
|
name,
|
|
@@ -97,7 +98,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
if (projectConfig) {
|
|
100
|
-
const servers = extractMCPServersFromToml(projectConfig);
|
|
101
|
+
const servers = extractMCPServersFromToml(projectConfig, path.dirname(projectConfigPath));
|
|
101
102
|
for (const [name, config] of Object.entries(servers)) {
|
|
102
103
|
items.push({
|
|
103
104
|
name,
|
|
@@ -139,7 +140,10 @@ interface CodexMCPConfig {
|
|
|
139
140
|
disabled_tools?: string[];
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
function extractMCPServersFromToml(
|
|
143
|
+
function extractMCPServersFromToml(
|
|
144
|
+
toml: Record<string, unknown>,
|
|
145
|
+
configDir: string,
|
|
146
|
+
): Record<string, Partial<MCPServer>> {
|
|
143
147
|
// Check for [mcp_servers.*] sections (Codex format)
|
|
144
148
|
if (!toml.mcp_servers || typeof toml.mcp_servers !== "object") {
|
|
145
149
|
return {};
|
|
@@ -149,10 +153,16 @@ function extractMCPServersFromToml(toml: Record<string, unknown>): Record<string
|
|
|
149
153
|
const result: Record<string, Partial<MCPServer>> = {};
|
|
150
154
|
|
|
151
155
|
for (const [name, config] of Object.entries(codexServers)) {
|
|
156
|
+
// Root relative cwd/command against the Codex config directory. Codex
|
|
157
|
+
// spawns the process with the resolved cwd, so a relative command is
|
|
158
|
+
// resolved by the OS from there — pass "cwd" so e.g. cwd="server",
|
|
159
|
+
// command="./bin/mcp" resolves to <configDir>/server/bin/mcp.
|
|
160
|
+
const rooted = resolvePluginStdioPaths({ command: config.command, cwd: config.cwd }, configDir, "cwd");
|
|
152
161
|
const server: Partial<MCPServer> = {
|
|
153
|
-
command:
|
|
162
|
+
...(rooted.command !== undefined && { command: rooted.command }),
|
|
154
163
|
args: config.args,
|
|
155
164
|
url: config.url,
|
|
165
|
+
...(rooted.cwd !== undefined && { cwd: rooted.cwd }),
|
|
156
166
|
};
|
|
157
167
|
|
|
158
168
|
// Build env by merging explicit env and forwarded env_vars
|
|
@@ -31,23 +31,38 @@ export function substitutePluginRoot<T>(value: T, rootPath: string): T {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
* the directory of the `.mcp.json` that declared them.
|
|
34
|
+
* Where a relative, path-like `command` is rooted by {@link resolvePluginStdioPaths}.
|
|
36
35
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
* - `"config-dir"` (default): the directory of the config file that declared the
|
|
37
|
+
* server — the plugin package root for `.mcp.json`. A plugin can ship its
|
|
38
|
+
* executable at the package root (`command: "./bin/server"`) yet run from a
|
|
39
|
+
* data subdir (`cwd: "work"`); the command stays `<pkg>/bin/server`.
|
|
40
|
+
* - `"cwd"`: the rooted `cwd`, falling back to the config dir when no `cwd` is
|
|
41
|
+
* set. This matches how the OS resolves a relative command against the
|
|
42
|
+
* subprocess's working directory, which is the Codex `config.toml` contract:
|
|
43
|
+
* `cwd = "server"`, `command = "./bin/mcp"` → `<configDir>/server/bin/mcp`.
|
|
44
|
+
*/
|
|
45
|
+
export type StdioCommandBase = "config-dir" | "cwd";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rebase relative filesystem values in a discovered stdio server config against
|
|
49
|
+
* the directory of the config file (`.mcp.json`/`config.toml`) that declared them.
|
|
50
|
+
*
|
|
51
|
+
* External configs (bundled ChatGPT/Codex plugins, Claude marketplace plugins)
|
|
52
|
+
* express `command`/`cwd` relative to their own config file, but MCP stdio
|
|
53
|
+
* spawning roots relative values at the session cwd — so a server shipping
|
|
40
54
|
* `command: "./bin/server"`, `cwd: "."` launches from the wrong directory and
|
|
41
55
|
* fails with ENOENT. This resolves those against `configDir` instead:
|
|
42
56
|
*
|
|
43
57
|
* - relative `cwd` → resolved against `configDir`;
|
|
44
58
|
* - path-like `command` (`./`, `../`, or the Windows `.\`/`..\` forms) →
|
|
45
|
-
* resolved against `
|
|
59
|
+
* resolved against the base selected by `commandBase` (see {@link StdioCommandBase});
|
|
46
60
|
* - bare executables (`npx`, `uvx`, …) and absolute paths are left untouched.
|
|
47
61
|
*/
|
|
48
62
|
export function resolvePluginStdioPaths(
|
|
49
63
|
config: { command?: string; cwd?: string },
|
|
50
64
|
configDir: string,
|
|
65
|
+
commandBase: StdioCommandBase = "config-dir",
|
|
51
66
|
): { command?: string; cwd?: string } {
|
|
52
67
|
const resolved: { command?: string; cwd?: string } = {};
|
|
53
68
|
if (typeof config.cwd === "string") {
|
|
@@ -55,7 +70,8 @@ export function resolvePluginStdioPaths(
|
|
|
55
70
|
}
|
|
56
71
|
if (config.command !== undefined) {
|
|
57
72
|
const isPathLike = /^\.\.?[/\\]/.test(config.command);
|
|
58
|
-
|
|
73
|
+
const base = commandBase === "cwd" ? (resolved.cwd ?? configDir) : configDir;
|
|
74
|
+
resolved.command = isPathLike ? path.resolve(base, config.command) : config.command;
|
|
59
75
|
}
|
|
60
76
|
return resolved;
|
|
61
77
|
}
|
|
@@ -1082,6 +1082,8 @@
|
|
|
1082
1082
|
<div class="thinking-text">${escapeHtml(thinking)}</div>
|
|
1083
1083
|
<div class="thinking-collapsed">Thinking ...</div>
|
|
1084
1084
|
</div>`;
|
|
1085
|
+
} else if (block.type === 'image') {
|
|
1086
|
+
html += `<div class="message-images"><img src="data:${block.mimeType};base64,${block.data}" class="message-image" /></div>`;
|
|
1085
1087
|
}
|
|
1086
1088
|
}
|
|
1087
1089
|
for (const block of msg.content) {
|
|
@@ -66,3 +66,76 @@ it("agent:// resolves a depth-2 subagent's .md output while its session is live
|
|
|
66
66
|
const resource = await new AgentProtocolHandler().resolve(new URL(`agent://${grandchildId}`) as never);
|
|
67
67
|
expect(resource.content).toBe("full report content");
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it("agent:// slash form resolves a nested subagent child (hierarchy separator)", async () => {
|
|
71
|
+
const root = tempDir.path();
|
|
72
|
+
const rootSessionFile = path.join(root, "slash-session.jsonl");
|
|
73
|
+
const rootArtifactsDir = rootSessionFile.slice(0, -6);
|
|
74
|
+
await fs.mkdir(rootArtifactsDir, { recursive: true });
|
|
75
|
+
const sharedArtifactManager = new ArtifactManager(rootArtifactsDir);
|
|
76
|
+
|
|
77
|
+
// Parent subagent adopts the root ArtifactManager; its own children are
|
|
78
|
+
// written one level deeper under its sessionFile-derived dir, dot-qualified.
|
|
79
|
+
const parentSessionFile = path.join(rootArtifactsDir, "Parent.jsonl");
|
|
80
|
+
const parentOwnDir = parentSessionFile.slice(0, -6);
|
|
81
|
+
await fs.mkdir(parentOwnDir, { recursive: true });
|
|
82
|
+
await fs.writeFile(path.join(parentOwnDir, "Parent.Child.md"), "child capsule");
|
|
83
|
+
// Parent output may be in the root dir; the nested child must still win.
|
|
84
|
+
await fs.writeFile(path.join(rootArtifactsDir, "Parent.md"), JSON.stringify({ Child: "wrong base output" }));
|
|
85
|
+
|
|
86
|
+
const fakeSession = {
|
|
87
|
+
sessionManager: { getArtifactsDir: () => sharedArtifactManager.dir },
|
|
88
|
+
} as unknown as AgentSession;
|
|
89
|
+
const registry = AgentRegistry.global();
|
|
90
|
+
registry.register({
|
|
91
|
+
id: "Main",
|
|
92
|
+
displayName: "main",
|
|
93
|
+
kind: "main",
|
|
94
|
+
session: fakeSession,
|
|
95
|
+
sessionFile: rootSessionFile,
|
|
96
|
+
});
|
|
97
|
+
registry.register({
|
|
98
|
+
id: "Parent",
|
|
99
|
+
displayName: "sub",
|
|
100
|
+
kind: "sub",
|
|
101
|
+
parentId: "Main",
|
|
102
|
+
session: fakeSession,
|
|
103
|
+
sessionFile: parentSessionFile,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const handler = new AgentProtocolHandler();
|
|
107
|
+
// Slash form is a hierarchy hop, not a jq extraction.
|
|
108
|
+
const slash = await handler.resolve(new URL("agent://Parent/Child") as never);
|
|
109
|
+
expect(slash.content).toBe("child capsule");
|
|
110
|
+
expect(slash.contentType).toBe("text/markdown");
|
|
111
|
+
// The canonical dotted id resolves to the same output.
|
|
112
|
+
const dotted = await handler.resolve(new URL("agent://Parent.Child") as never);
|
|
113
|
+
expect(dotted.content).toBe("child capsule");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("agent:// path form falls back to JSON extraction when no nested output matches", async () => {
|
|
117
|
+
const root = tempDir.path();
|
|
118
|
+
const rootSessionFile = path.join(root, "json-session.jsonl");
|
|
119
|
+
const rootArtifactsDir = rootSessionFile.slice(0, -6);
|
|
120
|
+
await fs.mkdir(rootArtifactsDir, { recursive: true });
|
|
121
|
+
const sharedArtifactManager = new ArtifactManager(rootArtifactsDir);
|
|
122
|
+
await fs.writeFile(path.join(rootArtifactsDir, "Worker.md"), JSON.stringify({ result: { ok: true } }));
|
|
123
|
+
|
|
124
|
+
const fakeSession = {
|
|
125
|
+
sessionManager: { getArtifactsDir: () => sharedArtifactManager.dir },
|
|
126
|
+
} as unknown as AgentSession;
|
|
127
|
+
const registry = AgentRegistry.global();
|
|
128
|
+
registry.register({
|
|
129
|
+
id: "Main",
|
|
130
|
+
displayName: "main",
|
|
131
|
+
kind: "main",
|
|
132
|
+
session: fakeSession,
|
|
133
|
+
sessionFile: rootSessionFile,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const handler = new AgentProtocolHandler();
|
|
137
|
+
// `result` names no nested output, so the path extracts JSON from Worker.md.
|
|
138
|
+
const extracted = await handler.resolve(new URL("agent://Worker/result") as never);
|
|
139
|
+
expect(extracted.contentType).toBe("application/json");
|
|
140
|
+
expect(JSON.parse(extracted.content)).toEqual({ ok: true });
|
|
141
|
+
});
|