diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6fcd162..6c4d4ccf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Auto-reply: fix /status usage summary filtering for the active provider. - Status: show provider prefix in /status model display. (#506) — thanks @mcinteerj - Status: compact /status with session token usage + estimated cost, add `/cost` per-response usage lines (tokens-only for OAuth). +- Status: show active auth profile and key snippet in /status. - macOS: package ClawdbotKit resources and Swift 6.2 compatibility dylib to avoid launch/tool crashes. (#473) — thanks @gupsammy - WhatsApp: group `/model list` output by provider for scannability. (#456) - thanks @mcinteerj - Hooks: allow per-hook model overrides for webhook/Gmail runs (e.g. GPT 5 Mini). diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 721139de6..f6d3a1a71 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -218,6 +218,70 @@ describe("trigger handling", () => { }); }); + it("reports active auth profile and key snippet in status", async () => { + await withTempHome(async (home) => { + const cfg = makeCfg(home); + const agentDir = join(home, ".clawdbot", "agents", "main", "agent"); + await fs.mkdir(agentDir, { recursive: true }); + await fs.writeFile( + join(agentDir, "auth-profiles.json"), + JSON.stringify( + { + version: 1, + profiles: { + "anthropic:work": { + type: "api_key", + provider: "anthropic", + key: "sk-test-1234567890abcdef", + }, + }, + lastGood: { anthropic: "anthropic:work" }, + }, + null, + 2, + ), + ); + + const sessionKey = resolveSessionKey("per-sender", { + From: "+1002", + To: "+2000", + Provider: "whatsapp", + } as Parameters[1]); + await fs.writeFile( + cfg.session.store, + JSON.stringify( + { + [sessionKey]: { + sessionId: "session-auth", + updatedAt: Date.now(), + authProfileOverride: "anthropic:work", + }, + }, + null, + 2, + ), + ); + + const res = await getReplyFromConfig( + { + Body: "/status", + From: "+1002", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1002", + }, + {}, + cfg, + ); + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("🔑 api-key"); + expect(text).toContain("…"); + expect(text).toContain("(anthropic:work)"); + expect(text).not.toContain("mixed"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("ignores inline /status and runs the agent", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index 2deb1a753..d27b685b9 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -1,6 +1,7 @@ import { ensureAuthProfileStore, - listProfilesForProvider, + resolveAuthProfileDisplayLabel, + resolveAuthProfileOrder, } from "../../agents/auth-profiles.js"; import { getCustomProviderApiKey, @@ -92,32 +93,65 @@ export type CommandContext = { to?: string; }; +function formatApiKeySnippet(apiKey: string): string { + const compact = apiKey.replace(/\s+/g, ""); + if (!compact) return "unknown"; + const edge = compact.length >= 12 ? 6 : 4; + const head = compact.slice(0, edge); + const tail = compact.slice(-edge); + return `${head}…${tail}`; +} + function resolveModelAuthLabel( provider?: string, cfg?: ClawdbotConfig, + sessionEntry?: SessionEntry, ): string | undefined { const resolved = provider?.trim(); if (!resolved) return undefined; + const providerKey = normalizeProviderId(resolved); const store = ensureAuthProfileStore(); - const profiles = listProfilesForProvider(store, resolved); - if (profiles.length > 0) { - const modes = new Set( - profiles - .map((id) => store.profiles[id]?.type) - .filter((mode): mode is "api_key" | "oauth" => Boolean(mode)), - ); - if (modes.has("oauth") && modes.has("api_key")) return "mixed"; - if (modes.has("oauth")) return "oauth"; - if (modes.has("api_key")) return "api-key"; + const profileOverride = sessionEntry?.authProfileOverride?.trim(); + const lastGood = + store.lastGood?.[providerKey] ?? store.lastGood?.[resolved]; + const order = resolveAuthProfileOrder({ + cfg, + store, + provider: providerKey, + preferredProfile: profileOverride, + }); + const candidates = [ + profileOverride, + lastGood, + ...order, + ].filter(Boolean) as string[]; + + for (const profileId of candidates) { + const profile = store.profiles[profileId]; + if (!profile || normalizeProviderId(profile.provider) !== providerKey) { + continue; + } + const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId }); + if (profile.type === "oauth") { + return `oauth${label ? ` (${label})` : ""}`; + } + const snippet = formatApiKeySnippet(profile.key); + return `api-key ${snippet}${label ? ` (${label})` : ""}`; } - const envKey = resolveEnvApiKey(resolved); + const envKey = resolveEnvApiKey(providerKey); if (envKey?.apiKey) { - return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key"; + if (envKey.source.includes("OAUTH_TOKEN")) { + return `oauth (${envKey.source})`; + } + return `api-key ${formatApiKeySnippet(envKey.apiKey)} (${envKey.source})`; } - if (getCustomProviderApiKey(cfg, resolved)) return "api-key"; + const customKey = getCustomProviderApiKey(cfg, providerKey); + if (customKey) { + return `api-key ${formatApiKeySnippet(customKey)} (models.json)`; + } return "unknown"; } @@ -469,7 +503,7 @@ export async function handleCommands(params: { resolvedVerbose: resolvedVerboseLevel, resolvedReasoning: resolvedReasoningLevel, resolvedElevated: resolvedElevatedLevel, - modelAuth: resolveModelAuthLabel(provider, cfg), + modelAuth: resolveModelAuthLabel(provider, cfg, sessionEntry), usageLine: usageLine ?? undefined, queue: { mode: queueSettings.mode,