@oh-my-pi/pi-coding-agent 13.5.2 → 13.5.4

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 CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.5.4] - 2026-03-01
6
+ ### Added
7
+
8
+ - Added `authServerUrl` field to `AuthDetectionResult` to capture OAuth server metadata from `Mcp-Auth-Server` headers
9
+ - Added `extractMcpAuthServerUrl()` function to parse and validate `Mcp-Auth-Server` URLs from error messages
10
+ - Added support for `/.well-known/oauth-protected-resource` discovery endpoint to resolve authorization servers
11
+ - Added recursive auth server discovery to follow `authorization_servers` references when discovering OAuth endpoints
12
+
13
+ - Added `omp agents unpack` CLI subcommand to export bundled subagent definitions to `~/.omp/agent/agents` by default, with `--project` support for `./.omp/agents`
14
+ ### Changed
15
+
16
+ - Enhanced `discoverOAuthEndpoints()` to accept optional `authServerUrl` parameter and query both auth server and resource server for OAuth metadata
17
+ - Improved OAuth metadata extraction to handle additional field name variations (`clientId`, `default_client_id`, `public_client_id`)
18
+ - Refactored OAuth endpoint discovery logic into reusable `findEndpoints()` helper for consistent metadata parsing across multiple sources
19
+ - Task subagents now strip inherited `AGENTS.md` context files and the task tool prompt no longer warns against repeating AGENTS guidance, aligning subagent context with explicit task inputs ([#233](https://github.com/can1357/oh-my-pi/issues/233))
20
+
21
+ ### Fixed
22
+
23
+ - Fixed MCP OAuth discovery to honor `Mcp-Auth-Server` metadata and resolve authorization endpoints from the declared auth server, restoring Figma MCP login URLs with `client_id` ([#235](https://github.com/can1357/oh-my-pi/issues/235))
24
+
25
+ ## [13.5.3] - 2026-03-01
26
+
27
+ ### Added
28
+
29
+ - Auto-include `ast_grep` and `ast_edit` tools when their text-based counterparts (`grep`, `edit`) are requested and the AST tools are enabled
30
+ - Enforced tool decision in plan mode—agent now requires calling either `ask` or `exit_plan_mode` when a turn ends without a required tool call
31
+ - Auto-correction of escaped tab indentation in edits (enabled by default, controllable via `PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS` environment variable)
32
+ - Warning when suspicious Unicode escape placeholder `\uDDDD` is detected in edit content
33
+
34
+ ### Changed
35
+
36
+ - Updated bash tool description to conditionally show `ast_grep` and `ast_edit` guidance based on tool availability in the session
37
+ - Replaced timeout-based cancellation with AbortSignal-based cancellation in the `ask` tool for more reliable user interaction handling
38
+ - Updated `ask` tool to distinguish between user-initiated cancellation and timeout-driven auto-selection, with only user cancellation aborting the turn
39
+ - Updated hashline documentation to clarify that `\t` in JSON represents a real tab character, not a literal backslash-t sequence
40
+
41
+ ### Fixed
42
+
43
+ - Fixed race condition in dialog overlay handling where multiple concurrent resolutions could occur
44
+ - Cancelling the `ask` tool now aborts the current turn instead of returning a normal cancelled selection, while timeout-driven auto-cancel still returns without aborting
45
+
5
46
  ## [13.5.2] - 2026-03-01
6
47
 
7
48
  ### Added
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": "13.5.2",
4
+ "version": "13.5.4",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.5.2",
45
- "@oh-my-pi/pi-agent-core": "13.5.2",
46
- "@oh-my-pi/pi-ai": "13.5.2",
47
- "@oh-my-pi/pi-natives": "13.5.2",
48
- "@oh-my-pi/pi-tui": "13.5.2",
49
- "@oh-my-pi/pi-utils": "13.5.2",
44
+ "@oh-my-pi/omp-stats": "13.5.4",
45
+ "@oh-my-pi/pi-agent-core": "13.5.4",
46
+ "@oh-my-pi/pi-ai": "13.5.4",
47
+ "@oh-my-pi/pi-natives": "13.5.4",
48
+ "@oh-my-pi/pi-tui": "13.5.4",
49
+ "@oh-my-pi/pi-utils": "13.5.4",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Agents CLI command handlers.
3
+ *
4
+ * Handles `omp agents unpack` for writing bundled agent definitions to disk.
5
+ */
6
+ import * as fs from "node:fs/promises";
7
+ import * as path from "node:path";
8
+ import { getAgentDir, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
9
+ import { YAML } from "bun";
10
+ import chalk from "chalk";
11
+ import { theme } from "../modes/theme/theme";
12
+ import { loadBundledAgents } from "../task/agents";
13
+ import type { AgentDefinition } from "../task/types";
14
+
15
+ export type AgentsAction = "unpack";
16
+
17
+ export interface AgentsCommandArgs {
18
+ action: AgentsAction;
19
+ flags: {
20
+ force?: boolean;
21
+ json?: boolean;
22
+ dir?: string;
23
+ user?: boolean;
24
+ project?: boolean;
25
+ };
26
+ }
27
+
28
+ interface UnpackResult {
29
+ targetDir: string;
30
+ total: number;
31
+ written: string[];
32
+ skipped: string[];
33
+ }
34
+
35
+ function writeStdout(line: string): void {
36
+ process.stdout.write(`${line}\n`);
37
+ }
38
+
39
+ function resolveTargetDir(flags: AgentsCommandArgs["flags"]): string {
40
+ if (flags.dir && flags.dir.trim().length > 0) {
41
+ return path.resolve(getProjectDir(), flags.dir.trim());
42
+ }
43
+
44
+ if (flags.user && flags.project) {
45
+ throw new Error("Choose either --user or --project, not both.");
46
+ }
47
+
48
+ if (flags.project) {
49
+ return path.resolve(getProjectDir(), ".omp", "agents");
50
+ }
51
+
52
+ return path.join(getAgentDir(), "agents");
53
+ }
54
+
55
+ function toFrontmatter(agent: AgentDefinition): Record<string, unknown> {
56
+ const frontmatter: Record<string, unknown> = {
57
+ name: agent.name,
58
+ description: agent.description,
59
+ };
60
+
61
+ if (agent.tools && agent.tools.length > 0) frontmatter.tools = agent.tools;
62
+ if (agent.spawns !== undefined) frontmatter.spawns = agent.spawns;
63
+ if (agent.model && agent.model.length > 0) frontmatter.model = agent.model;
64
+ if (agent.thinkingLevel) frontmatter["thinking-level"] = agent.thinkingLevel;
65
+ if (agent.output !== undefined) frontmatter.output = agent.output;
66
+ if (agent.blocking) frontmatter.blocking = true;
67
+
68
+ return frontmatter;
69
+ }
70
+
71
+ function serializeAgent(agent: AgentDefinition): string {
72
+ const frontmatter = YAML.stringify(toFrontmatter(agent), null, 2).trimEnd();
73
+ const body = agent.systemPrompt.trim();
74
+ return `---\n${frontmatter}\n---\n\n${body}\n`;
75
+ }
76
+
77
+ async function unpackBundledAgents(flags: AgentsCommandArgs["flags"]): Promise<UnpackResult> {
78
+ const targetDir = resolveTargetDir(flags);
79
+ await fs.mkdir(targetDir, { recursive: true });
80
+
81
+ const bundledAgents = [...loadBundledAgents()].sort((a, b) => a.name.localeCompare(b.name));
82
+ const written: string[] = [];
83
+ const skipped: string[] = [];
84
+
85
+ for (const agent of bundledAgents) {
86
+ const filePath = path.join(targetDir, `${agent.name}.md`);
87
+ if (!flags.force) {
88
+ try {
89
+ await fs.stat(filePath);
90
+ skipped.push(filePath);
91
+ continue;
92
+ } catch (error) {
93
+ if (!isEnoent(error)) throw error;
94
+ }
95
+ }
96
+
97
+ await Bun.write(filePath, serializeAgent(agent));
98
+ written.push(filePath);
99
+ }
100
+
101
+ return {
102
+ targetDir,
103
+ total: bundledAgents.length,
104
+ written,
105
+ skipped,
106
+ };
107
+ }
108
+
109
+ export async function runAgentsCommand(cmd: AgentsCommandArgs): Promise<void> {
110
+ switch (cmd.action) {
111
+ case "unpack": {
112
+ const result = await unpackBundledAgents(cmd.flags);
113
+ if (cmd.flags.json) {
114
+ writeStdout(JSON.stringify(result, null, 2));
115
+ return;
116
+ }
117
+
118
+ writeStdout(chalk.bold(`Bundled agents: ${result.total}`));
119
+ writeStdout(chalk.dim(`Target directory: ${result.targetDir}`));
120
+ writeStdout(chalk.green(`${theme.status.success} Written: ${result.written.length}`));
121
+ if (result.skipped.length > 0) {
122
+ writeStdout(
123
+ chalk.yellow(
124
+ `${theme.status.warning} Skipped existing: ${result.skipped.length} (use --force to overwrite)`,
125
+ ),
126
+ );
127
+ }
128
+
129
+ for (const filePath of result.written) {
130
+ writeStdout(chalk.dim(` + ${filePath}`));
131
+ }
132
+ for (const filePath of result.skipped) {
133
+ writeStdout(chalk.dim(` = ${filePath}`));
134
+ }
135
+ return;
136
+ }
137
+ }
138
+ }
package/src/cli/args.ts CHANGED
@@ -247,7 +247,10 @@ ${chalk.bold("Available Tools (all enabled by default):")}
247
247
  fetch - Fetch and process URLs
248
248
  web_search - Search the web
249
249
  ask - Ask user questions (interactive mode only)
250
- `;
250
+
251
+ ${chalk.bold("Useful Commands:")}
252
+ omp agents unpack - Export bundled subagents to ~/.omp/agent/agents (default)
253
+ omp agents unpack --project - Export bundled subagents to ./.omp/agents`;
251
254
  }
252
255
 
253
256
  export function printHelp(): void {
package/src/cli.ts CHANGED
@@ -16,6 +16,7 @@ process.title = APP_NAME;
16
16
 
17
17
  const commands: CommandEntry[] = [
18
18
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
19
+ { name: "agents", load: () => import("./commands/agents").then(m => m.default) },
19
20
  { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
20
21
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
21
22
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Manage bundled task agents.
3
+ */
4
+ import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli";
5
+ import { type AgentsAction, type AgentsCommandArgs, runAgentsCommand } from "../cli/agents-cli";
6
+ import { initTheme } from "../modes/theme/theme";
7
+
8
+ const ACTIONS: AgentsAction[] = ["unpack"];
9
+
10
+ export default class Agents extends Command {
11
+ static description = "Manage bundled task agents";
12
+
13
+ static args = {
14
+ action: Args.string({
15
+ description: "Agents action",
16
+ required: false,
17
+ options: ACTIONS,
18
+ }),
19
+ };
20
+
21
+ static flags = {
22
+ force: Flags.boolean({ char: "f", description: "Overwrite existing agent files" }),
23
+ json: Flags.boolean({ description: "Output JSON" }),
24
+ dir: Flags.string({ description: "Output directory (overrides --user/--project)" }),
25
+ user: Flags.boolean({ description: "Write to ~/.omp/agent/agents (default)" }),
26
+ project: Flags.boolean({ description: "Write to ./.omp/agents" }),
27
+ };
28
+
29
+ static examples = [
30
+ "# Export bundled agents into user config (default)\n omp agents unpack",
31
+ "# Export bundled agents into project config\n omp agents unpack --project",
32
+ "# Overwrite existing local agent files\n omp agents unpack --project --force",
33
+ "# Export into a custom directory\n omp agents unpack --dir ./tmp/agents --json",
34
+ ];
35
+
36
+ async run(): Promise<void> {
37
+ const { args, flags } = await this.parse(Agents);
38
+ if (!args.action) {
39
+ renderCommandHelp("omp", "agents", Agents);
40
+ return;
41
+ }
42
+
43
+ const cmd: AgentsCommandArgs = {
44
+ action: args.action as AgentsAction,
45
+ flags: {
46
+ force: flags.force,
47
+ json: flags.json,
48
+ dir: flags.dir,
49
+ user: flags.user,
50
+ project: flags.project,
51
+ },
52
+ };
53
+
54
+ await initTheme();
55
+ await runAgentsCommand(cmd);
56
+ }
57
+ }
@@ -255,7 +255,8 @@ handlebars.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectio
255
255
  */
256
256
  function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
257
257
  const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
258
- const text = typeof content === "string" ? content : String(content ?? "");
258
+ const raw = typeof content === "string" ? content : String(content ?? "");
259
+ const text = raw.replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r");
259
260
  const ref = `${num}#${computeLineHash(num, text)}`;
260
261
  return { num, text, ref };
261
262
  }
@@ -16,6 +16,61 @@ export function findApiKey(): string | null {
16
16
  return $env.EXA_API_KEY;
17
17
  }
18
18
 
19
+ function asRecord(value: unknown): Record<string, unknown> | null {
20
+ if (typeof value !== "object" || value === null) return null;
21
+ return value as Record<string, unknown>;
22
+ }
23
+
24
+ function parseJsonContent(text: string): unknown | null {
25
+ try {
26
+ return JSON.parse(text);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Normalize tools/call payloads across MCP servers.
34
+ *
35
+ * Exa currently returns different shapes depending on deployment/environment:
36
+ * - direct payload in result
37
+ * - structured payload under result.structuredContent / result.data / result.result
38
+ * - JSON payload embedded as text in result.content[]
39
+ */
40
+ function normalizeMcpToolPayload(payload: unknown): unknown {
41
+ const candidates: unknown[] = [];
42
+ const root = asRecord(payload);
43
+
44
+ if (root) {
45
+ if (root.structuredContent !== undefined) candidates.push(root.structuredContent);
46
+ if (root.data !== undefined) candidates.push(root.data);
47
+ if (root.result !== undefined) candidates.push(root.result);
48
+ candidates.push(root);
49
+
50
+ const content = root.content;
51
+ if (Array.isArray(content)) {
52
+ for (const item of content) {
53
+ const part = asRecord(item);
54
+ if (!part) continue;
55
+ const text = part.text;
56
+ if (typeof text !== "string" || text.trim().length === 0) continue;
57
+ const parsed = parseJsonContent(text);
58
+ if (parsed !== null) candidates.push(parsed);
59
+ }
60
+ }
61
+ } else {
62
+ candidates.push(payload);
63
+ }
64
+
65
+ for (const candidate of candidates) {
66
+ if (isSearchResponse(candidate)) {
67
+ return candidate;
68
+ }
69
+ }
70
+
71
+ return payload;
72
+ }
73
+
19
74
  /** Fetch available tools from Exa MCP */
20
75
  export async function fetchExaTools(apiKey: string | null, toolNames: string[]): Promise<MCPTool[]> {
21
76
  const params = new URLSearchParams();
@@ -65,7 +120,7 @@ export async function callExaTool(
65
120
  throw new Error(`MCP error: ${response.error.message}`);
66
121
  }
67
122
 
68
- return response.result;
123
+ return normalizeMcpToolPayload(response.result);
69
124
  }
70
125
 
71
126
  /** Call a tool on Websets MCP */
@@ -85,7 +140,7 @@ export async function callWebsetsTool(
85
140
  throw new Error(`MCP error: ${response.error.message}`);
86
141
  }
87
142
 
88
- return response.result;
143
+ return normalizeMcpToolPayload(response.result);
89
144
  }
90
145
 
91
146
  /** Format search results for LLM */
@@ -16,9 +16,25 @@ export interface AuthDetectionResult {
16
16
  requiresAuth: boolean;
17
17
  authType?: "oauth" | "apikey" | "unknown";
18
18
  oauth?: OAuthEndpoints;
19
+ authServerUrl?: string;
19
20
  message?: string;
20
21
  }
21
22
 
23
+ function parseMcpAuthServerUrl(errorMessage: string): string | undefined {
24
+ const match = errorMessage.match(/Mcp-Auth-Server:\s*([^;\]\s]+)/i);
25
+ if (!match || !match[1]) return undefined;
26
+
27
+ try {
28
+ return new URL(match[1]).toString();
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ export function extractMcpAuthServerUrl(error: Error): string | undefined {
35
+ return parseMcpAuthServerUrl(error.message);
36
+ }
37
+
22
38
  /**
23
39
  * Detect if an error indicates authentication is required.
24
40
  * Checks for common auth error patterns.
@@ -178,6 +194,8 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
178
194
  return { requiresAuth: false };
179
195
  }
180
196
 
197
+ const authServerUrl = extractMcpAuthServerUrl(error);
198
+
181
199
  // Try to extract OAuth endpoints
182
200
  const oauth = extractOAuthEndpoints(error);
183
201
 
@@ -186,6 +204,7 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
186
204
  requiresAuth: true,
187
205
  authType: "oauth",
188
206
  oauth,
207
+ authServerUrl,
189
208
  message: "Server requires OAuth authentication. Launching authorization flow...",
190
209
  };
191
210
  }
@@ -201,6 +220,7 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
201
220
  return {
202
221
  requiresAuth: true,
203
222
  authType: "apikey",
223
+ authServerUrl,
204
224
  message: "Server requires API key authentication.",
205
225
  };
206
226
  }
@@ -209,6 +229,7 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
209
229
  return {
210
230
  requiresAuth: true,
211
231
  authType: "unknown",
232
+ authServerUrl,
212
233
  message: "Server requires authentication but type could not be determined.",
213
234
  };
214
235
  }
@@ -217,56 +238,110 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
217
238
  * Try to discover OAuth endpoints by querying the server's well-known endpoints.
218
239
  * This is a fallback when error responses don't include OAuth metadata.
219
240
  */
220
- export async function discoverOAuthEndpoints(serverUrl: string): Promise<OAuthEndpoints | null> {
241
+ export async function discoverOAuthEndpoints(
242
+ serverUrl: string,
243
+ authServerUrl?: string,
244
+ ): Promise<OAuthEndpoints | null> {
221
245
  const wellKnownPaths = [
222
246
  "/.well-known/oauth-authorization-server",
223
247
  "/.well-known/openid-configuration",
248
+ "/.well-known/oauth-protected-resource",
224
249
  "/oauth/metadata",
225
250
  "/.mcp/auth",
226
251
  "/authorize", // Some MCP servers expose OAuth config here
227
252
  ];
253
+ const urlsToQuery = [authServerUrl, serverUrl].filter((value): value is string => Boolean(value));
254
+ const visitedAuthServers = new Set<string>();
228
255
 
229
- for (const path of wellKnownPaths) {
230
- try {
231
- const url = new URL(path, serverUrl);
232
- const response = await fetch(url.toString(), {
233
- method: "GET",
234
- headers: { Accept: "application/json" },
235
- });
256
+ const findEndpoints = (metadata: Record<string, unknown>): OAuthEndpoints | null => {
257
+ if (metadata.authorization_endpoint && metadata.token_endpoint) {
258
+ const scopesSupported = Array.isArray(metadata.scopes_supported)
259
+ ? metadata.scopes_supported.filter((scope): scope is string => typeof scope === "string").join(" ")
260
+ : undefined;
261
+ return {
262
+ authorizationUrl: String(metadata.authorization_endpoint),
263
+ tokenUrl: String(metadata.token_endpoint),
264
+ clientId:
265
+ typeof metadata.client_id === "string"
266
+ ? metadata.client_id
267
+ : typeof metadata.clientId === "string"
268
+ ? metadata.clientId
269
+ : typeof metadata.default_client_id === "string"
270
+ ? metadata.default_client_id
271
+ : typeof metadata.public_client_id === "string"
272
+ ? metadata.public_client_id
273
+ : undefined,
274
+ scopes:
275
+ scopesSupported ||
276
+ (typeof metadata.scopes === "string"
277
+ ? metadata.scopes
278
+ : typeof metadata.scope === "string"
279
+ ? metadata.scope
280
+ : undefined),
281
+ };
282
+ }
236
283
 
237
- if (response.ok) {
238
- const metadata = (await response.json()) as Record<string, any>;
284
+ if (metadata.oauth || metadata.authorization || metadata.auth) {
285
+ const oauthData = (metadata.oauth || metadata.authorization || metadata.auth) as Record<string, unknown>;
286
+ if (typeof oauthData.authorization_url === "string" && typeof oauthData.token_url === "string") {
287
+ return {
288
+ authorizationUrl: oauthData.authorization_url || String(oauthData.authorizationUrl),
289
+ tokenUrl: oauthData.token_url || String(oauthData.tokenUrl),
290
+ clientId:
291
+ typeof oauthData.client_id === "string"
292
+ ? oauthData.client_id
293
+ : typeof oauthData.clientId === "string"
294
+ ? oauthData.clientId
295
+ : typeof oauthData.default_client_id === "string"
296
+ ? oauthData.default_client_id
297
+ : typeof oauthData.public_client_id === "string"
298
+ ? oauthData.public_client_id
299
+ : undefined,
300
+ scopes:
301
+ typeof oauthData.scopes === "string"
302
+ ? oauthData.scopes
303
+ : typeof oauthData.scope === "string"
304
+ ? oauthData.scope
305
+ : undefined,
306
+ };
307
+ }
308
+ }
239
309
 
240
- // Check for standard OAuth discovery format
241
- if (metadata.authorization_endpoint && metadata.token_endpoint) {
242
- return {
243
- authorizationUrl: metadata.authorization_endpoint,
244
- tokenUrl: metadata.token_endpoint,
245
- clientId:
246
- metadata.client_id || metadata.clientId || metadata.default_client_id || metadata.public_client_id,
247
- scopes: metadata.scopes_supported?.join(" ") || metadata.scopes || metadata.scope,
248
- };
249
- }
310
+ return null;
311
+ };
312
+
313
+ for (const baseUrl of urlsToQuery) {
314
+ visitedAuthServers.add(baseUrl);
315
+ for (const path of wellKnownPaths) {
316
+ try {
317
+ const url = new URL(path, baseUrl);
318
+ const response = await fetch(url.toString(), {
319
+ method: "GET",
320
+ headers: { Accept: "application/json" },
321
+ });
250
322
 
251
- // Check for MCP-specific format
252
- if (metadata.oauth || metadata.authorization || metadata.auth) {
253
- const oauthData = metadata.oauth || metadata.authorization || metadata.auth;
254
- if (oauthData.authorization_url && oauthData.token_url) {
255
- return {
256
- authorizationUrl: oauthData.authorization_url || oauthData.authorizationUrl,
257
- tokenUrl: oauthData.token_url || oauthData.tokenUrl,
258
- clientId:
259
- oauthData.client_id ||
260
- oauthData.clientId ||
261
- oauthData.default_client_id ||
262
- oauthData.public_client_id,
263
- scopes: oauthData.scopes || oauthData.scope,
264
- };
323
+ if (response.ok) {
324
+ const metadata = (await response.json()) as Record<string, unknown>;
325
+ const endpoints = findEndpoints(metadata);
326
+ if (endpoints) return endpoints;
327
+
328
+ if (path === "/.well-known/oauth-protected-resource") {
329
+ const authServers = Array.isArray(metadata.authorization_servers)
330
+ ? metadata.authorization_servers.filter((entry): entry is string => typeof entry === "string")
331
+ : [];
332
+
333
+ for (const discoveredAuthServer of authServers) {
334
+ if (visitedAuthServers.has(discoveredAuthServer)) {
335
+ continue;
336
+ }
337
+ const discovered = await discoverOAuthEndpoints(serverUrl, discoveredAuthServer);
338
+ if (discovered) return discovered;
339
+ }
265
340
  }
266
341
  }
342
+ } catch {
343
+ // Ignore errors, try next path
267
344
  }
268
- } catch {
269
- // Ignore errors, try next path
270
345
  }
271
346
  }
272
347
 
@@ -947,7 +947,7 @@ export class MCPAddWizard extends Container {
947
947
  let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
948
948
  if (!oauth && this.#state.transport !== "stdio" && this.#state.url) {
949
949
  try {
950
- oauth = await discoverOAuthEndpoints(this.#state.url);
950
+ oauth = await discoverOAuthEndpoints(this.#state.url, authResult.authServerUrl);
951
951
  } catch {
952
952
  // Ignore discovery failures and fallback to manual auth.
953
953
  }
@@ -983,19 +983,6 @@ function resolveAggregateStatus(limits: UsageLimit[]): UsageLimit["status"] {
983
983
  return "exhausted";
984
984
  }
985
985
 
986
- function isZeroUsage(limit: UsageLimit): boolean {
987
- const amount = limit.amount;
988
- if (amount.usedFraction !== undefined) return amount.usedFraction <= 0;
989
- if (amount.used !== undefined) return amount.used <= 0;
990
- if (amount.unit === "percent" && amount.used !== undefined) return amount.used <= 0;
991
- if (amount.remainingFraction !== undefined) return amount.remainingFraction >= 1;
992
- return false;
993
- }
994
-
995
- function isZeroUsageGroup(limits: UsageLimit[]): boolean {
996
- return limits.length > 0 && limits.every(limit => isZeroUsage(limit));
997
- }
998
-
999
986
  function formatAggregateAmount(limits: UsageLimit[]): string {
1000
987
  const fractions = limits
1001
988
  .map(limit => resolveFraction(limit))
@@ -1117,13 +1104,6 @@ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs
1117
1104
  }
1118
1105
  }
1119
1106
 
1120
- const providerAllZero = isZeroUsageGroup(Array.from(limitGroups.values()).flatMap(group => group.limits));
1121
- if (providerAllZero) {
1122
- const providerTitle = `${resolveStatusIcon("ok", uiTheme)} ${uiTheme.fg("accent", `${providerName} (0%)`)}`;
1123
- lines.push(uiTheme.bold(providerTitle));
1124
- continue;
1125
- }
1126
-
1127
1107
  lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
1128
1108
 
1129
1109
  for (const group of limitGroups.values()) {
@@ -1144,18 +1124,6 @@ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs
1144
1124
 
1145
1125
  const status = resolveAggregateStatus(sortedLimits);
1146
1126
  const statusIcon = resolveStatusIcon(status, uiTheme);
1147
- if (isZeroUsageGroup(sortedLimits)) {
1148
- const resetText = resolveResetRange(sortedLimits, nowMs);
1149
- const resetSuffix = resetText ? ` | ${resetText}` : "";
1150
- const windowSuffix = formatWindowSuffix(group.label, group.windowLabel, uiTheme);
1151
- lines.push(
1152
- `${statusIcon} ${uiTheme.bold(group.label)} ${windowSuffix} ${uiTheme.fg(
1153
- "dim",
1154
- `0%${resetSuffix}`,
1155
- )}`.trim(),
1156
- );
1157
- continue;
1158
- }
1159
1127
 
1160
1128
  const windowSuffix = formatWindowSuffix(group.label, group.windowLabel, uiTheme);
1161
1129
  lines.push(`${statusIcon} ${uiTheme.bold(group.label)} ${windowSuffix}`.trim());