diff --git a/CHANGELOG.md b/CHANGELOG.md index 58634d8c3..32fbf04fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - macOS: Connections settings now use a custom sidebar to avoid toolbar toggle issues, with rounded styling and full-width row hit targets. - macOS: drop deprecated `afterMs` from agent wait params to match gateway schema. - Auth: add OpenAI Codex OAuth support and migrate legacy oauth.json into auth.json. +- Model: `/model` list shows auth source (masked key or OAuth email) per provider. - Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding. - Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments. - Status: show runtime (docker/direct) and move shortcuts to `/help`. diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 55c321c6e..a497f8a1a 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -124,6 +124,13 @@ function migrateOAuthStorageToAuthStorage( } } +export function hydrateAuthStorage( + authStorage: ReturnType, +): void { + ensureOAuthStorage(); + migrateOAuthStorageToAuthStorage(authStorage); +} + function isOAuthProvider(provider: string): provider is OAuthProvider { return ( provider === "anthropic" || diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index 7739d2d69..3933f19ed 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -578,6 +578,7 @@ describe("directive parsing", () => { expect(text).toContain("anthropic/claude-opus-4-5"); expect(text).toContain("openai/gpt-4.1-mini"); expect(text).not.toContain("claude-sonnet-4-1"); + expect(text).toContain("auth:"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -604,6 +605,7 @@ describe("directive parsing", () => { expect(text).toContain("anthropic/claude-opus-4-5"); expect(text).toContain("openai/gpt-4.1-mini"); expect(text).not.toContain("claude-sonnet-4-1"); + expect(text).toContain("auth:"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index a12573da4..317f59a4d 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -1,9 +1,13 @@ +import { getEnvApiKey } from "@mariozechner/pi-ai"; +import { discoverAuthStorage } from "@mariozechner/pi-coding-agent"; +import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js"; import { lookupContextTokens } from "../../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER, } from "../../agents/defaults.js"; +import { hydrateAuthStorage } from "../../agents/model-auth.js"; import { buildModelAliasIndex, type ModelAliasIndex, @@ -39,6 +43,40 @@ import { const SYSTEM_MARK = "⚙️"; +const maskApiKey = (value: string): string => { + const trimmed = value.trim(); + if (!trimmed) return "missing"; + if (trimmed.length <= 12) return trimmed; + return `${trimmed.slice(0, 6)}...${trimmed.slice(-6)}`; +}; + +const resolveAuthLabel = async ( + provider: string, + authStorage: ReturnType, +): Promise => { + const stored = authStorage.get(provider); + if (stored?.type === "oauth") { + const email = stored.email?.trim(); + return email ? `OAuth ${email}` : "OAuth (unknown)"; + } + if (stored?.type === "api_key") { + return maskApiKey(stored.key); + } + const envKey = getEnvApiKey(provider); + if (envKey) return maskApiKey(envKey); + if (provider === "anthropic") { + const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN?.trim(); + if (oauthEnv) return "OAuth (env)"; + } + try { + const key = await authStorage.getApiKey(provider); + if (key) return maskApiKey(key); + } catch { + // ignore missing auth + } + return "missing"; +}; + export type InlineDirectives = { cleaned: string; hasThinkDirective: boolean; @@ -202,6 +240,16 @@ export async function handleDirectiveOnly(params: { if (allowedModelCatalog.length === 0) { return { text: "No models available." }; } + const authStorage = discoverAuthStorage(resolveClawdbotAgentDir()); + hydrateAuthStorage(authStorage); + const authByProvider = new Map(); + for (const entry of allowedModelCatalog) { + if (authByProvider.has(entry.provider)) continue; + authByProvider.set( + entry.provider, + await resolveAuthLabel(entry.provider, authStorage), + ); + } const current = `${params.provider}/${params.model}`; const defaultLabel = `${defaultProvider}/${defaultModel}`; const header = @@ -219,9 +267,11 @@ export async function handleDirectiveOnly(params: { aliases && aliases.length > 0 ? ` (alias: ${aliases.join(", ")})` : ""; - const suffix = + const nameSuffix = entry.name && entry.name !== entry.id ? ` — ${entry.name}` : ""; - lines.push(`- ${label}${aliasSuffix}${suffix}`); + const authLabel = authByProvider.get(entry.provider) ?? "missing"; + const authSuffix = ` — auth: ${authLabel}`; + lines.push(`- ${label}${aliasSuffix}${nameSuffix}${authSuffix}`); } return { text: lines.join("\n") }; }