@oh-my-pi/pi-coding-agent 15.11.0 → 15.11.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 +57 -2
- package/dist/cli.js +678 -657
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +49 -4
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
- package/dist/types/extensibility/custom-tools/loader.d.ts +2 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -4
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +8 -4
- package/dist/types/irc/bus.d.ts +15 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +30 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +17 -3
- package/dist/types/slash-commands/available-commands.d.ts +34 -0
- package/dist/types/task/index.d.ts +3 -3
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +4 -4
- package/dist/types/tools/browser/registry.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +3 -2
- package/dist/types/tools/path-utils.d.ts +0 -4
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/package.json +11 -11
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +5 -4
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +55 -4
- package/src/edit/renderer.ts +96 -46
- package/src/exec/bash-executor.ts +21 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -1
- package/src/extensibility/custom-commands/loader.ts +3 -1
- package/src/extensibility/custom-commands/types.ts +6 -3
- package/src/extensibility/custom-tools/loader.ts +4 -7
- package/src/extensibility/custom-tools/types.ts +8 -4
- package/src/extensibility/extensions/loader.ts +2 -1
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/irc/bus.ts +14 -3
- package/src/lsp/defaults.json +6 -0
- package/src/lsp/render.ts +2 -28
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/memories/index.ts +2 -0
- package/src/modes/acp/acp-agent.ts +4 -67
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/plan-review-overlay.ts +32 -3
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/controllers/event-controller.ts +28 -4
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +4 -0
- package/src/modes/controllers/streaming-reveal.ts +16 -8
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +41 -2
- package/src/modes/rpc/rpc-client.ts +32 -0
- package/src/modes/rpc/rpc-mode.ts +82 -7
- package/src/modes/rpc/rpc-types.ts +23 -0
- package/src/modes/theme/theme.ts +13 -7
- package/src/modes/utils/ui-helpers.ts +13 -4
- package/src/prompts/memories/consolidation_system.md +4 -0
- package/src/prompts/system/irc-autoreply.md +6 -0
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/task.md +7 -2
- package/src/session/agent-session.ts +120 -10
- package/src/slash-commands/available-commands.ts +105 -0
- package/src/task/index.ts +15 -10
- package/src/task/render.ts +10 -4
- package/src/tools/bash.ts +5 -1
- package/src/tools/browser/attach.ts +26 -7
- package/src/tools/browser/registry.ts +11 -1
- package/src/tools/irc.ts +16 -4
- package/src/tools/job.ts +7 -3
- package/src/tools/path-utils.ts +22 -15
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
package/src/irc/bus.ts
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* AgentLifecycleManager, idle agents are woken with a real turn, and busy
|
|
8
8
|
* agents receive the message as a non-interrupting aside at the next step
|
|
9
9
|
* boundary (see AgentSession.deliverIrcMessage). Replies are real turns by
|
|
10
|
-
* the recipient, observed via `wait
|
|
10
|
+
* the recipient, observed via `wait` — with one exception: when the sender
|
|
11
|
+
* awaits a reply and the recipient is mid-turn with async execution
|
|
12
|
+
* disabled, the recipient session generates an ephemeral side-channel
|
|
13
|
+
* auto-reply (it may be blocked in a synchronous task spawn whose batch
|
|
14
|
+
* includes the sender, so a real turn could never happen in time).
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
@@ -80,8 +84,15 @@ export class IrcBus {
|
|
|
80
84
|
* context, so buffering it too would double-deliver via a later
|
|
81
85
|
* `wait`/`inbox` and inflate unread counts. Only a failed live hand-off
|
|
82
86
|
* is buffered for the recipient to drain later.
|
|
87
|
+
*
|
|
88
|
+
* `opts.expectsReply` marks sends whose caller is blocked on an answer
|
|
89
|
+
* (`send await:true`). It is forwarded to the recipient session so a
|
|
90
|
+
* mid-turn recipient that cannot reach a step boundary (async execution
|
|
91
|
+
* disabled — e.g. blocked in a synchronous task spawn awaiting the
|
|
92
|
+
* sender's own batch) can generate an ephemeral side-channel auto-reply
|
|
93
|
+
* instead of stranding the sender until timeout.
|
|
83
94
|
*/
|
|
84
|
-
async send(msg: Omit<IrcMessage, "id" | "ts"
|
|
95
|
+
async send(msg: Omit<IrcMessage, "id" | "ts">, opts?: { expectsReply?: boolean }): Promise<IrcDeliveryReceipt> {
|
|
85
96
|
const message: IrcMessage = { ...msg, id: Snowflake.next(), ts: Date.now() };
|
|
86
97
|
const ref = this.#registry.get(message.to);
|
|
87
98
|
if (!ref || ref.status === "aborted") {
|
|
@@ -118,7 +129,7 @@ export class IrcBus {
|
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
try {
|
|
121
|
-
const delivery = await session.deliverIrcMessage(message);
|
|
132
|
+
const delivery = await session.deliverIrcMessage(message, opts);
|
|
122
133
|
this.#relayToMainUi(message);
|
|
123
134
|
return { to: message.to, outcome: revived ? "revived" : delivery };
|
|
124
135
|
} catch (error) {
|
package/src/lsp/defaults.json
CHANGED
|
@@ -248,6 +248,12 @@
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
},
|
|
251
|
+
"expert": {
|
|
252
|
+
"command": "expert",
|
|
253
|
+
"args": ["--stdio"],
|
|
254
|
+
"fileTypes": [".ex", ".exs", ".heex", ".eex"],
|
|
255
|
+
"rootMarkers": ["mix.exs", "mix.lock"]
|
|
256
|
+
},
|
|
251
257
|
"erlangls": {
|
|
252
258
|
"command": "erlang_ls",
|
|
253
259
|
"args": [],
|
package/src/lsp/render.ts
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
* - Collapsible/expandable views
|
|
9
9
|
*/
|
|
10
10
|
import type { RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
|
11
|
-
import { type HighlightColors, highlightCode as nativeHighlightCode, supportsLanguage } from "@oh-my-pi/pi-natives";
|
|
12
11
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
13
|
-
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
12
|
+
import { getLanguageFromPath, highlightCode as highlightThemeCode, type Theme } from "../modes/theme/theme";
|
|
14
13
|
import {
|
|
15
14
|
formatExpandHint,
|
|
16
15
|
formatMoreItems,
|
|
@@ -219,7 +218,7 @@ function renderHover(
|
|
|
219
218
|
const beforeCode = fullText.slice(0, codeStart).trimEnd();
|
|
220
219
|
const afterCode = fullText.slice(fullText.indexOf("```", 3) + 3).trim();
|
|
221
220
|
|
|
222
|
-
const codeLines =
|
|
221
|
+
const codeLines = highlightThemeCode(code, lang, theme);
|
|
223
222
|
const icon = theme.styledSymbol("status.info", "accent");
|
|
224
223
|
const langLabel = lang ? theme.fg("mdCodeBlockBorder", ` ${lang}`) : "";
|
|
225
224
|
|
|
@@ -274,31 +273,6 @@ function renderHover(
|
|
|
274
273
|
return output.split("\n");
|
|
275
274
|
}
|
|
276
275
|
|
|
277
|
-
/**
|
|
278
|
-
* Syntax highlight code using native highlighter.
|
|
279
|
-
*/
|
|
280
|
-
function highlightCode(codeText: string, language: string, theme: Theme): string[] {
|
|
281
|
-
const validLang = language && supportsLanguage(language) ? language : undefined;
|
|
282
|
-
try {
|
|
283
|
-
const colors: HighlightColors = {
|
|
284
|
-
comment: theme.getFgAnsi("syntaxComment"),
|
|
285
|
-
keyword: theme.getFgAnsi("syntaxKeyword"),
|
|
286
|
-
function: theme.getFgAnsi("syntaxFunction"),
|
|
287
|
-
variable: theme.getFgAnsi("syntaxVariable"),
|
|
288
|
-
string: theme.getFgAnsi("syntaxString"),
|
|
289
|
-
number: theme.getFgAnsi("syntaxNumber"),
|
|
290
|
-
type: theme.getFgAnsi("syntaxType"),
|
|
291
|
-
operator: theme.getFgAnsi("syntaxOperator"),
|
|
292
|
-
punctuation: theme.getFgAnsi("syntaxPunctuation"),
|
|
293
|
-
inserted: theme.getFgAnsi("toolDiffAdded"),
|
|
294
|
-
deleted: theme.getFgAnsi("toolDiffRemoved"),
|
|
295
|
-
};
|
|
296
|
-
return nativeHighlightCode(codeText, validLang, colors).split("\n");
|
|
297
|
-
} catch {
|
|
298
|
-
return codeText.split("\n");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
276
|
// =============================================================================
|
|
303
277
|
// Diagnostics Rendering
|
|
304
278
|
// =============================================================================
|
package/src/mcp/manager.ts
CHANGED
|
@@ -1174,12 +1174,15 @@ export class MCPManager {
|
|
|
1174
1174
|
const shouldRefresh =
|
|
1175
1175
|
forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
|
|
1176
1176
|
if (shouldRefresh && credential.refresh && auth.tokenUrl) {
|
|
1177
|
+
const resource =
|
|
1178
|
+
auth.resource ?? (config.type === "http" || config.type === "sse" ? config.url : undefined);
|
|
1177
1179
|
try {
|
|
1178
1180
|
const refreshed = await refreshMCPOAuthToken(
|
|
1179
1181
|
auth.tokenUrl,
|
|
1180
1182
|
credential.refresh,
|
|
1181
1183
|
auth.clientId,
|
|
1182
1184
|
auth.clientSecret,
|
|
1185
|
+
resource,
|
|
1183
1186
|
);
|
|
1184
1187
|
const refreshedCredential = { type: "oauth" as const, ...refreshed };
|
|
1185
1188
|
await this.#authStorage.set(credentialId, refreshedCredential);
|
|
@@ -11,6 +11,7 @@ export interface OAuthEndpoints {
|
|
|
11
11
|
tokenUrl: string;
|
|
12
12
|
clientId?: string;
|
|
13
13
|
scopes?: string;
|
|
14
|
+
resource?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface AuthDetectionResult {
|
|
@@ -94,7 +95,12 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
94
95
|
(obj.default_client_id as string | undefined) ||
|
|
95
96
|
(obj.public_client_id as string | undefined);
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
const resource =
|
|
99
|
+
(obj.resource as string | undefined) ||
|
|
100
|
+
(obj.resource_uri as string | undefined) ||
|
|
101
|
+
(obj.resourceUri as string | undefined);
|
|
102
|
+
|
|
103
|
+
return { authorizationUrl, tokenUrl, clientId, scopes, resource };
|
|
98
104
|
};
|
|
99
105
|
|
|
100
106
|
const clientIdFromAuthUrl = (authorizationUrl: string): string | undefined => {
|
|
@@ -161,6 +167,7 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
161
167
|
challengeValues.get("realm");
|
|
162
168
|
const tokenUrl =
|
|
163
169
|
challengeValues.get("token_url") || challengeValues.get("token_uri") || challengeValues.get("token_endpoint");
|
|
170
|
+
const resource = challengeValues.get("resource") || challengeValues.get("resource_uri");
|
|
164
171
|
|
|
165
172
|
if (authorizationUrl && tokenUrl) {
|
|
166
173
|
return {
|
|
@@ -168,6 +175,7 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
168
175
|
tokenUrl,
|
|
169
176
|
clientId: challengeValues.get("client_id") || clientIdFromAuthUrl(authorizationUrl),
|
|
170
177
|
scopes: challengeValues.get("scope") || challengeValues.get("scopes") || scopeFromAuthUrl(authorizationUrl),
|
|
178
|
+
resource,
|
|
171
179
|
};
|
|
172
180
|
}
|
|
173
181
|
}
|
|
@@ -250,7 +258,7 @@ export async function discoverOAuthEndpoints(
|
|
|
250
258
|
serverUrl: string,
|
|
251
259
|
authServerUrl?: string,
|
|
252
260
|
resourceMetadataUrl?: string,
|
|
253
|
-
opts?: { fetch?: FetchImpl },
|
|
261
|
+
opts?: { fetch?: FetchImpl; protectedResource?: string },
|
|
254
262
|
): Promise<OAuthEndpoints | null> {
|
|
255
263
|
const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
|
|
256
264
|
const wellKnownPaths = [
|
|
@@ -264,6 +272,8 @@ export async function discoverOAuthEndpoints(
|
|
|
264
272
|
const urlsToQuery: string[] = [];
|
|
265
273
|
const visitedAuthServers = new Set<string>();
|
|
266
274
|
|
|
275
|
+
let protectedResource = opts?.protectedResource;
|
|
276
|
+
|
|
267
277
|
// Step 1: If a resource_metadata URL was provided, fetch it to discover auth servers.
|
|
268
278
|
// This follows the RFC 9728 chain: resource_metadata → authorization_servers.
|
|
269
279
|
if (resourceMetadataUrl && !visitedAuthServers.has(resourceMetadataUrl)) {
|
|
@@ -276,6 +286,9 @@ export async function discoverOAuthEndpoints(
|
|
|
276
286
|
});
|
|
277
287
|
if (metaResp.ok) {
|
|
278
288
|
const meta = (await metaResp.json()) as Record<string, unknown>;
|
|
289
|
+
if (typeof meta.resource === "string" && meta.resource.trim() !== "") {
|
|
290
|
+
protectedResource = meta.resource;
|
|
291
|
+
}
|
|
279
292
|
const authServers = Array.isArray(meta.authorization_servers)
|
|
280
293
|
? meta.authorization_servers.filter((entry): entry is string => typeof entry === "string")
|
|
281
294
|
: [];
|
|
@@ -304,6 +317,8 @@ export async function discoverOAuthEndpoints(
|
|
|
304
317
|
const scopesSupported = Array.isArray(metadata.scopes_supported)
|
|
305
318
|
? metadata.scopes_supported.filter((scope): scope is string => typeof scope === "string").join(" ")
|
|
306
319
|
: undefined;
|
|
320
|
+
const resource = typeof metadata.resource === "string" ? metadata.resource : protectedResource;
|
|
321
|
+
|
|
307
322
|
return {
|
|
308
323
|
authorizationUrl: String(metadata.authorization_endpoint),
|
|
309
324
|
tokenUrl: String(metadata.token_endpoint),
|
|
@@ -324,12 +339,15 @@ export async function discoverOAuthEndpoints(
|
|
|
324
339
|
: typeof metadata.scope === "string"
|
|
325
340
|
? metadata.scope
|
|
326
341
|
: undefined),
|
|
342
|
+
resource,
|
|
327
343
|
};
|
|
328
344
|
}
|
|
329
345
|
|
|
330
346
|
if (metadata.oauth || metadata.authorization || metadata.auth) {
|
|
331
347
|
const oauthData = (metadata.oauth || metadata.authorization || metadata.auth) as Record<string, unknown>;
|
|
332
348
|
if (typeof oauthData.authorization_url === "string" && typeof oauthData.token_url === "string") {
|
|
349
|
+
const resource = typeof oauthData.resource === "string" ? oauthData.resource : protectedResource;
|
|
350
|
+
|
|
333
351
|
return {
|
|
334
352
|
authorizationUrl: oauthData.authorization_url || String(oauthData.authorizationUrl),
|
|
335
353
|
tokenUrl: oauthData.token_url || String(oauthData.tokenUrl),
|
|
@@ -349,6 +367,7 @@ export async function discoverOAuthEndpoints(
|
|
|
349
367
|
: typeof oauthData.scope === "string"
|
|
350
368
|
? oauthData.scope
|
|
351
369
|
: undefined,
|
|
370
|
+
resource,
|
|
352
371
|
};
|
|
353
372
|
}
|
|
354
373
|
}
|
|
@@ -378,12 +397,18 @@ export async function discoverOAuthEndpoints(
|
|
|
378
397
|
? metadata.authorization_servers.filter((entry): entry is string => typeof entry === "string")
|
|
379
398
|
: [];
|
|
380
399
|
|
|
400
|
+
const discoveredProtectedResource =
|
|
401
|
+
typeof metadata.resource === "string" && metadata.resource.trim() !== ""
|
|
402
|
+
? metadata.resource
|
|
403
|
+
: protectedResource;
|
|
404
|
+
|
|
381
405
|
for (const discoveredAuthServer of authServers) {
|
|
382
406
|
if (visitedAuthServers.has(discoveredAuthServer)) {
|
|
383
407
|
continue;
|
|
384
408
|
}
|
|
385
409
|
const discovered = await discoverOAuthEndpoints(serverUrl, discoveredAuthServer, undefined, {
|
|
386
410
|
fetch: fetchImpl,
|
|
411
|
+
protectedResource: discoveredProtectedResource,
|
|
387
412
|
});
|
|
388
413
|
if (discovered) return discovered;
|
|
389
414
|
}
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -98,6 +98,23 @@ function resolveCallbackOptions(config: MCPOAuthConfig): OAuthCallbackFlowOption
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function resolveResourceUri(resource: string | undefined): string | undefined {
|
|
102
|
+
const trimmed = resource?.trim();
|
|
103
|
+
if (!trimmed) return undefined;
|
|
104
|
+
if (trimmed !== resource) {
|
|
105
|
+
throw new Error("OAuth resource URI must not include surrounding whitespace");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parsed = new URL(trimmed);
|
|
109
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
110
|
+
throw new Error("OAuth resource URI must use http or https");
|
|
111
|
+
}
|
|
112
|
+
if (parsed.hash) {
|
|
113
|
+
throw new Error("OAuth resource URI must not include a fragment");
|
|
114
|
+
}
|
|
115
|
+
return trimmed;
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
export interface MCPOAuthConfig {
|
|
102
119
|
/** Authorization endpoint URL */
|
|
103
120
|
authorizationUrl: string;
|
|
@@ -115,6 +132,8 @@ export interface MCPOAuthConfig {
|
|
|
115
132
|
callbackPort?: number;
|
|
116
133
|
/** Custom callback path (default: /callback or redirectUri pathname) */
|
|
117
134
|
callbackPath?: string;
|
|
135
|
+
/** MCP resource URI for RFC 8707 resource indicators */
|
|
136
|
+
resource?: string;
|
|
118
137
|
/** Fetch implementation for token exchange and discovery requests. */
|
|
119
138
|
fetch?: FetchImpl;
|
|
120
139
|
}
|
|
@@ -128,6 +147,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
128
147
|
#registeredClientSecret?: string;
|
|
129
148
|
#codeVerifier?: string;
|
|
130
149
|
#fetch: FetchImpl;
|
|
150
|
+
#resource?: string;
|
|
131
151
|
|
|
132
152
|
constructor(
|
|
133
153
|
private config: MCPOAuthConfig,
|
|
@@ -136,6 +156,9 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
136
156
|
super(ctrl, resolveCallbackOptions(config));
|
|
137
157
|
this.#resolvedClientId = this.#resolveClientId(config);
|
|
138
158
|
this.#fetch = config.fetch ?? ctrl.fetch ?? fetch;
|
|
159
|
+
this.#resource = resolveResourceUri(
|
|
160
|
+
config.resource ?? this.#resourceFromAuthorizationUrl(config.authorizationUrl),
|
|
161
|
+
);
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
/**
|
|
@@ -157,6 +180,9 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
157
180
|
get registeredClientSecret(): string | undefined {
|
|
158
181
|
return this.#registeredClientSecret;
|
|
159
182
|
}
|
|
183
|
+
get resource(): string | undefined {
|
|
184
|
+
return this.#resource;
|
|
185
|
+
}
|
|
160
186
|
|
|
161
187
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
162
188
|
if (!this.#resolvedClientId) {
|
|
@@ -176,6 +202,12 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
176
202
|
if (this.config.scopes && !params.get("scope")) {
|
|
177
203
|
params.set("scope", this.config.scopes);
|
|
178
204
|
}
|
|
205
|
+
const existingResource = params.get("resource")?.trim();
|
|
206
|
+
if (existingResource) {
|
|
207
|
+
this.#resource = resolveResourceUri(existingResource);
|
|
208
|
+
} else if (this.#resource) {
|
|
209
|
+
params.set("resource", this.#resource);
|
|
210
|
+
}
|
|
179
211
|
params.set("redirect_uri", redirectUri);
|
|
180
212
|
params.set("state", state);
|
|
181
213
|
|
|
@@ -212,6 +244,9 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
212
244
|
this.#codeVerifier = undefined;
|
|
213
245
|
|
|
214
246
|
// Add client secret if provided
|
|
247
|
+
if (this.#resource) {
|
|
248
|
+
params.set("resource", this.#resource);
|
|
249
|
+
}
|
|
215
250
|
const clientSecret = this.config.clientSecret ?? this.#registeredClientSecret;
|
|
216
251
|
if (clientSecret) {
|
|
217
252
|
params.set("client_secret", clientSecret);
|
|
@@ -285,6 +320,13 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
285
320
|
return undefined;
|
|
286
321
|
}
|
|
287
322
|
}
|
|
323
|
+
#resourceFromAuthorizationUrl(authorizationUrl: string): string | undefined {
|
|
324
|
+
try {
|
|
325
|
+
return new URL(authorizationUrl).searchParams.get("resource") ?? undefined;
|
|
326
|
+
} catch {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
288
330
|
|
|
289
331
|
/**
|
|
290
332
|
* Try OAuth dynamic client registration when provider requires a client_id.
|
|
@@ -407,14 +449,18 @@ export async function refreshMCPOAuthToken(
|
|
|
407
449
|
refreshToken: string,
|
|
408
450
|
clientId?: string,
|
|
409
451
|
clientSecret?: string,
|
|
452
|
+
resourceOrOpts?: string | { fetch?: FetchImpl },
|
|
410
453
|
opts?: { fetch?: FetchImpl },
|
|
411
454
|
): Promise<OAuthCredentials> {
|
|
412
|
-
const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
|
|
455
|
+
const fetchImpl: FetchImpl = (typeof resourceOrOpts === "string" ? opts?.fetch : resourceOrOpts?.fetch) ?? fetch;
|
|
456
|
+
const resource = typeof resourceOrOpts === "string" ? resourceOrOpts : undefined;
|
|
413
457
|
const params = new URLSearchParams({
|
|
414
458
|
grant_type: "refresh_token",
|
|
415
459
|
refresh_token: refreshToken,
|
|
416
460
|
});
|
|
417
461
|
if (clientId) params.set("client_id", clientId);
|
|
462
|
+
const resolvedResource = resolveResourceUri(resource);
|
|
463
|
+
if (resolvedResource) params.set("resource", resolvedResource);
|
|
418
464
|
if (clientSecret) params.set("client_secret", clientSecret);
|
|
419
465
|
|
|
420
466
|
const response = await fetchImpl(tokenUrl, {
|
|
@@ -25,6 +25,7 @@ import { isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
|
25
25
|
/** Subprocess argv for launching an MCP stdio server. */
|
|
26
26
|
export interface StdioSpawnCommand {
|
|
27
27
|
cmd: string[];
|
|
28
|
+
windowsHide?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/** Inputs used to resolve platform-specific stdio spawn behavior. */
|
|
@@ -153,6 +154,7 @@ export async function resolveStdioSpawnCommand(
|
|
|
153
154
|
|
|
154
155
|
return {
|
|
155
156
|
cmd: [resolveComSpec(options.env), "/d", "/s", "/c", buildCmdExeCommand(resolvedCommand, args)],
|
|
157
|
+
windowsHide: true,
|
|
156
158
|
};
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -255,6 +257,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
255
257
|
stdin: "pipe",
|
|
256
258
|
stdout: "pipe",
|
|
257
259
|
stderr: "pipe",
|
|
260
|
+
windowsHide: spawnCommand.windowsHide,
|
|
258
261
|
});
|
|
259
262
|
|
|
260
263
|
this.#connected = true;
|
package/src/mcp/types.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface MCPAuthConfig {
|
|
|
55
55
|
clientId?: string;
|
|
56
56
|
/** Client secret — persisted for token refresh */
|
|
57
57
|
clientSecret?: string;
|
|
58
|
+
/** MCP resource URI — persisted for OAuth resource indicators during refresh */
|
|
59
|
+
resource?: string;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** Base server config with shared options */
|
package/src/memories/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ModelRegistry } from "../config/model-registry";
|
|
|
11
11
|
import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
|
|
12
12
|
import type { Settings } from "../config/settings";
|
|
13
13
|
import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
|
|
14
|
+
import consolidationSystemTemplate from "../prompts/memories/consolidation_system.md" with { type: "text" };
|
|
14
15
|
import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
|
|
15
16
|
import stageOneInputTemplate from "../prompts/memories/stage_one_input.md" with { type: "text" };
|
|
16
17
|
import stageOneSystemTemplate from "../prompts/memories/stage_one_system.md" with { type: "text" };
|
|
@@ -752,6 +753,7 @@ async function runConsolidationModel(options: {
|
|
|
752
753
|
const response = await completeSimple(
|
|
753
754
|
model,
|
|
754
755
|
{
|
|
756
|
+
systemPrompt: [consolidationSystemTemplate],
|
|
755
757
|
messages: [{ role: "user", content: [{ type: "text", text: input }], timestamp: Date.now() }],
|
|
756
758
|
},
|
|
757
759
|
{
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
} from "../../extensibility/extensions";
|
|
57
57
|
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
|
58
58
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
59
|
-
import { buildSkillPromptMessage
|
|
59
|
+
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
|
60
60
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
61
61
|
import { resolveLocalUrlToPath } from "../../internal-urls";
|
|
62
62
|
import { MCPManager } from "../../mcp/manager";
|
|
@@ -71,12 +71,8 @@ import {
|
|
|
71
71
|
type SessionInfo as StoredSessionInfo,
|
|
72
72
|
type UsageStatistics,
|
|
73
73
|
} from "../../session/session-manager";
|
|
74
|
-
import {
|
|
75
|
-
|
|
76
|
-
ACP_BUILTIN_SLASH_COMMANDS,
|
|
77
|
-
executeAcpBuiltinSlashCommand,
|
|
78
|
-
isAcpBuiltinShadowedName,
|
|
79
|
-
} from "../../slash-commands/acp-builtins";
|
|
74
|
+
import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
75
|
+
import { buildAvailableSlashCommands, toAcpAvailableCommands } from "../../slash-commands/available-commands";
|
|
80
76
|
import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
|
|
81
77
|
import { normalizeLocalScheme } from "../../tools/path-utils";
|
|
82
78
|
import { runResolveInvocation } from "../../tools/resolve";
|
|
@@ -1662,66 +1658,7 @@ export class AcpAgent implements Agent {
|
|
|
1662
1658
|
}
|
|
1663
1659
|
|
|
1664
1660
|
async #buildAvailableCommands(session: AgentSession): Promise<AvailableCommand[]> {
|
|
1665
|
-
|
|
1666
|
-
const seenNames = new Set<string>();
|
|
1667
|
-
const appendCommand = (command: AvailableCommand): void => {
|
|
1668
|
-
if (seenNames.has(command.name)) {
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
seenNames.add(command.name);
|
|
1672
|
-
commands.push(command);
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
// Advertise in the order dispatch resolves them (mirrors AgentSession
|
|
1676
|
-
// dispatch: builtins → skills → extensions → custom TS → file-based).
|
|
1677
|
-
// `appendCommand` dedupes by name so earlier entries win; extension
|
|
1678
|
-
// commands therefore correctly shadow custom TS commands of the same
|
|
1679
|
-
// name, matching the runtime behaviour of #tryExecuteExtensionCommand
|
|
1680
|
-
// running before #tryExecuteCustomCommand.
|
|
1681
|
-
for (const command of ACP_BUILTIN_SLASH_COMMANDS) {
|
|
1682
|
-
appendCommand(command);
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
if (session.skillsSettings?.enableSkillCommands) {
|
|
1686
|
-
for (const skill of session.skills) {
|
|
1687
|
-
appendCommand({
|
|
1688
|
-
name: getSkillSlashCommandName(skill),
|
|
1689
|
-
description: skill.description || `Run ${skill.name} skill`,
|
|
1690
|
-
input: { hint: "arguments" },
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
for (const command of session.extensionRunner?.getRegisteredCommands(ACP_BUILTIN_RESERVED_NAMES) ?? []) {
|
|
1696
|
-
// Reserved-set filtering in getRegisteredCommands only covers exact
|
|
1697
|
-
// names; colon-namespaced names whose prefix is a builtin (e.g.
|
|
1698
|
-
// `model:foo`) would still dispatch to the builtin in ACP.
|
|
1699
|
-
if (isAcpBuiltinShadowedName(command.name)) {
|
|
1700
|
-
continue;
|
|
1701
|
-
}
|
|
1702
|
-
appendCommand({
|
|
1703
|
-
name: command.name,
|
|
1704
|
-
description: command.description ?? "(extension command)",
|
|
1705
|
-
input: { hint: "arguments" },
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
for (const command of session.customCommands) {
|
|
1710
|
-
appendCommand({
|
|
1711
|
-
name: command.command.name,
|
|
1712
|
-
description: command.command.description,
|
|
1713
|
-
input: { hint: "arguments" },
|
|
1714
|
-
});
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
for (const command of await loadSlashCommands({ cwd: session.sessionManager.getCwd() })) {
|
|
1718
|
-
appendCommand({
|
|
1719
|
-
name: command.name,
|
|
1720
|
-
description: command.description,
|
|
1721
|
-
});
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
return commands;
|
|
1661
|
+
return toAcpAvailableCommands(await buildAvailableSlashCommands(session));
|
|
1725
1662
|
}
|
|
1726
1663
|
|
|
1727
1664
|
#toSessionInfo(session: StoredSessionInfo): SessionInfo {
|
|
@@ -36,6 +36,16 @@ export class AssistantMessageComponent extends Container {
|
|
|
36
36
|
* transcript keeps the error in history.
|
|
37
37
|
*/
|
|
38
38
|
#errorPinned = false;
|
|
39
|
+
/**
|
|
40
|
+
* Monotonic content version reported to the transcript container via
|
|
41
|
+
* {@link getTranscriptBlockVersion}. Bumped by {@link updateContent} — the
|
|
42
|
+
* choke point every mutator funnels through, including the post-finalize
|
|
43
|
+
* ones: `setErrorPinned(false)` restoring the inline error at the next
|
|
44
|
+
* turn's `agent_start`, late tool-result images, async Kitty conversions,
|
|
45
|
+
* and `setUsageInfo`. Without it, the container's committed-scrollback
|
|
46
|
+
* bypass would replay this block's pre-mutation bytes forever.
|
|
47
|
+
*/
|
|
48
|
+
#blockVersion = 0;
|
|
39
49
|
/** Whether the last updateContent carried an in-flight streaming partial; such
|
|
40
50
|
* renders bypass the markdown module LRU (see Markdown.transientRenderCache). */
|
|
41
51
|
#lastUpdateTransient = false;
|
|
@@ -86,6 +96,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
86
96
|
return this.#transcriptBlockFinalized;
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
getTranscriptBlockVersion(): number {
|
|
100
|
+
return this.#blockVersion;
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
markTranscriptBlockFinalized(): void {
|
|
90
104
|
this.#transcriptBlockFinalized = true;
|
|
91
105
|
}
|
|
@@ -215,6 +229,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
215
229
|
}
|
|
216
230
|
|
|
217
231
|
updateContent(message: AssistantMessage, opts?: { transient?: boolean }): void {
|
|
232
|
+
this.#blockVersion++;
|
|
218
233
|
this.#lastMessage = message;
|
|
219
234
|
this.#lastUpdateTransient = opts?.transient === true;
|
|
220
235
|
|
|
@@ -73,7 +73,11 @@ export class BtwPanelComponent extends Container {
|
|
|
73
73
|
this.addChild(new Text(this.#footerLine(), 1, 0));
|
|
74
74
|
this.addChild(new Spacer(1));
|
|
75
75
|
this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
|
|
76
|
-
this
|
|
76
|
+
// Component-scoped: a rebuild replaces only this panel's own children
|
|
77
|
+
// (streaming deltas arrive per token, and a full compose would re-walk
|
|
78
|
+
// the whole transcript each time). Before the panel is mounted the TUI
|
|
79
|
+
// cannot resolve it and falls back to a full compose on its own.
|
|
80
|
+
this.#tui.requestComponentRender(this);
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
#footerLine(): string {
|
|
@@ -57,6 +57,7 @@ export interface MCPAddWizardOAuthResult {
|
|
|
57
57
|
credentialId: string;
|
|
58
58
|
clientId?: string;
|
|
59
59
|
clientSecret?: string;
|
|
60
|
+
resource?: string;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
interface WizardState {
|
|
@@ -71,6 +72,7 @@ interface WizardState {
|
|
|
71
72
|
oauthClientId: string;
|
|
72
73
|
oauthClientSecret: string;
|
|
73
74
|
oauthScopes: string;
|
|
75
|
+
oauthResource: string;
|
|
74
76
|
oauthCredentialId: string | null;
|
|
75
77
|
apiKey: string;
|
|
76
78
|
authLocation: AuthLocation | null;
|
|
@@ -101,6 +103,7 @@ export class MCPAddWizard extends Container {
|
|
|
101
103
|
oauthClientId: "",
|
|
102
104
|
oauthClientSecret: "",
|
|
103
105
|
oauthScopes: "",
|
|
106
|
+
oauthResource: "",
|
|
104
107
|
oauthCredentialId: null,
|
|
105
108
|
apiKey: "",
|
|
106
109
|
authLocation: null,
|
|
@@ -122,6 +125,7 @@ export class MCPAddWizard extends Container {
|
|
|
122
125
|
clientId: string,
|
|
123
126
|
clientSecret: string,
|
|
124
127
|
scopes: string,
|
|
128
|
+
resource?: string,
|
|
125
129
|
) => Promise<MCPAddWizardOAuthResult>)
|
|
126
130
|
| null = null;
|
|
127
131
|
#onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
|
|
@@ -136,6 +140,7 @@ export class MCPAddWizard extends Container {
|
|
|
136
140
|
clientId: string,
|
|
137
141
|
clientSecret: string,
|
|
138
142
|
scopes: string,
|
|
143
|
+
resource?: string,
|
|
139
144
|
) => Promise<MCPAddWizardOAuthResult>,
|
|
140
145
|
onTestConnection?: (config: MCPServerConfig) => Promise<void>,
|
|
141
146
|
onRender?: () => void,
|
|
@@ -987,6 +992,7 @@ export class MCPAddWizard extends Container {
|
|
|
987
992
|
this.#state.oauthTokenUrl = oauth.tokenUrl;
|
|
988
993
|
this.#state.oauthClientId = oauth.clientId || "";
|
|
989
994
|
this.#state.oauthScopes = oauth.scopes || "";
|
|
995
|
+
this.#state.oauthResource = oauth.resource || (this.#state.transport === "stdio" ? "" : this.#state.url);
|
|
990
996
|
this.#state.authMethod = "oauth";
|
|
991
997
|
|
|
992
998
|
this.#contentContainer.clear();
|
|
@@ -1054,6 +1060,7 @@ export class MCPAddWizard extends Container {
|
|
|
1054
1060
|
type: "oauth",
|
|
1055
1061
|
credentialId: this.#state.oauthCredentialId,
|
|
1056
1062
|
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1063
|
+
resource: this.#state.oauthResource || undefined,
|
|
1057
1064
|
clientId: this.#state.oauthClientId || undefined,
|
|
1058
1065
|
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1059
1066
|
};
|
|
@@ -1081,6 +1088,7 @@ export class MCPAddWizard extends Container {
|
|
|
1081
1088
|
type: "oauth",
|
|
1082
1089
|
credentialId: this.#state.oauthCredentialId,
|
|
1083
1090
|
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1091
|
+
resource: this.#state.oauthResource || undefined,
|
|
1084
1092
|
clientId: this.#state.oauthClientId || undefined,
|
|
1085
1093
|
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1086
1094
|
};
|
|
@@ -1142,12 +1150,14 @@ export class MCPAddWizard extends Container {
|
|
|
1142
1150
|
|
|
1143
1151
|
try {
|
|
1144
1152
|
// Call OAuth handler
|
|
1153
|
+
const oauthResource = this.#state.oauthResource || (this.#state.transport === "stdio" ? "" : this.#state.url);
|
|
1145
1154
|
const oauthResult = await this.#onOAuthCallback(
|
|
1146
1155
|
this.#state.oauthAuthUrl,
|
|
1147
1156
|
this.#state.oauthTokenUrl,
|
|
1148
1157
|
this.#state.oauthClientId,
|
|
1149
1158
|
this.#state.oauthClientSecret,
|
|
1150
1159
|
this.#state.oauthScopes,
|
|
1160
|
+
oauthResource || undefined,
|
|
1151
1161
|
);
|
|
1152
1162
|
|
|
1153
1163
|
// Store credential ID + any dynamically-registered client credentials,
|
|
@@ -1155,6 +1165,7 @@ export class MCPAddWizard extends Container {
|
|
|
1155
1165
|
this.#state.oauthCredentialId = oauthResult.credentialId;
|
|
1156
1166
|
if (oauthResult.clientId) this.#state.oauthClientId = oauthResult.clientId;
|
|
1157
1167
|
if (oauthResult.clientSecret) this.#state.oauthClientSecret = oauthResult.clientSecret;
|
|
1168
|
+
this.#state.oauthResource = oauthResult.resource ?? oauthResource;
|
|
1158
1169
|
|
|
1159
1170
|
// Show success message
|
|
1160
1171
|
this.#contentContainer.clear();
|
|
@@ -1284,6 +1295,7 @@ export class MCPAddWizard extends Container {
|
|
|
1284
1295
|
type: "oauth",
|
|
1285
1296
|
credentialId: this.#state.oauthCredentialId,
|
|
1286
1297
|
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1298
|
+
resource: this.#state.oauthResource || undefined,
|
|
1287
1299
|
clientId: this.#state.oauthClientId || undefined,
|
|
1288
1300
|
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1289
1301
|
};
|
|
@@ -1312,6 +1324,7 @@ export class MCPAddWizard extends Container {
|
|
|
1312
1324
|
type: "oauth",
|
|
1313
1325
|
credentialId: this.#state.oauthCredentialId,
|
|
1314
1326
|
tokenUrl: this.#state.oauthTokenUrl || undefined,
|
|
1327
|
+
resource: this.#state.oauthResource || undefined,
|
|
1315
1328
|
clientId: this.#state.oauthClientId || undefined,
|
|
1316
1329
|
clientSecret: this.#state.oauthClientSecret || undefined,
|
|
1317
1330
|
};
|