* add gated codex goals support * sync README SDK versions * bump claude agent sdk * add codex goals settings toggle * reuse status dashboard message and simplify settings actions * refine settings page UX * fix settings nav hash routing * add dashboard UX verification * refine dashboard settings and inbox UX * remove inbox top-level navigation * remove dashboard health top-level navigation * fix: allow runtime image attachment paths * feat: configure attachment allowlist dirs * fix: clean duplicate dashboard status messages * fix: poll dashboard duplicate cleanup between status updates * fix: delete dashboard duplicates on create * Refine settings IA with tabbed sections * Add read-only runtime inventory settings * Fix runtime inventory MCP JSON parsing * Add room skill settings inventory * Add room skill setting mutations
455 lines
13 KiB
TypeScript
455 lines
13 KiB
TypeScript
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<Record<string, unknown> | 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<string, unknown>;
|
|
}
|
|
|
|
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<Response | null> {
|
|
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) {
|
|
return jsonResponse({ error: errorMessage(err) }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
async function handleFastModeRoute(
|
|
request: Request,
|
|
jsonResponse: JsonResponse,
|
|
deps: SettingsRouteDependencies,
|
|
): Promise<Response | null> {
|
|
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<Response | null> {
|
|
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<Response | null> {
|
|
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<Response | null> {
|
|
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<Response> {
|
|
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<Response | null> {
|
|
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<Response | null> {
|
|
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<Response | null> {
|
|
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<Response | null> {
|
|
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<Response | null> {
|
|
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,
|
|
};
|