@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2
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 +3556 -3504
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +917 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +270 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/config-cli.ts +15 -3
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/backend.ts +3 -3
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/prelude.py +28 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +28 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +26 -4
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +24 -5
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/types.ts +1 -1
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +61 -34
- package/src/session/agent-session.ts +283 -39
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Tool } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit/utils";
|
|
5
5
|
import guidedGoalInterviewPrompt from "../prompts/goals/guided-goal-interview.md" with { type: "text" };
|
|
6
6
|
import guidedGoalSystemPrompt from "../prompts/goals/guided-goal-system.md" with { type: "text" };
|
|
@@ -37,6 +37,21 @@ export type GuidedGoalTurnResult =
|
|
|
37
37
|
export interface GuidedGoalTurnOptions {
|
|
38
38
|
messages: readonly GuidedGoalMessage[];
|
|
39
39
|
signal?: AbortSignal;
|
|
40
|
+
/**
|
|
41
|
+
* Stable Codex transport session id reused across every turn of one
|
|
42
|
+
* interview. `handleGuidedGoalCommand` runs up to six turns; minting a fresh
|
|
43
|
+
* id per turn opens a new websocket-only Codex socket each time (kept in
|
|
44
|
+
* `providerSessionState` until session dispose), which can trip
|
|
45
|
+
* `websocket_connection_limit_reached` and drop back to the SSE path this
|
|
46
|
+
* fix avoids. Callers pass one id for the whole interview; omitted for
|
|
47
|
+
* one-shot callers, which mint a unique id per call.
|
|
48
|
+
*/
|
|
49
|
+
sideSessionId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Mint a guided-goal Codex side-session id keyed off the main session id. */
|
|
53
|
+
export function newGuidedGoalSessionId(session: AgentSession): string {
|
|
54
|
+
return `${session.sessionId}:guided-goal:${Snowflake.next()}`;
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
function parseGuidedGoalPayload(value: unknown): GuidedGoalTurnResult {
|
|
@@ -106,6 +121,19 @@ export async function runGuidedGoalTurn(
|
|
|
106
121
|
reasoning: toReasoningEffort(thinkingLevel),
|
|
107
122
|
disableReasoning: shouldDisableReasoning(thinkingLevel),
|
|
108
123
|
toolChoice: { type: "tool", name: RESPOND_TOOL_NAME },
|
|
124
|
+
// Route through the session's provider transport so websocket-only Codex
|
|
125
|
+
// models (gpt-5.6-luna/sol/terra) get a websocket session instead of
|
|
126
|
+
// falling back to SSE — the Codex SSE /responses endpoint does not serve
|
|
127
|
+
// those ids and rejects the turn with "Model not found" (#5304, same class
|
|
128
|
+
// as the /btw regression in #5213). The side session id is minted once per
|
|
129
|
+
// interview and reused across turns so a multi-question interview shares one
|
|
130
|
+
// Codex socket instead of opening a fresh one each turn; it stays distinct
|
|
131
|
+
// from the main session id so the oneshot's append-only turn state never
|
|
132
|
+
// pollutes the main conversation.
|
|
133
|
+
sessionId: options.sideSessionId ?? newGuidedGoalSessionId(session),
|
|
134
|
+
promptCacheKey: session.sessionId,
|
|
135
|
+
preferWebsockets: session.preferWebsockets,
|
|
136
|
+
providerSessionState: session.providerSessionState,
|
|
109
137
|
},
|
|
110
138
|
{ telemetry: resolveTelemetry(session.agent.telemetry, session.sessionId), oneshotKind: "guided_goal_setup" },
|
|
111
139
|
);
|
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
* in-memory message array; parked refs (session disposed, sessionFile
|
|
6
6
|
* retained) load read-only from the JSONL session file — no writer, no lock.
|
|
7
7
|
*
|
|
8
|
+
* Agents that are no longer in the `AgentRegistry` — one-shot helpers
|
|
9
|
+
* unregistered after `finalizeSubagentLifecycle` (`keepAlive: false`, e.g. the
|
|
10
|
+
* `eval` `agent()` bridge), agents released via the Agent Hub / vibe kill, or
|
|
11
|
+
* any agent after a session resume — remain reachable: `resolve`, `complete`,
|
|
12
|
+
* and the index all fall back to scanning artifacts dirs for `<id>.jsonl`,
|
|
13
|
+
* mirroring how `agent://` reads `.md` outputs straight off disk.
|
|
14
|
+
*
|
|
8
15
|
* URL forms:
|
|
9
|
-
* - history:// - Index of all registry agents (id, status, kind, last activity)
|
|
16
|
+
* - history:// - Index of all registry + on-disk agents (id, status, kind, last activity)
|
|
10
17
|
* - history://<agentId> - Concise markdown transcript of that agent
|
|
11
18
|
*/
|
|
12
19
|
import type { AgentRef } from "../registry/agent-registry";
|
|
13
20
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
14
21
|
import { formatSessionHistoryMarkdown } from "../session/session-history-format";
|
|
15
22
|
import { loadSessionMessagesReadOnly } from "../session/session-loader";
|
|
23
|
+
import { sessionFilesFromDisk } from "./registry-helpers";
|
|
16
24
|
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
17
25
|
|
|
18
26
|
/** Humanize a last-activity timestamp as `Ns/Nm/Nh/Nd ago`. */
|
|
@@ -27,11 +35,21 @@ function formatAgo(timestamp: number): string {
|
|
|
27
35
|
return `${Math.floor(hours / 24)}d ago`;
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
/** One row of the history index — either a registered ref or a disk-only transcript. */
|
|
39
|
+
interface IndexEntry {
|
|
40
|
+
id: string;
|
|
41
|
+
status: string;
|
|
42
|
+
kind: string;
|
|
43
|
+
parent: string;
|
|
44
|
+
lastActivity: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* Handler for history:// URLs.
|
|
32
49
|
*
|
|
33
|
-
* Resolves agent ids against the global AgentRegistry,
|
|
34
|
-
* for
|
|
50
|
+
* Resolves agent ids against the global AgentRegistry, then falls back to
|
|
51
|
+
* on-disk `.jsonl` transcripts, serving read-only history for live, parked,
|
|
52
|
+
* and unregistered agents alike.
|
|
35
53
|
*/
|
|
36
54
|
export class HistoryProtocolHandler implements ProtocolHandler {
|
|
37
55
|
readonly scheme = "history";
|
|
@@ -45,7 +63,7 @@ export class HistoryProtocolHandler implements ProtocolHandler {
|
|
|
45
63
|
const visible = registry.list().filter(ref => ref.kind !== "advisor");
|
|
46
64
|
|
|
47
65
|
if (!agentId) {
|
|
48
|
-
const content = this.#renderIndex(visible);
|
|
66
|
+
const content = await this.#renderIndex(visible);
|
|
49
67
|
return {
|
|
50
68
|
url: url.href,
|
|
51
69
|
content,
|
|
@@ -61,7 +79,13 @@ export class HistoryProtocolHandler implements ProtocolHandler {
|
|
|
61
79
|
const lower = agentId.toLowerCase();
|
|
62
80
|
ref = visible.find(candidate => candidate.id.toLowerCase() === lower);
|
|
63
81
|
}
|
|
82
|
+
|
|
64
83
|
if (!ref) {
|
|
84
|
+
// Registry miss — the agent may have been unregistered or lost on resume.
|
|
85
|
+
// Serve its transcript straight from disk if the session file persists.
|
|
86
|
+
const disk = await this.#resolveFromDisk(agentId);
|
|
87
|
+
if (disk) return { ...disk, url: url.href };
|
|
88
|
+
|
|
65
89
|
const known = visible.map(candidate => candidate.id);
|
|
66
90
|
const knownStr = known.length > 0 ? known.join(", ") : "none";
|
|
67
91
|
throw new Error(`Unknown agent: ${agentId}\nKnown agents: ${knownStr}\nList all with history://`);
|
|
@@ -76,6 +100,10 @@ export class HistoryProtocolHandler implements ProtocolHandler {
|
|
|
76
100
|
messages = await loadSessionMessagesReadOnly(ref.sessionFile);
|
|
77
101
|
notes.push(`Source: session file (read-only, ${ref.status})`);
|
|
78
102
|
} else {
|
|
103
|
+
// No live session and no retained sessionFile — try the disk scan before
|
|
104
|
+
// giving up, in case the transcript lingers under an artifacts dir.
|
|
105
|
+
const disk = await this.#resolveFromDisk(ref.id);
|
|
106
|
+
if (disk) return { ...disk, url: url.href };
|
|
79
107
|
throw new Error(`Agent ${ref.id} has no transcript: session is gone and no session file was retained`);
|
|
80
108
|
}
|
|
81
109
|
|
|
@@ -90,29 +118,81 @@ export class HistoryProtocolHandler implements ProtocolHandler {
|
|
|
90
118
|
};
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Load a transcript for `agentId` from an on-disk `.jsonl` session file,
|
|
123
|
+
* matched case-insensitively. Returns `undefined` when no file is found.
|
|
124
|
+
*/
|
|
125
|
+
async #resolveFromDisk(agentId: string): Promise<InternalResource | undefined> {
|
|
126
|
+
const files = await sessionFilesFromDisk();
|
|
127
|
+
const lower = agentId.toLowerCase();
|
|
128
|
+
let matchedId: string | undefined;
|
|
129
|
+
let sessionFile: string | undefined;
|
|
130
|
+
for (const [id, file] of files) {
|
|
131
|
+
if (id === agentId || id.toLowerCase() === lower) {
|
|
132
|
+
matchedId = id;
|
|
133
|
+
sessionFile = file;
|
|
134
|
+
if (id === agentId) break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!matchedId || !sessionFile) return undefined;
|
|
138
|
+
const messages = await loadSessionMessagesReadOnly(sessionFile);
|
|
139
|
+
const content = formatSessionHistoryMarkdown(messages, { title: `${matchedId} (on disk)` });
|
|
140
|
+
return {
|
|
141
|
+
url: "",
|
|
142
|
+
content,
|
|
143
|
+
contentType: "text/markdown",
|
|
144
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
145
|
+
sourcePath: sessionFile,
|
|
146
|
+
notes: ["Source: session file (read-only, unregistered)"],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async #renderIndex(refs: AgentRef[]): Promise<string> {
|
|
151
|
+
const entries: IndexEntry[] = refs.map(ref => ({
|
|
152
|
+
id: ref.id,
|
|
153
|
+
status: ref.status,
|
|
154
|
+
kind: ref.kind,
|
|
155
|
+
parent: ref.parentId ?? "—",
|
|
156
|
+
lastActivity: formatAgo(ref.lastActivity),
|
|
157
|
+
}));
|
|
158
|
+
// Merge on-disk transcripts for agents absent from the registry.
|
|
159
|
+
const registered = new Set(refs.map(ref => ref.id));
|
|
160
|
+
const disk = await sessionFilesFromDisk();
|
|
161
|
+
for (const id of disk.keys()) {
|
|
162
|
+
if (registered.has(id)) continue;
|
|
163
|
+
entries.push({ id, status: "on disk", kind: "—", parent: "—", lastActivity: "—" });
|
|
164
|
+
}
|
|
165
|
+
|
|
94
166
|
const lines: string[] = ["# Agents", ""];
|
|
95
|
-
if (
|
|
167
|
+
if (entries.length === 0) {
|
|
96
168
|
lines.push("No agents registered.");
|
|
97
169
|
return `${lines.join("\n")}\n`;
|
|
98
170
|
}
|
|
99
171
|
lines.push("| id | status | kind | parent | last activity |", "|---|---|---|---|---|");
|
|
100
|
-
for (const
|
|
101
|
-
lines.push(
|
|
102
|
-
`| ${ref.id} | ${ref.status} | ${ref.kind} | ${ref.parentId ?? "—"} | ${formatAgo(ref.lastActivity)} |`,
|
|
103
|
-
);
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
lines.push(`| ${entry.id} | ${entry.status} | ${entry.kind} | ${entry.parent} | ${entry.lastActivity} |`);
|
|
104
174
|
}
|
|
105
175
|
lines.push("", "Read a transcript with `read history://<id>`.");
|
|
106
176
|
return `${lines.join("\n")}\n`;
|
|
107
177
|
}
|
|
108
178
|
|
|
109
179
|
async complete(): Promise<UrlCompletion[]> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
180
|
+
const completions: UrlCompletion[] = [];
|
|
181
|
+
const seen = new Set<string>();
|
|
182
|
+
for (const ref of AgentRegistry.global().list()) {
|
|
183
|
+
if (ref.kind === "advisor") continue;
|
|
184
|
+
seen.add(ref.id);
|
|
185
|
+
completions.push({
|
|
114
186
|
value: ref.id,
|
|
115
187
|
description: `${ref.status} · ${ref.kind}${ref.parentId ? ` · parent ${ref.parentId}` : ""}`,
|
|
116
|
-
})
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const disk = await sessionFilesFromDisk();
|
|
191
|
+
for (const id of disk.keys()) {
|
|
192
|
+
if (seen.has(id)) continue;
|
|
193
|
+
seen.add(id);
|
|
194
|
+
completions.push({ value: id, description: "on disk" });
|
|
195
|
+
}
|
|
196
|
+
return completions;
|
|
117
197
|
}
|
|
118
198
|
}
|
|
@@ -6,7 +6,7 @@ import { getMnemopiSessionState, type MnemopiScopedMemoryHit, type MnemopiSessio
|
|
|
6
6
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
7
7
|
import { buildDirectoryResource } from "./filesystem-resource";
|
|
8
8
|
import { validateRelativePath } from "./skill-protocol";
|
|
9
|
-
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
9
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
|
|
10
10
|
|
|
11
11
|
const DEFAULT_MEMORY_FILE = "memory_summary.md";
|
|
12
12
|
const MEMORY_NAMESPACE = "root";
|
|
@@ -28,6 +28,11 @@ export function memoryRootsFromRegistry(): string[] {
|
|
|
28
28
|
return roots;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function memoryRootsForContext(context?: ResolveContext): string[] {
|
|
32
|
+
if (context?.cwd) return [getMemoryRoot(getAgentDir(), context.cwd)];
|
|
33
|
+
return memoryRootsFromRegistry();
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
function ensureWithinRoot(targetPath: string, rootPath: string): void {
|
|
32
37
|
if (targetPath !== rootPath && !targetPath.startsWith(`${rootPath}${path.sep}`)) {
|
|
33
38
|
throw new Error("memory:// URL escapes memory root");
|
|
@@ -196,16 +201,15 @@ function renderMnemopiMemory(url: InternalUrl, hit: MnemopiScopedMemoryHit): Int
|
|
|
196
201
|
|
|
197
202
|
/**
|
|
198
203
|
* Protocol handler for memory:// URLs.
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* sharing a cwd see the same file regardless of order.
|
|
204
|
+
* Resolves file-backed roots against the calling session cwd when provided.
|
|
205
|
+
* Contextless callers fall back to the live-session registry for legacy
|
|
206
|
+
* cross-session lookups.
|
|
203
207
|
*/
|
|
204
208
|
export class MemoryProtocolHandler implements ProtocolHandler {
|
|
205
209
|
readonly scheme = "memory";
|
|
206
210
|
readonly immutable = true;
|
|
207
211
|
|
|
208
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
212
|
+
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
209
213
|
const namespace = url.rawHost || url.hostname;
|
|
210
214
|
if (!namespace) {
|
|
211
215
|
throw new Error("memory:// URL requires a namespace: memory://root or memory://<memory-id>");
|
|
@@ -230,7 +234,7 @@ export class MemoryProtocolHandler implements ProtocolHandler {
|
|
|
230
234
|
);
|
|
231
235
|
}
|
|
232
236
|
|
|
233
|
-
const roots =
|
|
237
|
+
const roots = memoryRootsForContext(context);
|
|
234
238
|
if (roots.length === 0) {
|
|
235
239
|
throw new Error(
|
|
236
240
|
"Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
|
|
@@ -259,9 +263,9 @@ export class MemoryProtocolHandler implements ProtocolHandler {
|
|
|
259
263
|
throw new Error(`Memory file not found: ${url.href}`);
|
|
260
264
|
}
|
|
261
265
|
|
|
262
|
-
async complete(): Promise<UrlCompletion[]> {
|
|
266
|
+
async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
|
|
263
267
|
const completions: UrlCompletion[] = [];
|
|
264
|
-
if (
|
|
268
|
+
if (memoryRootsForContext(context).length > 0) {
|
|
265
269
|
completions.push({ value: MEMORY_NAMESPACE, description: "Project memory summary" });
|
|
266
270
|
}
|
|
267
271
|
if (mnemopiSessionStatesFromRegistry().length > 0) {
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Shared helpers for internal-url protocol handlers that resolve IDs against
|
|
3
3
|
* registered agent sessions.
|
|
4
4
|
*/
|
|
5
|
+
|
|
6
|
+
import type { Dirent } from "node:fs";
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
10
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
6
11
|
|
|
7
12
|
const extraArtifactsDirs = new Set<string>();
|
|
@@ -35,9 +40,53 @@ export function artifactsDirsFromRegistry(): string[] {
|
|
|
35
40
|
if (!dirs.includes(dir)) dirs.push(dir);
|
|
36
41
|
};
|
|
37
42
|
for (const ref of AgentRegistry.global().list()) {
|
|
38
|
-
addDir(ref.session?.sessionManager
|
|
43
|
+
addDir(ref.session?.sessionManager?.getArtifactsDir());
|
|
39
44
|
if (ref.sessionFile) addDir(ref.sessionFile.slice(0, -6));
|
|
40
45
|
}
|
|
41
46
|
for (const dir of extraArtifactsDirs) addDir(dir);
|
|
42
47
|
return dirs;
|
|
43
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Recursively scan artifacts dirs for agent session transcripts, keyed by
|
|
52
|
+
* agent id (the `.jsonl` basename). Used by `history://` so transcripts of
|
|
53
|
+
* agents no longer in the registry (unregistered one-shot helpers, released
|
|
54
|
+
* agents, or any agent after session resume) remain reachable — mirroring how
|
|
55
|
+
* `agent://` reads `.md` outputs straight off disk.
|
|
56
|
+
*
|
|
57
|
+
* Layout follows `task/index.ts`: a subagent's transcript is
|
|
58
|
+
* `<artifactsDir>/<AgentId>.jsonl`, and its own children nest one level deeper
|
|
59
|
+
* under `<artifactsDir>/<AgentId>/<AgentId>.<ChildId>.jsonl`. Advisor
|
|
60
|
+
* transcripts (`__advisor*.jsonl`) are observability-only and excluded;
|
|
61
|
+
* EPERM-rewrite backups (`.bak`) are skipped. When the same id appears in
|
|
62
|
+
* multiple dirs, the first hit wins (registry dirs are scanned first).
|
|
63
|
+
*/
|
|
64
|
+
export async function sessionFilesFromDisk(): Promise<Map<string, string>> {
|
|
65
|
+
const found = new Map<string, string>();
|
|
66
|
+
const seenDirs = new Set<string>();
|
|
67
|
+
const scan = async (dir: string, depth: number): Promise<void> => {
|
|
68
|
+
if (depth > 8 || seenDirs.has(dir)) return;
|
|
69
|
+
seenDirs.add(dir);
|
|
70
|
+
let entries: Dirent[];
|
|
71
|
+
try {
|
|
72
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (isEnoent(err) || (err as NodeJS.ErrnoException).code === "ENOTDIR") return;
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
await scan(path.join(dir, entry.name), depth + 1);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!entry.isFile()) continue;
|
|
83
|
+
const name = entry.name;
|
|
84
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
85
|
+
if (name.startsWith("__advisor")) continue;
|
|
86
|
+
const id = name.slice(0, -".jsonl".length);
|
|
87
|
+
if (!found.has(id)) found.set(id, path.join(dir, name));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
for (const dir of artifactsDirsFromRegistry()) await scan(dir, 0);
|
|
91
|
+
return found;
|
|
92
|
+
}
|
package/src/launch/broker.ts
CHANGED
|
@@ -84,9 +84,6 @@ interface DaemonLogRead {
|
|
|
84
84
|
function quoteShellArg(value: string): string {
|
|
85
85
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
86
86
|
}
|
|
87
|
-
function quoteCmdArg(value: string): string {
|
|
88
|
-
return `"${value.replaceAll('"', '""')}"`;
|
|
89
|
-
}
|
|
90
87
|
|
|
91
88
|
function terminalState(state: DaemonSnapshot["state"]): boolean {
|
|
92
89
|
return state === "exited" || state === "failed";
|
|
@@ -434,6 +431,13 @@ class DaemonBroker {
|
|
|
434
431
|
if (spec.detached && spec.pty) {
|
|
435
432
|
throw new Error("A detached daemon cannot allocate a PTY");
|
|
436
433
|
}
|
|
434
|
+
if (
|
|
435
|
+
spec.pty &&
|
|
436
|
+
process.platform === "win32" &&
|
|
437
|
+
[".bat", ".cmd"].includes(path.extname(spec.application).toLowerCase())
|
|
438
|
+
) {
|
|
439
|
+
throw new Error('Windows batch files require application "cmd.exe" with the batch path after "/c"');
|
|
440
|
+
}
|
|
437
441
|
const existing = this.#records.get(spec.name);
|
|
438
442
|
if (existing) await this.#refreshDetached(existing);
|
|
439
443
|
if (existing && !terminalState(existing.snapshot.state)) {
|
|
@@ -520,38 +524,47 @@ class DaemonBroker {
|
|
|
520
524
|
}
|
|
521
525
|
|
|
522
526
|
async #launchPty(record: ManagedDaemon, generation: number): Promise<void> {
|
|
523
|
-
const pidPath = path.join(record.dir, "process.pid");
|
|
524
|
-
await fs.rm(pidPath, { force: true });
|
|
525
|
-
const argv = [record.spec.application, ...record.spec.args];
|
|
526
|
-
const command =
|
|
527
|
-
process.platform === "win32"
|
|
528
|
-
? argv.map(quoteCmdArg).join(" ")
|
|
529
|
-
: [`printf '%s' "$$" > ${quoteShellArg(pidPath)}`, `exec ${argv.map(quoteShellArg).join(" ")}`].join("; ");
|
|
530
527
|
const session = new PtySession();
|
|
531
528
|
record.pty = session;
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
.
|
|
529
|
+
const options = {
|
|
530
|
+
cwd: record.spec.cwd,
|
|
531
|
+
env: workerEnvFromParent({ TERM: "xterm-256color", ...record.spec.env }),
|
|
532
|
+
cols: DAEMON_PTY_COLUMNS,
|
|
533
|
+
rows: DAEMON_PTY_ROWS,
|
|
534
|
+
};
|
|
535
|
+
const onChunk = (error: Error | null, chunk: string): void => {
|
|
536
|
+
if (generation !== record.generation) return;
|
|
537
|
+
if (error) record.log?.append(`PTY output error: ${error.message}\n`);
|
|
538
|
+
if (chunk) this.#onOutput(record, generation, chunk);
|
|
539
|
+
};
|
|
540
|
+
let run: Promise<PtyRunResult>;
|
|
541
|
+
if (process.platform === "win32") {
|
|
542
|
+
run = session.startArgv(
|
|
535
543
|
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
cols: DAEMON_PTY_COLUMNS,
|
|
540
|
-
rows: DAEMON_PTY_ROWS,
|
|
541
|
-
shell,
|
|
544
|
+
application: record.spec.application,
|
|
545
|
+
args: record.spec.args,
|
|
546
|
+
...options,
|
|
542
547
|
},
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
548
|
+
onChunk,
|
|
549
|
+
);
|
|
550
|
+
} else {
|
|
551
|
+
const pidPath = path.join(record.dir, "process.pid");
|
|
552
|
+
await fs.rm(pidPath, { force: true });
|
|
553
|
+
const argv = [record.spec.application, ...record.spec.args];
|
|
554
|
+
const command = [
|
|
555
|
+
`printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
|
|
556
|
+
`exec ${argv.map(quoteShellArg).join(" ")}`,
|
|
557
|
+
].join("; ");
|
|
558
|
+
run = session.start({ command, shell: process.env.SHELL, ...options }, onChunk);
|
|
559
|
+
}
|
|
560
|
+
void run
|
|
549
561
|
.then(result => this.#onPtyExit(record, generation, result))
|
|
550
562
|
.catch(error =>
|
|
551
563
|
this.#settle(record, generation, undefined, error instanceof Error ? error.message : String(error)),
|
|
552
564
|
);
|
|
553
565
|
|
|
554
566
|
if (process.platform === "win32") return;
|
|
567
|
+
const pidPath = path.join(record.dir, "process.pid");
|
|
555
568
|
const deadline = Date.now() + 5_000;
|
|
556
569
|
const pidFile = Bun.file(pidPath);
|
|
557
570
|
while (Date.now() < deadline && generation === record.generation) {
|
package/src/lsp/client.ts
CHANGED
|
@@ -750,8 +750,14 @@ export async function getOrCreateClient(
|
|
|
750
750
|
|
|
751
751
|
client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
|
|
752
752
|
|
|
753
|
-
//
|
|
753
|
+
// Finish the initialize handshake before publishing the client as ready.
|
|
754
754
|
await sendNotification(client, "initialized", {}, signal);
|
|
755
|
+
await sendNotification(
|
|
756
|
+
client,
|
|
757
|
+
"workspace/didChangeConfiguration",
|
|
758
|
+
{ settings: config.settings ?? {} },
|
|
759
|
+
signal,
|
|
760
|
+
);
|
|
755
761
|
|
|
756
762
|
client.status = "ready";
|
|
757
763
|
// Publish only after init succeeds: pre-init clients are reachable
|
package/src/main.ts
CHANGED
|
@@ -636,6 +636,27 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
636
636
|
return undefined;
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
640
|
+
|
|
641
|
+
export function normalizeContinueSessionArgs(parsed: Args, rawArgs?: readonly string[]): void {
|
|
642
|
+
if (!parsed.continue || parsed.resume || parsed.fork) return;
|
|
643
|
+
|
|
644
|
+
let message: string | undefined;
|
|
645
|
+
if (parsed.unrecognizedFlags.length === 0 && parsed.messages.length === 1) {
|
|
646
|
+
message = parsed.messages[0]?.trim();
|
|
647
|
+
} else if (rawArgs) {
|
|
648
|
+
const continueIndex = rawArgs.findIndex(arg => arg === "--continue" || arg === "-c");
|
|
649
|
+
message = rawArgs[continueIndex + 1]?.trim();
|
|
650
|
+
}
|
|
651
|
+
if (!message || !SESSION_ID_ARG_RE.test(message)) return;
|
|
652
|
+
|
|
653
|
+
const messageIndex = parsed.messages.indexOf(message);
|
|
654
|
+
if (messageIndex === -1) return;
|
|
655
|
+
parsed.resume = message;
|
|
656
|
+
parsed.continue = false;
|
|
657
|
+
parsed.messages.splice(messageIndex, 1);
|
|
658
|
+
}
|
|
659
|
+
|
|
639
660
|
/** Resolves CLI session flags into an existing, forked, in-memory, or cancelled session manager. */
|
|
640
661
|
export async function createSessionManager(
|
|
641
662
|
parsed: Args,
|
|
@@ -665,6 +686,8 @@ export async function createSessionManager(
|
|
|
665
686
|
if (parsed.noSession) {
|
|
666
687
|
return SessionManager.inMemory();
|
|
667
688
|
}
|
|
689
|
+
normalizeContinueSessionArgs(parsed);
|
|
690
|
+
|
|
668
691
|
if (typeof parsed.resume === "string") {
|
|
669
692
|
const sessionArg = parsed.resume;
|
|
670
693
|
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
@@ -1188,6 +1211,11 @@ export async function runRootCommand(
|
|
|
1188
1211
|
);
|
|
1189
1212
|
}
|
|
1190
1213
|
|
|
1214
|
+
// Resolve an explicit `--continue <id>` before extension flags are loaded.
|
|
1215
|
+
// Reading the token immediately after `--continue` distinguishes the session
|
|
1216
|
+
// id from UUID-shaped values owned by later extension flags.
|
|
1217
|
+
normalizeContinueSessionArgs(parsedArgs, rawArgs);
|
|
1218
|
+
|
|
1191
1219
|
// Create session manager based on CLI flags. SessionResolutionError signals a
|
|
1192
1220
|
// user-facing failure (unknown --resume/--fork id, non-interactive fork
|
|
1193
1221
|
// prompt, --fork with --no-session): print + exit cleanly instead of letting
|
|
@@ -1386,6 +1414,7 @@ export async function runRootCommand(
|
|
|
1386
1414
|
},
|
|
1387
1415
|
};
|
|
1388
1416
|
const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
|
|
1417
|
+
normalizeContinueSessionArgs(initialArgs, rawArgs);
|
|
1389
1418
|
// Fail fast on stale/typo flags (e.g. `omp --list-models`) now that we
|
|
1390
1419
|
// know the real extension flag set. Without this check the unrecognized
|
|
1391
1420
|
// token gets silently consumed and any following positional leaks as the
|
|
@@ -11,10 +11,23 @@ export interface OAuthEndpoints {
|
|
|
11
11
|
authorizationUrl: string;
|
|
12
12
|
tokenUrl: string;
|
|
13
13
|
clientId?: string;
|
|
14
|
+
/** Dynamic client registration endpoint advertised by the authorization server. */
|
|
15
|
+
registrationUrl?: string;
|
|
14
16
|
scopes?: string;
|
|
15
17
|
resource?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function readRegistrationUrl(metadata: Record<string, unknown>): string | undefined {
|
|
21
|
+
const value =
|
|
22
|
+
metadata.registration_endpoint ??
|
|
23
|
+
metadata.registrationEndpoint ??
|
|
24
|
+
metadata.registration_url ??
|
|
25
|
+
metadata.registrationUrl ??
|
|
26
|
+
metadata.registration_uri ??
|
|
27
|
+
metadata.registrationUri;
|
|
28
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export interface AuthDetectionResult {
|
|
19
32
|
requiresAuth: boolean;
|
|
20
33
|
authType?: "oauth" | "apikey" | "unknown";
|
|
@@ -102,7 +115,7 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
102
115
|
(obj.resource_uri as string | undefined) ||
|
|
103
116
|
(obj.resourceUri as string | undefined);
|
|
104
117
|
|
|
105
|
-
return { authorizationUrl, tokenUrl, clientId, scopes, resource };
|
|
118
|
+
return { authorizationUrl, tokenUrl, registrationUrl: readRegistrationUrl(obj), clientId, scopes, resource };
|
|
106
119
|
};
|
|
107
120
|
|
|
108
121
|
const clientIdFromAuthUrl = (authorizationUrl: string): string | undefined => {
|
|
@@ -175,6 +188,10 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
175
188
|
return {
|
|
176
189
|
authorizationUrl,
|
|
177
190
|
tokenUrl,
|
|
191
|
+
registrationUrl:
|
|
192
|
+
challengeValues.get("registration_endpoint") ||
|
|
193
|
+
challengeValues.get("registration_url") ||
|
|
194
|
+
challengeValues.get("registration_uri"),
|
|
178
195
|
clientId: challengeValues.get("client_id") || clientIdFromAuthUrl(authorizationUrl),
|
|
179
196
|
scopes: challengeValues.get("scope") || challengeValues.get("scopes") || scopeFromAuthUrl(authorizationUrl),
|
|
180
197
|
resource,
|
|
@@ -415,6 +432,7 @@ export async function discoverOAuthEndpoints(
|
|
|
415
432
|
return {
|
|
416
433
|
authorizationUrl: String(metadata.authorization_endpoint),
|
|
417
434
|
tokenUrl: String(metadata.token_endpoint),
|
|
435
|
+
registrationUrl: readRegistrationUrl(metadata),
|
|
418
436
|
clientId:
|
|
419
437
|
typeof metadata.client_id === "string"
|
|
420
438
|
? metadata.client_id
|
|
@@ -438,6 +456,7 @@ export async function discoverOAuthEndpoints(
|
|
|
438
456
|
return {
|
|
439
457
|
authorizationUrl: oauthData.authorization_url || String(oauthData.authorizationUrl),
|
|
440
458
|
tokenUrl: oauthData.token_url || String(oauthData.tokenUrl),
|
|
459
|
+
registrationUrl: readRegistrationUrl(oauthData),
|
|
441
460
|
clientId:
|
|
442
461
|
typeof oauthData.client_id === "string"
|
|
443
462
|
? oauthData.client_id
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -277,6 +277,8 @@ export interface MCPOAuthConfig {
|
|
|
277
277
|
authorizationUrl: string;
|
|
278
278
|
/** Token endpoint URL */
|
|
279
279
|
tokenUrl: string;
|
|
280
|
+
/** Dynamic client registration endpoint advertised by the authorization server. */
|
|
281
|
+
registrationUrl?: string;
|
|
280
282
|
/** Client ID (optional when already embedded in authorization URL) */
|
|
281
283
|
clientId?: string;
|
|
282
284
|
/** Client secret (optional for PKCE flows) */
|
|
@@ -561,7 +563,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
561
563
|
* accept the later authorize request for the same scope set.
|
|
562
564
|
*/
|
|
563
565
|
async #tryRegisterClient(redirectUri: string): Promise<void> {
|
|
564
|
-
const registrationEndpoint = await this.#resolveRegistrationEndpoint();
|
|
566
|
+
const registrationEndpoint = this.config.registrationUrl ?? (await this.#resolveRegistrationEndpoint());
|
|
565
567
|
if (!registrationEndpoint) return;
|
|
566
568
|
|
|
567
569
|
try {
|