diff --git a/src/app/api/settings/app/route.ts b/src/app/api/settings/app/route.ts new file mode 100644 index 0000000..47ccfdd --- /dev/null +++ b/src/app/api/settings/app/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getSetting, setSetting } from '@/lib/db'; + +/** + * CodePilot app-level settings (stored in SQLite, separate from ~/.claude/settings.json). + * Used for API configuration (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, etc.) + */ + +const ALLOWED_KEYS = [ + 'anthropic_auth_token', + 'anthropic_base_url', +]; + +export async function GET() { + try { + const result: Record = {}; + for (const key of ALLOWED_KEYS) { + const value = getSetting(key); + if (value !== undefined) { + // Mask token for security (only return last 8 chars) + if (key === 'anthropic_auth_token' && value.length > 8) { + result[key] = '***' + value.slice(-8); + } else { + result[key] = value; + } + } + } + return NextResponse.json({ settings: result }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to read app settings'; + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { settings } = body; + + if (!settings || typeof settings !== 'object') { + return NextResponse.json({ error: 'Invalid settings data' }, { status: 400 }); + } + + for (const [key, value] of Object.entries(settings)) { + if (!ALLOWED_KEYS.includes(key)) continue; + const strValue = String(value ?? '').trim(); + if (strValue) { + // Don't overwrite token if user sent the masked version back + if (key === 'anthropic_auth_token' && strValue.startsWith('***')) { + continue; + } + setSetting(key, strValue); + } else { + // Empty value = remove the setting + setSetting(key, ''); + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to save app settings'; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 4f0bf98..d60e5bf 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -60,6 +60,112 @@ export default function SettingsPage() { ); } +// --- API Configuration Section (CodePilot app settings, stored in SQLite) --- +function ApiConfigSection() { + const [token, setToken] = useState(""); + const [baseUrl, setBaseUrl] = useState(""); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [status, setStatus] = useState<"idle" | "saved" | "error">("idle"); + + useEffect(() => { + fetch("/api/settings/app") + .then((r) => r.json()) + .then((data) => { + const s = data.settings || {}; + setToken(s.anthropic_auth_token || ""); + setBaseUrl(s.anthropic_base_url || ""); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + const handleSave = async () => { + setSaving(true); + setStatus("idle"); + try { + const res = await fetch("/api/settings/app", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + settings: { + anthropic_auth_token: token, + anthropic_base_url: baseUrl, + }, + }), + }); + if (res.ok) { + setStatus("saved"); + setTimeout(() => setStatus("idle"), 2000); + } else { + setStatus("error"); + } + } catch { + setStatus("error"); + } finally { + setSaving(false); + } + }; + + if (loading) return null; + + return ( +
+
+ +

+ Optional. Configure a custom Anthropic-compatible API for Claude Code. + Leave empty to use the default authentication (claude login / shell environment). +

+
+
+
+ + setBaseUrl(e.target.value)} + className="mt-1 font-mono text-sm" + /> +
+
+ + setToken(e.target.value)} + className="mt-1 font-mono text-sm" + /> +
+
+
+ + {status === "saved" && ( + Saved + )} + {status === "error" && ( + Failed to save + )} +
+
+ ); +} + +// --- Claude CLI Settings Section (manages ~/.claude/settings.json) --- function SettingsPageInner() { const [settings, setSettings] = useState({}); const [originalSettings, setOriginalSettings] = useState({}); @@ -168,12 +274,14 @@ function SettingsPageInner() {

Settings

- Manage your Claude CLI settings (~/.claude/settings.json) + Manage CodePilot and Claude CLI settings

-
+
+ + {loading ? (
diff --git a/src/lib/claude-client.ts b/src/lib/claude-client.ts index 76ff14a..09b4d95 100644 --- a/src/lib/claude-client.ts +++ b/src/lib/claude-client.ts @@ -14,6 +14,7 @@ import type { } from '@anthropic-ai/claude-agent-sdk'; import type { ClaudeStreamOptions, SSEEvent, TokenUsage, MCPServerConfig, PermissionRequestEvent } from '@/types'; import { registerPendingPermission } from './permission-registry'; +import { getSetting } from './db'; import { execFileSync } from 'child_process'; import os from 'os'; import path from 'path'; @@ -132,15 +133,26 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream({ async start(controller) { try { + // Build env for the Claude Code subprocess. + // Start with process.env (includes user shell env from Electron's loadUserShellEnv). + // Then overlay any API config the user set in CodePilot settings (optional). + const sdkEnv: Record = { ...process.env as Record }; + + const appToken = getSetting('anthropic_auth_token'); + const appBaseUrl = getSetting('anthropic_base_url'); + if (appToken) { + sdkEnv.ANTHROPIC_AUTH_TOKEN = appToken; + } + if (appBaseUrl) { + sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl; + } + const queryOptions: Options = { cwd: workingDirectory || process.cwd(), abortController, includePartialMessages: true, permissionMode: (permissionMode as Options['permissionMode']) || 'acceptEdits', - // Explicitly pass the full process.env to the Claude Code subprocess. - // In packaged Electron, this includes user shell env vars (API keys, etc.) - // loaded by the main process via loadUserShellEnv(). - env: process.env as Record, + env: sdkEnv, }; // Find claude binary for packaged app where PATH is limited