import { addClaudeAccountFromToken, getActiveCodexSettingsIndex, getCodexFeatures, getFastMode, getModelConfig, listClaudeAccounts, listCodexAccounts, refreshAllCodexAccounts, refreshCodexAccount, removeAccountDirectory, setActiveCodexSettingsIndex, updateCodexFeatures, updateFastMode, updateModelConfig, type ClaudeAccountSummary, type CodexFeatureSnapshot, type CodexAccountSummary, type FastModeSnapshot, type ModelConfigSnapshot, } from './settings-store.js'; import { getRuntimeInventory, type RuntimeInventorySnapshot, } from './runtime-inventory.js'; import { getRoomSkillSettings, RoomSkillSettingsError, updateRoomSkillSetting, type RoomSkillSettingsSnapshot, type RoomSkillSettingUpdateInput, } from './room-skill-settings.js'; import { checkMoaModel, getMoaSettings, updateMoaSettings, type MoaSettingsSnapshot, } from './settings-store-moa.js'; type JsonResponse = ( value: unknown, init?: ResponseInit, request?: Request, ) => Response; export interface SettingsRouteDependencies { addClaudeAccountFromToken: typeof addClaudeAccountFromToken; checkMoaModel: typeof checkMoaModel; getActiveCodexSettingsIndex: typeof getActiveCodexSettingsIndex; getCodexFeatures: typeof getCodexFeatures; getFastMode: typeof getFastMode; getModelConfig: typeof getModelConfig; getMoaSettings: typeof getMoaSettings; getRuntimeInventory: typeof getRuntimeInventory; getRoomSkillSettings: typeof getRoomSkillSettings; listClaudeAccounts: typeof listClaudeAccounts; listCodexAccounts: typeof listCodexAccounts; refreshAllCodexAccounts: typeof refreshAllCodexAccounts; refreshCodexAccount: typeof refreshCodexAccount; removeAccountDirectory: typeof removeAccountDirectory; setActiveCodexSettingsIndex: typeof setActiveCodexSettingsIndex; updateCodexFeatures: typeof updateCodexFeatures; updateFastMode: typeof updateFastMode; updateModelConfig: typeof updateModelConfig; updateMoaSettings: typeof updateMoaSettings; updateRoomSkillSetting: typeof updateRoomSkillSetting; } interface SettingsRouteContext { url: URL; request: Request; jsonResponse: JsonResponse; deps?: SettingsRouteDependencies; } const defaultSettingsRouteDependencies: SettingsRouteDependencies = { addClaudeAccountFromToken, checkMoaModel, getActiveCodexSettingsIndex, getCodexFeatures, getFastMode, getModelConfig, getMoaSettings, getRuntimeInventory, getRoomSkillSettings, listClaudeAccounts, listCodexAccounts, refreshAllCodexAccounts, refreshCodexAccount, removeAccountDirectory, setActiveCodexSettingsIndex, updateCodexFeatures, updateFastMode, updateModelConfig, updateMoaSettings, updateRoomSkillSetting, }; function readMethod(method: string): boolean { return method === 'GET' || method === 'HEAD'; } async function readJsonObject( request: Request, jsonResponse: JsonResponse, ): Promise | Response> { let body: unknown; try { body = await request.json(); } catch { return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 }); } if (!body || typeof body !== 'object' || Array.isArray(body)) { return jsonResponse( { error: 'Body must be a JSON object' }, { status: 400 }, ); } return body as Record; } function errorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } function methodNotAllowed( jsonResponse: JsonResponse, allowed: string[], ): Response { return jsonResponse( { error: 'Method not allowed' }, { status: 405, headers: { Allow: allowed.join(', ') }, }, ); } async function handleModelSettingsRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (readMethod(request.method)) return jsonResponse(deps.getModelConfig()); if (request.method !== 'PUT' && request.method !== 'PATCH') return null; const body = await readJsonObject(request, jsonResponse); if (body instanceof Response) return body; try { return jsonResponse(deps.updateModelConfig(body)); } catch (err) { const message = errorMessage(err); const status = message.includes('not supported') ? 400 : 500; return jsonResponse({ error: message }, { status }); } } async function handleFastModeRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (readMethod(request.method)) return jsonResponse(deps.getFastMode()); if (request.method !== 'PUT' && request.method !== 'PATCH') return null; const body = await readJsonObject(request, jsonResponse); if (body instanceof Response) return body; try { return jsonResponse(deps.updateFastMode(body)); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 500 }); } } async function handleCodexFeaturesRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (readMethod(request.method)) return jsonResponse(deps.getCodexFeatures()); if (request.method !== 'PUT' && request.method !== 'PATCH') return null; const body = await readJsonObject(request, jsonResponse); if (body instanceof Response) return body; try { return jsonResponse(deps.updateCodexFeatures(body)); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 500 }); } } async function handleMoaSettingsRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (readMethod(request.method)) return jsonResponse(deps.getMoaSettings()); if (request.method !== 'PUT' && request.method !== 'PATCH') return null; const body = await readJsonObject(request, jsonResponse); if (body instanceof Response) return body; try { return jsonResponse(deps.updateMoaSettings(body)); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 500 }); } } async function handleMoaCheckRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (request.method !== 'POST') return null; const body = await readJsonObject(request, jsonResponse); if (body instanceof Response) return body; const name = typeof body.name === 'string' ? body.name.trim() : ''; if (!name) return jsonResponse({ error: 'name is required' }, { status: 400 }); try { return jsonResponse({ ok: true, status: await deps.checkMoaModel(name) }); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 400 }); } } function handleAccountsRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Response | null { if (!readMethod(request.method)) return null; return jsonResponse({ claude: deps.listClaudeAccounts(), codex: deps.listCodexAccounts(), codexCurrentIndex: deps.getActiveCodexSettingsIndex(), }); } function handleRuntimeInventoryRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Response | null { if (!readMethod(request.method)) return null; return jsonResponse(deps.getRuntimeInventory()); } async function handleRoomSkillsRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (readMethod(request.method)) { return jsonResponse(deps.getRoomSkillSettings()); } if (request.method !== 'PATCH' && request.method !== 'PUT') { return methodNotAllowed(jsonResponse, ['GET', 'HEAD', 'PATCH', 'PUT']); } const body = await readJsonObject(request, jsonResponse); if (body instanceof Response) return body; const input: RoomSkillSettingUpdateInput = { roomJid: typeof body.roomJid === 'string' ? body.roomJid : '', agentType: typeof body.agentType === 'string' ? body.agentType : '', skillId: typeof body.skillId === 'string' ? body.skillId : '', enabled: body.enabled === true, }; if (typeof body.enabled !== 'boolean') { return jsonResponse( { error: 'enabled must be a boolean' }, { status: 400 }, ); } try { return jsonResponse(deps.updateRoomSkillSetting(input)); } catch (err) { if (err instanceof RoomSkillSettingsError) { return jsonResponse({ error: err.message }, { status: err.status }); } return jsonResponse({ error: errorMessage(err) }, { status: 500 }); } } async function handleClaudeAccountAddRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (request.method !== 'POST') return null; let body: { token?: unknown }; try { body = (await request.json()) as { token?: unknown }; } catch { return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 }); } const token = typeof body?.token === 'string' ? body.token.trim() : ''; if (!token) return jsonResponse({ error: 'token is required' }, { status: 400 }); try { const result = deps.addClaudeAccountFromToken(token); return jsonResponse({ ok: true, ...result }); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 400 }); } } function handleAccountDeleteRoute( request: Request, accountMatch: RegExpMatchArray, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Response | null { if (request.method !== 'DELETE') return null; const provider = accountMatch[1] as 'claude' | 'codex'; const index = Number.parseInt(accountMatch[2], 10); try { deps.removeAccountDirectory(provider, index); return jsonResponse({ ok: true, provider, index }); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 400 }); } } async function handleCodexRefreshRoute( request: Request, refreshMatch: RegExpMatchArray, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (request.method !== 'POST') return null; const index = Number.parseInt(refreshMatch[1], 10); try { const updated = await deps.refreshCodexAccount(index); return jsonResponse({ ok: true, account: updated }); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 400 }); } } async function handleCodexRefreshAllRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (request.method !== 'POST') return null; try { const result = await deps.refreshAllCodexAccounts(); return jsonResponse({ ok: true, ...result }); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 500 }); } } async function handleCodexCurrentRoute( request: Request, jsonResponse: JsonResponse, deps: SettingsRouteDependencies, ): Promise { if (request.method !== 'PUT') return null; let body: { index?: unknown }; try { body = (await request.json()) as { index?: unknown }; } catch { return jsonResponse({ error: 'Invalid JSON body' }, { status: 400 }); } const idx = typeof body?.index === 'number' ? body.index : Number.NaN; if (!Number.isInteger(idx)) { return jsonResponse({ error: 'index must be an integer' }, { status: 400 }); } try { deps.setActiveCodexSettingsIndex(idx); return jsonResponse({ ok: true, codexCurrentIndex: deps.getActiveCodexSettingsIndex(), }); } catch (err) { return jsonResponse({ error: errorMessage(err) }, { status: 400 }); } } export async function handleSettingsRoute({ url, request, jsonResponse, deps = defaultSettingsRouteDependencies, }: SettingsRouteContext): Promise { if (url.pathname === '/api/settings/accounts') { return handleAccountsRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/models') { return handleModelSettingsRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/runtime-inventory') { return handleRuntimeInventoryRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/room-skills') { return handleRoomSkillsRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/fast-mode') { return handleFastModeRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/codex-features') { return handleCodexFeaturesRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/moa') { return handleMoaSettingsRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/moa/check') { return handleMoaCheckRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/accounts/claude') { return handleClaudeAccountAddRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/accounts/codex/refresh-all') { return handleCodexRefreshAllRoute(request, jsonResponse, deps); } if (url.pathname === '/api/settings/accounts/codex/current') { return handleCodexCurrentRoute(request, jsonResponse, deps); } const accountMatch = url.pathname.match( /^\/api\/settings\/accounts\/(claude|codex)\/(\d+)$/, ); if (accountMatch) { return handleAccountDeleteRoute(request, accountMatch, jsonResponse, deps); } const refreshMatch = url.pathname.match( /^\/api\/settings\/accounts\/codex\/(\d+)\/refresh$/, ); if (refreshMatch) { return handleCodexRefreshRoute(request, refreshMatch, jsonResponse, deps); } return null; } export type { ClaudeAccountSummary, CodexAccountSummary, CodexFeatureSnapshot, FastModeSnapshot, ModelConfigSnapshot, MoaSettingsSnapshot, RuntimeInventorySnapshot, RoomSkillSettingsSnapshot, };