Skip to content

Commit b0ef23d

Browse files
lanmowerclaude
andcommitted
refactor: extract runs/scripts/agent-auth/auth-config HTTP routes from server.js
Move runsMatch/runsSearchMatch/runById handlers to lib/routes-runs.js (157L), scripts/cancel/resume/inject handlers to lib/routes-scripts.js (136L), agentAuth/agentUpdate to lib/routes-agent-actions.js (118L), and auth/configs/save-config to lib/routes-auth-config.js (30L, uses server.js getProviderConfigs/saveProviderConfig deps). server.js: 2406L → 1399L total (-1007L across both extractions). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8e7bde0 commit b0ef23d

6 files changed

Lines changed: 455 additions & 744 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Refactor
44
- Extract message/stream/queue routes (messagesMatch, streamMatch, queueMatch handlers) to lib/routes-messages.js (140L) and session/chunk/full/execution routes to lib/routes-sessions.js (145L); server.js reduced from 2406L to 2127L; both files ≤200L; wired via _messagesRoutes._match and _sessionsRoutes._match in request handler
5+
- Extract runs/scripts/agent-auth/auth-config HTTP routes from server.js to lib/routes-runs.js (157L), lib/routes-scripts.js (136L), lib/routes-agent-actions.js (118L), lib/routes-auth-config.js (30L); routes-auth-config uses getProviderConfigs/saveProviderConfig from server.js deps (no duplication); server.js reduced from 2406L to 1399L total (-1007L)
56
- Extract processMessageWithStreaming (539L), scheduleRetry, drainMessageQueue, and parseRateLimitResetTime from server.js into lib/process-message.js (127L, createProcessMessage factory), lib/stream-event-handler.js (116L, createEventHandler), lib/message-queue.js (63L, createMessageQueue), lib/process-message-rate-limit.js (19L); all files ≤200L; server.js reduced by ~660L and imports/wires all factories after broadcastSync is created
67
- refactor: extract broadcastSync to lib/broadcast.js (createBroadcast factory) and recovery functions to lib/recovery.js (createRecovery factory); server.js reduced from 3419L to 3226L
78
- refactor: remove JSDoc and standalone code comments from scripts/patch-fsbrowse.js; reduce from 229L to 200L

lib/routes-agent-actions.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import os from 'os';
2+
import { spawn } from 'child_process';
3+
4+
export function register(deps) {
5+
const { sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir } = deps;
6+
7+
const routes = {};
8+
9+
routes['_match'] = (method, pathOnly) => {
10+
let m;
11+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/))) return (req, res) => handleAgentAuth(req, res, m[1]);
12+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/agents\/([^/]+)\/update$/))) return (req, res) => handleAgentUpdate(req, res, m[1]);
13+
return null;
14+
};
15+
16+
async function handleAgentAuth(req, res, agentId) {
17+
const agent = discoveredAgents.find(a => a.id === agentId);
18+
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
19+
20+
if (agentId === 'codex' || agentId === 'cli-codex') {
21+
try {
22+
const result = await startCodexOAuth(req, { PORT, BASE_URL });
23+
const conversationId = '__agent_auth__';
24+
broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
25+
broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening OpenAI OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
26+
const pollId = setInterval(() => {
27+
const state = getCodexOAuthState();
28+
if (state.status === 'success') {
29+
clearInterval(pollId);
30+
const email = state.email || '';
31+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
32+
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
33+
} else if (state.status === 'error') {
34+
clearInterval(pollId);
35+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${state.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
36+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: state.error, timestamp: Date.now() });
37+
}
38+
}, 1000);
39+
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
40+
sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
41+
} catch (e) {
42+
console.error('[codex-oauth] /api/agents/codex/auth failed:', e);
43+
sendJSON(req, res, 500, { error: e.message });
44+
}
45+
return;
46+
}
47+
48+
if (agentId === 'gemini') {
49+
try {
50+
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
51+
const conversationId = '__agent_auth__';
52+
broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
53+
broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening Google OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
54+
const pollId = setInterval(() => {
55+
const state = getGeminiOAuthState();
56+
if (state.status === 'success') {
57+
clearInterval(pollId);
58+
const email = state.email || '';
59+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
60+
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
61+
} else if (state.status === 'error') {
62+
clearInterval(pollId);
63+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${state.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
64+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: state.error, timestamp: Date.now() });
65+
}
66+
}, 1000);
67+
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
68+
sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
69+
} catch (e) {
70+
console.error('[gemini-oauth] /api/agents/gemini/auth failed:', e);
71+
sendJSON(req, res, 500, { error: e.message });
72+
}
73+
return;
74+
}
75+
76+
const authCommands = {
77+
'claude-code': { cmd: 'claude', args: ['setup-token'] },
78+
'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
79+
};
80+
const authCmd = authCommands[agentId];
81+
if (!authCmd) { sendJSON(req, res, 400, { error: 'No auth command for this agent' }); return; }
82+
const conversationId = '__agent_auth__';
83+
if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Auth process already running' }); return; }
84+
const child = spawn(authCmd.cmd, authCmd.args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, FORCE_COLOR: '1' }, shell: os.platform() === 'win32' });
85+
activeScripts.set(conversationId, { process: child, script: 'auth-' + agentId, startTime: Date.now() });
86+
broadcastSync({ type: 'script_started', conversationId, script: 'auth-' + agentId, agentId, timestamp: Date.now() });
87+
const onData = (stream) => (chunk) => broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
88+
child.stdout.on('data', onData('stdout'));
89+
child.stderr.on('data', onData('stderr'));
90+
child.stdout.on('error', () => {});
91+
child.stderr.on('error', () => {});
92+
child.on('error', (err) => { activeScripts.delete(conversationId); broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() }); });
93+
child.on('close', (code) => { activeScripts.delete(conversationId); broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() }); });
94+
sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
95+
}
96+
97+
async function handleAgentUpdate(req, res, agentId) {
98+
const updateCommands = { 'claude-code': { cmd: 'claude', args: ['update', '--yes'] } };
99+
const updateCmd = updateCommands[agentId];
100+
if (!updateCmd) { sendJSON(req, res, 400, { error: 'No update command for this agent' }); return; }
101+
const conversationId = '__agent_update__';
102+
if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Update already running' }); return; }
103+
const child = spawn(updateCmd.cmd, updateCmd.args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, FORCE_COLOR: '1' }, shell: os.platform() === 'win32' });
104+
activeScripts.set(conversationId, { process: child, script: 'update-' + agentId, startTime: Date.now() });
105+
broadcastSync({ type: 'script_started', conversationId, script: 'update-' + agentId, agentId, timestamp: Date.now() });
106+
const onData = (stream) => (chunk) => broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
107+
child.stdout.on('data', onData('stdout'));
108+
child.stderr.on('data', onData('stderr'));
109+
child.stdout.on('error', () => {});
110+
child.stderr.on('error', () => {});
111+
child.on('error', (err) => { activeScripts.delete(conversationId); broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() }); });
112+
child.on('close', (code) => { activeScripts.delete(conversationId); modelCache.delete(agentId); broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() }); });
113+
sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
114+
}
115+
116+
return routes;
117+
}

lib/routes-auth-config.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function register(deps) {
2+
const { sendJSON, parseBody, getProviderConfigs, saveProviderConfig } = deps;
3+
4+
const routes = {};
5+
6+
routes['GET /api/auth/configs'] = (req, res) => {
7+
sendJSON(req, res, 200, getProviderConfigs());
8+
};
9+
10+
routes['POST /api/auth/save-config'] = async (req, res) => {
11+
try {
12+
const body = await parseBody(req);
13+
const { providerId, apiKey, defaultModel } = body || {};
14+
if (typeof providerId !== 'string' || !providerId.length || providerId.length > 100) { sendJSON(req, res, 400, { error: 'Invalid providerId' }); return; }
15+
if (typeof apiKey !== 'string' || !apiKey.length || apiKey.length > 10000) { sendJSON(req, res, 400, { error: 'Invalid apiKey' }); return; }
16+
if (defaultModel !== undefined && (typeof defaultModel !== 'string' || defaultModel.length > 200)) { sendJSON(req, res, 400, { error: 'Invalid defaultModel' }); return; }
17+
const configPath = saveProviderConfig(providerId, apiKey, defaultModel || '');
18+
sendJSON(req, res, 200, { success: true, path: configPath });
19+
} catch (err) {
20+
sendJSON(req, res, 400, { error: err.message });
21+
}
22+
};
23+
24+
routes['_match'] = (method, pathOnly) => {
25+
const key = `${method} ${pathOnly}`;
26+
return routes[key] || null;
27+
};
28+
29+
return routes;
30+
}

lib/routes-runs.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import os from 'os';
2+
3+
export function register(deps) {
4+
const { sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD } = deps;
5+
6+
const routes = {};
7+
8+
routes['_match'] = (method, pathOnly) => {
9+
const key = `${method} ${pathOnly}`;
10+
if (routes[key]) return routes[key];
11+
let m;
12+
if ((m = pathOnly.match(/^\/api\/runs\/([^/]+)$/))) return (req, res) => handleRunById(req, res, m[1]);
13+
if (method === 'GET' && (m = pathOnly.match(/^\/api\/runs\/([^/]+)\/wait$/))) return (req, res) => handleRunWait(req, res, m[1]);
14+
if (method === 'GET' && (m = pathOnly.match(/^\/api\/runs\/([^/]+)\/stream$/))) return (req, res) => { res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' })); };
15+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/))) return (req, res) => handleRunCancel(req, res, m[1]);
16+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/cancel$/))) return (req, res) => handleThreadRunCancel(req, res, m[1], m[2]);
17+
if (method === 'GET' && (m = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/wait$/))) return (req, res) => handleThreadRunWait(req, res, m[1], m[2]);
18+
return null;
19+
};
20+
21+
routes['POST /api/runs'] = async (req, res) => {
22+
let body = '';
23+
for await (const chunk of req) { body += chunk; }
24+
let parsed = {};
25+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
26+
const { input, agentId } = parsed;
27+
if (!input) { sendJSON(req, res, 400, { error: 'Missing input in request body' }); return; }
28+
const resolvedAgentId = agentId || 'claude-code';
29+
const resolvedModel = parsed.model || null;
30+
const cwd = parsed.workingDirectory || STARTUP_CWD;
31+
const thread = queries.createConversation(resolvedAgentId, 'Stateless Run', cwd);
32+
const session = queries.createSession(thread.id, resolvedAgentId, 'pending');
33+
const content = typeof input === 'string' ? input : JSON.stringify(input);
34+
const message = queries.createMessage(thread.id, 'user', content);
35+
processMessageWithStreaming(thread.id, message.id, session.id, content, resolvedAgentId, resolvedModel);
36+
sendJSON(req, res, 200, { id: session.id, status: 'pending', started_at: session.started_at, agentId: resolvedAgentId });
37+
};
38+
39+
routes['POST /api/runs/search'] = async (req, res) => {
40+
const sessions = queries.getAllSessions();
41+
const runs = sessions.slice(0, 50).map(s => ({ id: s.id, status: s.status, started_at: s.started_at, completed_at: s.completed_at, agentId: s.agentId, input: null, output: null })).reverse();
42+
sendJSON(req, res, 200, runs);
43+
};
44+
45+
routes['POST /api/runs/stream'] = (req, res) => { res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' })); };
46+
47+
routes['POST /api/runs/wait'] = async (req, res) => {
48+
const body = await parseBody(req);
49+
const { agent_id, input, config } = body;
50+
if (!agent_id) { sendJSON(req, res, 422, { error: 'agent_id is required' }); return; }
51+
const agent = discoveredAgents.find(a => a.id === agent_id);
52+
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
53+
const run = queries.createRun(agent_id, null, input, config);
54+
sendJSON(req, res, 200, run);
55+
};
56+
57+
async function handleRunById(req, res, runId) {
58+
if (req.method === 'GET') {
59+
const run = queries.getRun(runId);
60+
if (!run) { sendJSON(req, res, 404, { error: 'Run not found' }); return; }
61+
sendJSON(req, res, 200, run);
62+
return;
63+
}
64+
if (req.method === 'POST') {
65+
const run = queries.getRun(runId);
66+
if (!run) { sendJSON(req, res, 404, { error: 'Run not found' }); return; }
67+
if (run.status !== 'pending') { sendJSON(req, res, 409, { error: 'Run is not resumable' }); return; }
68+
sendJSON(req, res, 200, run);
69+
return;
70+
}
71+
if (req.method === 'DELETE') {
72+
try { queries.deleteRun(runId); res.writeHead(204); res.end(); } catch { sendJSON(req, res, 404, { error: 'Run not found' }); }
73+
}
74+
}
75+
76+
async function handleRunWait(req, res, runId) {
77+
const run = queries.getRun(runId);
78+
if (!run) { sendJSON(req, res, 404, { error: 'Run not found' }); return; }
79+
const startTime = Date.now();
80+
const poll = setInterval(() => {
81+
const cur = queries.getRun(runId);
82+
const done = cur && ['success', 'error', 'cancelled'].includes(cur.status);
83+
if (done) { clearInterval(poll); sendJSON(req, res, 200, cur); }
84+
else if (Date.now() - startTime > 30000) { clearInterval(poll); sendJSON(req, res, 408, { error: 'Run still pending after 30s', run_id: runId, status: cur?.status || run.status }); }
85+
}, 500);
86+
req.on('close', () => clearInterval(poll));
87+
}
88+
89+
async function handleRunCancel(req, res, runId) {
90+
try {
91+
const run = queries.getRun(runId);
92+
if (!run) { sendJSON(req, res, 404, { error: 'Run not found' }); return; }
93+
if (['success', 'error', 'cancelled'].includes(run.status)) { sendJSON(req, res, 409, { error: 'Run already completed or cancelled' }); return; }
94+
const cancelled = queries.cancelRun(runId);
95+
const threadId = run.thread_id;
96+
if (threadId) {
97+
const execution = activeExecutions.get(threadId);
98+
if (execution?.pid) {
99+
try { process.kill(-execution.pid, 'SIGTERM'); } catch { try { process.kill(execution.pid, 'SIGTERM'); } catch {} }
100+
setTimeout(() => { try { process.kill(-execution.pid, 'SIGKILL'); } catch { try { process.kill(execution.pid, 'SIGKILL'); } catch {} } }, 3000);
101+
}
102+
if (execution?.sessionId) queries.updateSession(execution.sessionId, { status: 'error', error: 'Cancelled by user', completed_at: Date.now() });
103+
activeExecutions.delete(threadId);
104+
queries.setIsStreaming(threadId, false);
105+
broadcastSync({ type: 'streaming_cancelled', sessionId: execution?.sessionId || runId, conversationId: threadId, runId, timestamp: Date.now() });
106+
}
107+
sendJSON(req, res, 200, cancelled);
108+
} catch (err) {
109+
if (err.message === 'Run not found') sendJSON(req, res, 404, { error: err.message });
110+
else if (err.message.includes('already completed')) sendJSON(req, res, 409, { error: err.message });
111+
else sendJSON(req, res, 500, { error: err.message });
112+
}
113+
}
114+
115+
async function handleThreadRunCancel(req, res, threadId, runId) {
116+
try {
117+
const run = queries.getRun(runId);
118+
if (!run) { sendJSON(req, res, 404, { error: 'Run not found' }); return; }
119+
if (run.thread_id !== threadId) { sendJSON(req, res, 400, { error: 'Run does not belong to specified thread' }); return; }
120+
if (['success', 'error', 'cancelled'].includes(run.status)) { sendJSON(req, res, 409, { error: 'Run already completed or cancelled' }); return; }
121+
const cancelled = queries.cancelRun(runId);
122+
const execution = activeExecutions.get(threadId);
123+
if (execution?.pid) {
124+
try { process.kill(-execution.pid, 'SIGTERM'); } catch { try { process.kill(execution.pid, 'SIGTERM'); } catch {} }
125+
setTimeout(() => { try { process.kill(-execution.pid, 'SIGKILL'); } catch { try { process.kill(execution.pid, 'SIGKILL'); } catch {} } }, 3000);
126+
}
127+
if (execution?.sessionId) queries.updateSession(execution.sessionId, { status: 'error', error: 'Cancelled by user', completed_at: Date.now() });
128+
activeExecutions.delete(threadId);
129+
activeProcessesByRunId.delete(runId);
130+
queries.setIsStreaming(threadId, false);
131+
broadcastSync({ type: 'run_cancelled', runId, threadId, sessionId: execution?.sessionId, timestamp: Date.now() });
132+
broadcastSync({ type: 'streaming_cancelled', sessionId: execution?.sessionId || runId, conversationId: threadId, runId, timestamp: Date.now() });
133+
sendJSON(req, res, 200, cancelled);
134+
} catch (err) {
135+
if (err.message === 'Run not found') sendJSON(req, res, 404, { error: err.message });
136+
else if (err.message.includes('already completed')) sendJSON(req, res, 409, { error: err.message });
137+
else sendJSON(req, res, 500, { error: err.message });
138+
}
139+
}
140+
141+
async function handleThreadRunWait(req, res, threadId, runId) {
142+
const run = queries.getRun(runId);
143+
if (!run) { sendJSON(req, res, 404, { error: 'Run not found' }); return; }
144+
if (run.thread_id !== threadId) { sendJSON(req, res, 400, { error: 'Run does not belong to specified thread' }); return; }
145+
const startTime = Date.now();
146+
const poll = setInterval(() => {
147+
const cur = queries.getRun(runId);
148+
const done = cur && ['success', 'error', 'cancelled'].includes(cur.status);
149+
if (done) { clearInterval(poll); sendJSON(req, res, 200, cur); }
150+
else if (Date.now() - startTime > 30000) { clearInterval(poll); sendJSON(req, res, 408, { error: 'Run still pending after 30s', run_id: runId, status: cur?.status || run.status }); }
151+
}, 500);
152+
req.on('close', () => clearInterval(poll));
153+
}
154+
155+
return routes;
156+
}

0 commit comments

Comments
 (0)