diff options
| author | Adam Malczewski <[email protected]> | 2026-05-22 15:24:13 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-22 15:24:13 +0900 |
| commit | 9ecaabd87c0e51b8a7408dabb0133a9344586859 (patch) | |
| tree | 4b5809ab23948a5e3f558f3aa34c52d5038f4e19 /packages/core/src/credentials | |
| parent | 8f1dd855f0c4c877bff8d3a4ba193432b268b1c2 (diff) | |
| download | dispatch-9ecaabd87c0e51b8a7408dabb0133a9344586859.tar.gz dispatch-9ecaabd87c0e51b8a7408dabb0133a9344586859.zip | |
feat: agent builder, CWD support, auto-save, UI polish, unavailable tool handling
- Agent Builder: full CRUD with card grid, drag-and-drop model reorder, edit/delete
- Auto-save on edit with 600ms debounce, AbortController for concurrency, fieldset disabled until name entered
- Agent definitions stored as TOML with cwd field, loaded from global/project dirs
- Working directory: per-tab CWD override in Chat Settings, agent default CWD, auto-create on first message
- CWD validation: check-dir endpoint with ~ expansion, real-time validity indicator
- Subagent CWD validated against parent's effective CWD using path.relative
- Unavailable tool calls: caught gracefully, shown as tool call with error badge, model retries
- UI: tab bar border radius, sidebar border removed, chat input ghost style, scroll-to-bottom rectangle
- Skills dir collapse uses CSS rotation, Model Choice renamed to Chat Settings, System Prompt view removed
- Reusable SkillsBrowser/ToolPermissions with external mode for Agent Builder
- ModelSelector: Agent/Manual toggle, agent list, Agent Settings link
- Page router, skills recursive scanning, bin/up gopass removed, docker volume mounts
Diffstat (limited to 'packages/core/src/credentials')
| -rw-r--r-- | packages/core/src/credentials/api-keys.ts | 19 | ||||
| -rw-r--r-- | packages/core/src/credentials/claude.ts | 95 | ||||
| -rw-r--r-- | packages/core/src/credentials/index.ts | 26 | ||||
| -rw-r--r-- | packages/core/src/credentials/opencode.ts | 9 | ||||
| -rw-r--r-- | packages/core/src/credentials/store.ts | 26 |
5 files changed, 111 insertions, 64 deletions
diff --git a/packages/core/src/credentials/api-keys.ts b/packages/core/src/credentials/api-keys.ts index 5f92ffa..af5aa0e 100644 --- a/packages/core/src/credentials/api-keys.ts +++ b/packages/core/src/credentials/api-keys.ts @@ -33,9 +33,9 @@ export function setApiKey(keyId: string, provider: string, apiKey: string): void */ export function getApiKey(keyId: string): string | null { const db = getDatabase(); - const row = db.query( - "SELECT api_key FROM api_keys WHERE key_id = $keyId", - ).get({ $keyId: keyId }) as { api_key: string } | null; + const row = db + .query("SELECT api_key FROM api_keys WHERE key_id = $keyId") + .get({ $keyId: keyId }) as { api_key: string } | null; return row?.api_key ?? null; } @@ -57,11 +57,16 @@ export function deleteApiKey(keyId: string): void { /** * List all stored API keys with metadata (key value excluded for security). */ -export function listApiKeys(): Array<{ keyId: string; provider: string; importedAt: number; updatedAt: number }> { +export function listApiKeys(): Array<{ + keyId: string; + provider: string; + importedAt: number; + updatedAt: number; +}> { const db = getDatabase(); - const rows = db.query( - "SELECT key_id, provider, imported_at, updated_at FROM api_keys ORDER BY key_id", - ).all() as Array<Record<string, unknown>>; + const rows = db + .query("SELECT key_id, provider, imported_at, updated_at FROM api_keys ORDER BY key_id") + .all() as Array<Record<string, unknown>>; return rows.map((row) => ({ keyId: row.key_id as string, provider: row.provider as string, diff --git a/packages/core/src/credentials/claude.ts b/packages/core/src/credentials/claude.ts index 1b9d148..6018207 100644 --- a/packages/core/src/credentials/claude.ts +++ b/packages/core/src/credentials/claude.ts @@ -1,9 +1,16 @@ -import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync } from "node:fs"; -import { getStoredCredentials, updateStoredTokens, listStoredCredentials } from "./store.js"; -import { getDatabase } from "../db/index.js"; -import { dirname, join, basename } from "node:path"; -import { homedir } from "node:os"; import { createHash } from "node:crypto"; +import { + chmodSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import { getDatabase } from "../db/index.js"; +import { getStoredCredentials, listStoredCredentials, updateStoredTokens } from "./store.js"; export interface ClaudeCredentials { accessToken: string; @@ -41,7 +48,10 @@ function parseCredentialsFile(raw: string): ClaudeCredentials | null { const data = (parsed as Record<string, unknown>).claudeAiOauth ?? parsed; const creds = data as Record<string, unknown>; - if ((creds as Record<string, unknown>).mcpOAuth && !(creds as Record<string, unknown>).accessToken) { + if ( + (creds as Record<string, unknown>).mcpOAuth && + !(creds as Record<string, unknown>).accessToken + ) { return null; } @@ -57,7 +67,8 @@ function parseCredentialsFile(raw: string): ClaudeCredentials | null { accessToken: creds.accessToken as string, refreshToken: creds.refreshToken as string, expiresAt: creds.expiresAt as number, - subscriptionType: typeof creds.subscriptionType === "string" ? creds.subscriptionType : undefined, + subscriptionType: + typeof creds.subscriptionType === "string" ? creds.subscriptionType : undefined, }; } @@ -122,7 +133,7 @@ async function refreshViaOAuth(refreshToken: string): Promise<ClaudeCredentials return null; } - const data = await response.json() as Record<string, unknown>; + const data = (await response.json()) as Record<string, unknown>; if (!data.access_token || typeof data.access_token !== "string") { return null; } @@ -131,7 +142,8 @@ async function refreshViaOAuth(refreshToken: string): Promise<ClaudeCredentials accessToken: data.access_token as string, refreshToken: (data.refresh_token as string) ?? refreshToken, expiresAt: Date.now() + ((data.expires_in as number) ?? 36_000) * 1000, - subscriptionType: typeof data.subscriptionType === "string" ? data.subscriptionType : undefined, + subscriptionType: + typeof data.subscriptionType === "string" ? data.subscriptionType : undefined, }; } catch { return null; @@ -220,7 +232,11 @@ export function discoverClaudeAccounts(): ClaudeAccount[] { export function refreshAccountCredentials(account: ClaudeAccount): ClaudeCredentials | null { const cached = accountCacheMap.get(account.id); const now = Date.now(); - if (cached && now - cached.cachedAt < CREDENTIAL_CACHE_TTL_MS && cached.creds.expiresAt > now + 60_000) { + if ( + cached && + now - cached.cachedAt < CREDENTIAL_CACHE_TTL_MS && + cached.creds.expiresAt > now + 60_000 + ) { return cached.creds; } @@ -257,10 +273,16 @@ export function refreshAccountCredentials(account: ClaudeAccount): ClaudeCredent return null; } -export async function refreshAccountCredentialsAsync(account: ClaudeAccount): Promise<ClaudeCredentials | null> { +export async function refreshAccountCredentialsAsync( + account: ClaudeAccount, +): Promise<ClaudeCredentials | null> { const cached = accountCacheMap.get(account.id); const now = Date.now(); - if (cached && now - cached.cachedAt < CREDENTIAL_CACHE_TTL_MS && cached.creds.expiresAt > now + 60_000) { + if ( + cached && + now - cached.cachedAt < CREDENTIAL_CACHE_TTL_MS && + cached.creds.expiresAt > now + 60_000 + ) { return cached.creds; } @@ -294,7 +316,12 @@ export async function refreshAccountCredentialsAsync(account: ClaudeAccount): Pr account.credentials = refreshed; // Update DB if this is a DB-backed account, otherwise write to file if (account.source.startsWith("db:")) { - updateStoredTokens(account.id, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt); + updateStoredTokens( + account.id, + refreshed.accessToken, + refreshed.refreshToken, + refreshed.expiresAt, + ); } else { writeCredentialsFile(account.source, refreshed); } @@ -323,14 +350,14 @@ function computeCch(messageText: string): string { } function computeVersionSuffix(messageText: string, version: string): string { - const sampled = [4, 7, 20] - .map((i) => (i < messageText.length ? messageText[i] : "0")) - .join(""); + const sampled = [4, 7, 20].map((i) => (i < messageText.length ? messageText[i] : "0")).join(""); const input = `${BILLING_SALT}${sampled}${version}`; return createHash("sha256").update(input).digest("hex").slice(0, 3); } -export function buildBillingHeaderValue(messages: Array<{ role: string; content: string }>): string { +export function buildBillingHeaderValue( + messages: Array<{ role: string; content: string }>, +): string { const text = extractFirstUserMessageText(messages); const version = process.env.ANTHROPIC_CLI_VERSION ?? CC_VERSION; const suffix = computeVersionSuffix(text, version); @@ -404,11 +431,16 @@ export async function fetchAnthropicModels(accessToken: string): Promise<string[ return []; } - const data = (await response.json()) as { data?: Array<{ id: string }>; models?: Array<{ id: string }> }; + const data = (await response.json()) as { + data?: Array<{ id: string }>; + models?: Array<{ id: string }>; + }; const entries = data.data ?? data.models ?? []; return entries.map((m) => m.id).filter(Boolean); } catch (err) { - console.warn(`dispatch: failed to fetch Anthropic models: ${err instanceof Error ? err.message : String(err)}`); + console.warn( + `dispatch: failed to fetch Anthropic models: ${err instanceof Error ? err.message : String(err)}`, + ); return []; } } @@ -434,7 +466,9 @@ export interface ClaudeProfile { * Validate that Claude credentials are usable by hitting the OAuth profile endpoint. * Returns the profile info if valid, or null if the token is dead. */ -export async function validateAccountCredentials(account: ClaudeAccount): Promise<ClaudeProfile | null> { +export async function validateAccountCredentials( + account: ClaudeAccount, +): Promise<ClaudeProfile | null> { const creds = await refreshAccountCredentialsAsync(account); if (!creds) return null; @@ -487,7 +521,8 @@ async function fetchClaudeUsage(accessToken: string): Promise<ClaudeUsageReport // API returns utilization as 0-100 percentage; normalize to 0-1 fraction const rawUtil = typeof b.utilization === "number" ? b.utilization : undefined; const utilization = rawUtil !== undefined ? rawUtil / 100 : undefined; - const resetsAt = typeof b.resets_at === "string" ? Date.parse(b.resets_at as string) : undefined; + const resetsAt = + typeof b.resets_at === "string" ? Date.parse(b.resets_at as string) : undefined; if (utilization === undefined && resetsAt === undefined) return undefined; return { utilization, resetsAt }; }; @@ -503,9 +538,13 @@ async function fetchClaudeUsage(accessToken: string): Promise<ClaudeUsageReport // Try to extract identity const accountId = - typeof data.account_id === "string" ? data.account_id : - typeof data.user_id === "string" ? data.user_id : - typeof data.org_id === "string" ? data.org_id : undefined; + typeof data.account_id === "string" + ? data.account_id + : typeof data.user_id === "string" + ? data.user_id + : typeof data.org_id === "string" + ? data.org_id + : undefined; if (accountId) report.accountId = accountId; const email = typeof data.email === "string" ? data.email : undefined; @@ -520,9 +559,9 @@ async function fetchClaudeUsage(accessToken: string): Promise<ClaudeUsageReport function getCachedUsage(keyId: string): ClaudeUsageReport | null { try { const db = getDatabase(); - const row = db.query( - "SELECT report_json FROM usage_cache WHERE key_id = $keyId", - ).get({ $keyId: keyId }) as { report_json: string } | null; + const row = db + .query("SELECT report_json FROM usage_cache WHERE key_id = $keyId") + .get({ $keyId: keyId }) as { report_json: string } | null; if (!row) return null; return JSON.parse(row.report_json) as ClaudeUsageReport; } catch { @@ -559,4 +598,4 @@ export async function getAccountUsage(account: ClaudeAccount): Promise<ClaudeUsa return report; } return getCachedUsage(account.id); -}
\ No newline at end of file +} diff --git a/packages/core/src/credentials/index.ts b/packages/core/src/credentials/index.ts index 8cbedd9..e7f8a12 100644 --- a/packages/core/src/credentials/index.ts +++ b/packages/core/src/credentials/index.ts @@ -1,4 +1,12 @@ export { + deleteApiKey, + getApiKey, + listApiKeys, + resolveApiKey, + type StoredApiKey, + setApiKey, +} from "./api-keys.js"; +export { ANTHROPIC_MODELS_FALLBACK, buildBillingHeaderValue, type ClaudeAccount, @@ -7,11 +15,11 @@ export { type ClaudeUsageBucket, type ClaudeUsageReport, discoverClaudeAccounts, - getClaudeAccountsFromDB, fetchAnthropicModels, getAccountUsage, getAnthropicBetas, getAnthropicHeaders, + getClaudeAccountsFromDB, refreshAccountCredentials, refreshAccountCredentialsAsync, SYSTEM_IDENTITY, @@ -27,18 +35,10 @@ export { type OpencodeUsageReport, } from "./opencode.js"; export { - type StoredCredential, - importCredentialsFromFile, - getStoredCredentials, - updateStoredTokens, deleteStoredCredentials, + getStoredCredentials, + importCredentialsFromFile, listStoredCredentials, + type StoredCredential, + updateStoredTokens, } from "./store.js"; -export { - type StoredApiKey, - setApiKey, - getApiKey, - resolveApiKey, - deleteApiKey, - listApiKeys, -} from "./api-keys.js"; diff --git a/packages/core/src/credentials/opencode.ts b/packages/core/src/credentials/opencode.ts index b8a9d4d..d4d4851 100644 --- a/packages/core/src/credentials/opencode.ts +++ b/packages/core/src/credentials/opencode.ts @@ -70,9 +70,7 @@ function parseOcBucket( return { utilization, resetsAt }; } -export async function fetchOpencodeUsage( - keyId: string, -): Promise<OpencodeUsageReport | null> { +export async function fetchOpencodeUsage(keyId: string): Promise<OpencodeUsageReport | null> { const cookie = resolveApiKey("opencode-cookie"); const wsId = getWorkspaceId(keyId); @@ -96,10 +94,7 @@ export async function fetchOpencodeUsage( const html = await response.text(); // Auth redirect check - if ( - html.includes("/auth/authorize") || - html.includes('window.location="/auth/authorize"') - ) { + if (html.includes("/auth/authorize") || html.includes('window.location="/auth/authorize"')) { return null; } diff --git a/packages/core/src/credentials/store.ts b/packages/core/src/credentials/store.ts index 6c814f9..662b322 100644 --- a/packages/core/src/credentials/store.ts +++ b/packages/core/src/credentials/store.ts @@ -1,6 +1,6 @@ +import { existsSync, readFileSync } from "node:fs"; import { getDatabase } from "../db/index.js"; import type { ClaudeCredentials } from "./claude.js"; -import { existsSync, readFileSync } from "node:fs"; export interface StoredCredential { keyId: string; @@ -41,7 +41,8 @@ function parseCredentialsFile(raw: string): ClaudeCredentials | null { accessToken: creds.accessToken as string, refreshToken: creds.refreshToken as string, expiresAt: creds.expiresAt as number, - subscriptionType: typeof creds.subscriptionType === "string" ? creds.subscriptionType : undefined, + subscriptionType: + typeof creds.subscriptionType === "string" ? creds.subscriptionType : undefined, }; } @@ -62,7 +63,10 @@ export function importCredentialsFromFile( try { raw = readFileSync(filePath, "utf-8").trim(); } catch (e) { - return { success: false, error: `Failed to read file: ${e instanceof Error ? e.message : String(e)}` }; + return { + success: false, + error: `Failed to read file: ${e instanceof Error ? e.message : String(e)}`, + }; } if (!raw) { @@ -106,9 +110,11 @@ export function importCredentialsFromFile( */ export function getStoredCredentials(keyId: string): StoredCredential | null { const db = getDatabase(); - const row = db.query( - "SELECT key_id, provider, access_token, refresh_token, expires_at, subscription_type, source_file, imported_at, updated_at FROM credentials WHERE key_id = $keyId", - ).get({ $keyId: keyId }) as Record<string, unknown> | null; + const row = db + .query( + "SELECT key_id, provider, access_token, refresh_token, expires_at, subscription_type, source_file, imported_at, updated_at FROM credentials WHERE key_id = $keyId", + ) + .get({ $keyId: keyId }) as Record<string, unknown> | null; if (!row) return null; @@ -159,9 +165,11 @@ export function deleteStoredCredentials(keyId: string): void { */ export function listStoredCredentials(): StoredCredential[] { const db = getDatabase(); - const rows = db.query( - "SELECT key_id, provider, access_token, refresh_token, expires_at, subscription_type, source_file, imported_at, updated_at FROM credentials ORDER BY key_id", - ).all() as Array<Record<string, unknown>>; + const rows = db + .query( + "SELECT key_id, provider, access_token, refresh_token, expires_at, subscription_type, source_file, imported_at, updated_at FROM credentials ORDER BY key_id", + ) + .all() as Array<Record<string, unknown>>; return rows.map((row) => ({ keyId: row.key_id as string, |
