* feat: add comprehensive API key manager (/gsd keys) Add /gsd keys command with 6 subcommands for full API key lifecycle management: list, add, remove, test, rotate, and doctor. - list/status: Dashboard grouped by category (LLM, search, tool, remote) with masked key previews, OAuth expiry, env var source detection - add: Interactive provider picker with OAuth vs API key choice, prefix validation, and env var activation - remove: Multi-key support with individual or bulk removal - test: Lightweight API validation per provider with latency reporting and error classification (401/429/5xx/timeout) - rotate: Remove-and-replace flow with optional pre-save validation - doctor: Health checks for expired OAuth, empty keys, duplicates, env var conflicts, file permissions, missing LLM provider Includes unified provider registry (22 providers), tab completions, and redirect from /gsd setup keys. 44 unit tests. * fix: convert key-manager tests from vitest to node:test for CI typecheck Extension tests use node:test + node:assert/strict (not vitest) since tsconfig.extensions.json includes test files and vitest types are not available in the CI typecheck step.
9.1 KiB
API Key Manager — Implementation Plan
Problem Statement
GSD has solid API key infrastructure (AuthStorage, OAuth flows, rate-limit backoff, multi-key rotation) but lacks a user-facing CLI for day-to-day key management. Users currently must either:
- Run the full onboarding wizard to add keys
- Manually edit
~/.gsd/agent/auth.json - Use the limited
/gsd setup keysflow (only covers 5 tool keys, no LLM keys)
There's no way to list, test, remove, or inspect key health from the CLI.
Scope
Build /gsd keys — a comprehensive API key management command with subcommands:
/gsd keys → Show key status dashboard
/gsd keys list → List all configured keys with status
/gsd keys add <provider> → Add/replace a key for a provider
/gsd keys remove <provider> → Remove a key for a provider
/gsd keys test [provider] → Validate key(s) by making a lightweight API call
/gsd keys rotate <provider> → Remove old key and prompt for new one
/gsd keys doctor → Health check all keys (expired OAuth, empty keys, backoff state)
Architecture
New Files
| File | Purpose |
|---|---|
src/resources/extensions/gsd/key-manager.ts |
Core key manager logic (list, add, remove, test, rotate, doctor) |
src/resources/extensions/gsd/tests/key-manager.test.ts |
Unit tests |
Modified Files
| File | Change |
|---|---|
src/resources/extensions/gsd/commands.ts |
Add /gsd keys subcommand routing + completions |
No changes to core packages
All work stays in the GSD extension layer. We use AuthStorage as-is — no modifications to pi-coding-agent or pi-ai.
Phase 1: Key Status Dashboard (/gsd keys and /gsd keys list)
What it shows
GSD API Key Manager
LLM Providers
✓ anthropic — OAuth (expires in 23h 41m)
✓ openai — API key (sk-...a4Bf)
✗ google — not configured
✗ groq — not configured (env: GROQ_API_KEY)
Tool Keys
✓ tavily — API key (tvly-...x92k)
✓ context7 — API key (c7-...m3np)
✗ brave — not configured (env: BRAVE_API_KEY)
✗ jina — not configured (env: JINA_API_KEY)
Remote Integrations
✓ discord_bot — API key (configured)
✗ slack_bot — not configured
✗ telegram_bot — not configured
Search Providers
✓ tavily — API key (tvly-...x92k)
Source: ~/.gsd/agent/auth.json
3 keys configured | 2 from env vars | 1 OAuth token
Implementation
- Read all known provider IDs from
env-api-keys.tsenvMap + LLM_PROVIDER_IDS + TOOL_KEYS - Check
authStorage.has(),authStorage.get(), andgetEnvApiKey()for each - For API keys: show masked preview (first 4 + last 4 chars)
- For OAuth: show expiration time remaining
- For env vars: indicate source is environment
- Group by category (LLM, Tools, Remote, Search)
- Show backoff status if any keys are currently backed off
Phase 2: Add Key (/gsd keys add <provider>)
Flow
- If
<provider>not specified → show interactive provider picker (grouped by category) - If provider has OAuth available → offer "Browser login" or "API key" choice
- For API key: masked password input → prefix validation → save to auth.json
- For OAuth: delegate to existing
authStorage.login()flow - Confirm save with masked preview
Provider Registry
Build a unified provider registry that merges:
LLM_PROVIDER_IDSfrom onboarding.tsTOOL_KEYSfrom commands.tsenvMapfrom env-api-keys.ts- Remote bot tokens (discord_bot, slack_bot, telegram_bot)
Each entry has:
interface ProviderInfo {
id: string
label: string
category: 'llm' | 'tool' | 'search' | 'remote'
envVar?: string // Known env var name
prefixes?: string[] // Expected key prefixes for validation
hasOAuth?: boolean // Whether OAuth login is available
dashboardUrl?: string // Where to get the key
}
Phase 3: Remove Key (/gsd keys remove <provider>)
Flow
- If
<provider>not specified → show picker of configured keys only - Confirm removal (show what will be removed)
- Call
authStorage.remove(provider) - Clear corresponding env var from process.env
- Notify success
Multi-key handling
If a provider has multiple keys (round-robin), show:
anthropic has 3 API keys configured:
[1] sk-ant-...a4Bf
[2] sk-ant-...x92k
[3] sk-ant-...m3np
Remove: all | specific index?
Phase 4: Test Key (/gsd keys test [provider])
Validation Strategy
For each provider, make the lightest possible API call:
| Provider | Test Method |
|---|---|
| anthropic | POST /v1/messages with max_tokens: 1 and a trivial prompt |
| openai | GET /v1/models (list models endpoint) |
GET /v1beta/models |
|
| groq | GET /openai/v1/models |
| brave | GET /res/v1/web/search?q=test&count=1 |
| tavily | POST /search with minimal params |
| context7 | Lightweight search query |
| jina | GET / health check |
| discord_bot | GET /api/v10/users/@me |
| slack_bot | POST auth.test |
| telegram_bot | GET /getMe |
Output
Testing API keys...
✓ anthropic — valid (claude-sonnet-4-20250514 available) 142ms
✓ openai — valid (gpt-4o available) 89ms
✗ groq — invalid (401 Unauthorized)
✓ tavily — valid 203ms
⚠ brave — rate limited (retry in 28s)
— jina — skipped (not configured)
3 valid | 1 invalid | 1 rate-limited | 1 skipped
Error Classification
- 401/403 → "invalid key"
- 429 → "rate limited (retry in Xs)"
- 5xx → "server error"
- timeout → "unreachable"
- success → "valid" + model info if available
Phase 5: Rotate Key (/gsd keys rotate <provider>)
Flow
- Show current key (masked)
- Prompt for new key
- Validate prefix format
- Optionally test the new key before saving (
/gsd keys testlogic) - Replace in auth.json
- Update process.env
- Confirm
Phase 6: Key Doctor (/gsd keys doctor)
Checks
- Expired OAuth tokens — OAuth credentials past their expiration
- Empty keys — Providers with empty string keys (from skipped onboarding)
- Duplicate keys — Same key stored under multiple providers
- Missing required keys — LLM provider not configured at all
- Backoff state — Keys currently in rate-limit backoff
- Env var conflicts — Key in auth.json differs from env var
- File permissions — auth.json not 0o600
Output
API Key Health Check
⚠ anthropic: OAuth token expires in 4m — will auto-refresh
✗ groq: empty key stored (from skipped setup) — run /gsd keys add groq
⚠ openai: env var OPENAI_API_KEY differs from auth.json — env takes priority
✗ auth.json permissions: 0644 (should be 0600) — fixing...
✓ No duplicate keys found
✓ No keys in backoff state
2 warnings | 1 issue fixed | 1 action needed
Integration Points
Command Registration (commands.ts)
Add to the /gsd subcommand router:
if (trimmed === "keys" || trimmed.startsWith("keys ")) {
const keysArgs = trimmed.replace(/^keys\s*/, "").trim();
await handleKeys(keysArgs, ctx);
return;
}
Add tab completions for keys subcommands:
if (parts[0] === "keys" && parts.length <= 2) {
// list, add, remove, test, rotate, doctor
}
Redirect /gsd setup keys
Update handleSetup to route keys to the new handler instead of handleConfig.
Help Text
Add to the help output in the appropriate category.
Testing Strategy
Unit Tests (key-manager.test.ts)
- Provider registry — All known providers have correct metadata
- Key masking — Masks correctly for various key lengths
- Status formatting — Dashboard output matches expected format
- Add key — Stores via AuthStorage.inMemory()
- Remove key — Removes correctly, handles multi-key providers
- Doctor checks — Detects expired OAuth, empty keys, permission issues
- Test key result formatting — Correct status symbols and messages
Integration-level (manual)
- Full add → test → rotate → remove flow
- OAuth provider login flow
- Multi-key round-robin after adding multiple keys
Implementation Order
- Phase 1 —
key-manager.tswith provider registry + list/status dashboard - Phase 2 — Add key (interactive picker + validation)
- Phase 3 — Remove key (with multi-key handling)
- Phase 4 — Test key (lightweight API calls per provider)
- Phase 5 — Rotate key (remove + add in one flow)
- Phase 6 — Key doctor (health checks)
- Wire up — Command registration, completions, help text, redirect setup keys
- Tests — Unit tests for all phases
Out of Scope
- Encrypted-at-rest storage (would require a master password / keyring integration — separate effort)
- Per-project key scoping (would require project-level auth.json — separate effort)
- Key usage tracking/audit log (would require persistent metrics — separate effort)
- Changes to
pi-coding-agentorpi-aipackages