agentgui 1.0.838 → 1.0.839

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -27,6 +27,12 @@ import { register as registerThreadRoutes } from './lib/routes-threads.js';
27
27
  import { register as registerDebugRoutes } from './lib/routes-debug.js';
28
28
  import { register as registerConvRoutes } from './lib/routes-conversations.js';
29
29
  import { register as registerAgentRoutes } from './lib/routes-agents.js';
30
+ import { register as registerMessagesRoutes } from './lib/routes-messages.js';
31
+ import { register as registerSessionsRoutes } from './lib/routes-sessions.js';
32
+ import { register as registerRunsRoutes } from './lib/routes-runs.js';
33
+ import { register as registerScriptsRoutes } from './lib/routes-scripts.js';
34
+ import { register as registerAgentActionsRoutes } from './lib/routes-agent-actions.js';
35
+ import { register as registerAuthConfigRoutes } from './lib/routes-auth-config.js';
30
36
  import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
31
37
  import { WSOptimizer } from './lib/ws-optimizer.js';
32
38
  import { WsRouter } from './lib/ws-protocol.js';
@@ -550,1049 +556,29 @@ const server = http.createServer(async (req, res) => {
550
556
  const convHandler = _convRoutes._match(req.method, pathOnly);
551
557
  if (convHandler) { await convHandler(req, res); return; }
552
558
 
553
- const messagesMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages$/);
554
- if (messagesMatch) {
555
- if (req.method === 'GET') {
556
- const url = new URL(req.url, 'http://localhost');
557
- const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 500);
558
- const offset = Math.max(parseInt(url.searchParams.get('offset') || '0'), 0);
559
- const result = queries.getPaginatedMessages(messagesMatch[1], limit, offset);
560
- sendJSON(req, res, 200, result);
561
- return;
562
- }
563
-
564
- if (req.method === 'POST') {
565
- const conversationId = messagesMatch[1];
566
- const conv = queries.getConversation(conversationId);
567
- if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
568
- const body = await parseBody(req);
569
- const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
570
- const model = body.model || conv.model || null;
571
- const subAgent = body.subAgent || conv.subAgent || null;
572
- const idempotencyKey = body.idempotencyKey || null;
573
- const message = queries.createMessage(conversationId, 'user', body.content, idempotencyKey);
574
- queries.createEvent('message.created', { role: 'user', messageId: message.id }, conversationId);
575
- broadcastSync({ type: 'message_created', conversationId, message, timestamp: Date.now() });
576
-
577
- if (activeExecutions.has(conversationId)) {
578
- if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
579
- messageQueues.get(conversationId).push({ content: body.content, agentId, model, messageId: message.id, subAgent });
580
- const queueLength = messageQueues.get(conversationId).length;
581
- broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: message.id, timestamp: Date.now() });
582
- sendJSON(req, res, 200, { message, queued: true, queuePosition: queueLength, idempotencyKey });
583
- return;
584
- }
585
-
586
- const session = queries.createSession(conversationId);
587
- queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, conversationId, session.id);
588
-
589
- activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
590
- queries.setIsStreaming(conversationId, true);
591
-
592
- broadcastSync({
593
- type: 'streaming_start',
594
- sessionId: session.id,
595
- conversationId,
596
- messageId: message.id,
597
- agentId,
598
- timestamp: Date.now()
599
- });
600
-
601
- sendJSON(req, res, 201, { message, session, idempotencyKey });
602
-
603
- processMessageWithStreaming(conversationId, message.id, session.id, body.content, agentId, model, subAgent)
604
- .catch(err => {
605
- console.error(`[messages] Uncaught error for conv ${conversationId}:`, err.message);
606
- debugLog(`[messages] Uncaught error: ${err.message}`);
607
- logError('processMessageWithStreaming', err, { convId: conversationId });
608
- });
609
- return;
610
- }
611
- }
612
-
613
- const streamMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/stream$/);
614
- if (streamMatch && req.method === 'POST') {
615
- const conversationId = streamMatch[1];
616
- const body = await parseBody(req);
617
- const conv = queries.getConversation(conversationId);
618
- if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
619
-
620
- const prompt = body.content || body.message || '';
621
- const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
622
- const model = body.model || conv.model || null;
623
- const subAgent = body.subAgent || conv.subAgent || null;
624
-
625
- const userMessage = queries.createMessage(conversationId, 'user', prompt);
626
- queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
627
-
628
- broadcastSync({ type: 'message_created', conversationId, message: userMessage, timestamp: Date.now() });
629
-
630
- if (activeExecutions.has(conversationId)) {
631
- debugLog(`[stream] Conversation ${conversationId} is busy, queuing message`);
632
- if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
633
- messageQueues.get(conversationId).push({ content: prompt, agentId, model, messageId: userMessage.id, subAgent });
634
-
635
- const queueLength = messageQueues.get(conversationId).length;
636
- broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: userMessage.id, timestamp: Date.now() });
637
-
638
- sendJSON(req, res, 200, { message: userMessage, queued: true, queuePosition: queueLength });
639
- return;
640
- }
641
-
642
- const session = queries.createSession(conversationId);
643
- queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
644
-
645
- activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
646
- queries.setIsStreaming(conversationId, true);
647
-
648
- broadcastSync({
649
- type: 'streaming_start',
650
- sessionId: session.id,
651
- conversationId,
652
- messageId: userMessage.id,
653
- agentId,
654
- timestamp: Date.now()
655
- });
656
-
657
- sendJSON(req, res, 200, { message: userMessage, session, streamId: session.id });
658
-
659
- processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId, model, subAgent)
660
- .catch(err => debugLog(`[stream] Uncaught error: ${err.stack || err.message}`));
661
- return;
662
- }
663
-
664
- const queueMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue$/);
665
- if (queueMatch && req.method === 'GET') {
666
- const conversationId = queueMatch[1];
667
- const conv = queries.getConversation(conversationId);
668
- if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
669
- const queue = messageQueues.get(conversationId) || [];
670
- sendJSON(req, res, 200, { queue });
671
- return;
672
- }
673
-
674
- const queueItemMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue\/([^/]+)$/);
675
- if (queueItemMatch && req.method === 'DELETE') {
676
- const conversationId = queueItemMatch[1];
677
- const messageId = queueItemMatch[2];
678
- const queue = messageQueues.get(conversationId);
679
- if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
680
- const index = queue.findIndex(q => q.messageId === messageId);
681
- if (index === -1) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
682
- queue.splice(index, 1);
683
- if (queue.length === 0) messageQueues.delete(conversationId);
684
- broadcastSync({ type: 'queue_status', conversationId, queueLength: queue?.length || 0, timestamp: Date.now() });
685
- sendJSON(req, res, 200, { deleted: true });
686
- return;
687
- }
688
-
689
- if (queueItemMatch && req.method === 'PATCH') {
690
- const conversationId = queueItemMatch[1];
691
- const messageId = queueItemMatch[2];
692
- const body = await parseBody(req);
693
- const queue = messageQueues.get(conversationId);
694
- if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
695
- const item = queue.find(q => q.messageId === messageId);
696
- if (!item) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
697
- if (body.content !== undefined) item.content = body.content;
698
- if (body.agentId !== undefined) item.agentId = body.agentId;
699
- broadcastSync({ type: 'queue_updated', conversationId, messageId, content: item.content, agentId: item.agentId, timestamp: Date.now() });
700
- sendJSON(req, res, 200, { updated: true, item });
701
- return;
702
- }
703
-
704
- const messageMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages\/([^/]+)$/);
705
- if (messageMatch && req.method === 'GET') {
706
- const msg = queries.getMessage(messageMatch[2]);
707
- if (!msg || msg.conversationId !== messageMatch[1]) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
708
- sendJSON(req, res, 200, { message: msg });
709
- return;
710
- }
711
-
712
- const sessionMatch = pathOnly.match(/^\/api\/sessions\/([^/]+)$/);
713
- if (sessionMatch && req.method === 'GET') {
714
- const sess = queries.getSession(sessionMatch[1]);
715
- if (!sess) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
716
- const events = queries.getSessionEvents(sessionMatch[1]);
717
- sendJSON(req, res, 200, { session: sess, events });
718
- return;
719
- }
720
-
721
- const fullLoadMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/full$/);
722
- if (fullLoadMatch && req.method === 'GET') {
723
- const conversationId = fullLoadMatch[1];
724
- const conv = queries.getConversation(conversationId);
725
- if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
726
- const latestSession = queries.getLatestSession(conversationId);
727
- const isActivelyStreaming = activeExecutions.has(conversationId);
728
-
729
- const url = new URL(req.url, 'http://localhost');
730
- const chunkLimit = Math.min(parseInt(url.searchParams.get('chunkLimit') || '500'), 5000);
731
- const allChunks = url.searchParams.get('allChunks') === '1';
732
-
733
- const totalChunks = queries.getConversationChunkCount(conversationId);
734
- let chunks;
735
- if (allChunks || totalChunks <= chunkLimit) {
736
- chunks = queries.getConversationChunks(conversationId);
737
- } else {
738
- chunks = queries.getRecentConversationChunks(conversationId, chunkLimit);
739
- }
740
- const msgResult = queries.getPaginatedMessages(conversationId, 100, 0);
741
- const rateLimitInfo = rateLimitState.get(conversationId) || null;
742
- sendJSON(req, res, 200, {
743
- conversation: conv,
744
- isActivelyStreaming,
745
- latestSession,
746
- chunks,
747
- totalChunks,
748
- messages: msgResult.messages,
749
- rateLimitInfo
750
- });
751
- return;
752
- }
753
-
754
- const conversationChunksMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/chunks$/);
755
- if (conversationChunksMatch && req.method === 'GET') {
756
- const conversationId = conversationChunksMatch[1];
757
- const conv = queries.getConversation(conversationId);
758
- if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
759
-
760
- const url = new URL(req.url, 'http://localhost');
761
- const since = parseInt(url.searchParams.get('since') || '0');
762
- const all = url.searchParams.get('all') === 'true';
763
- const totalChunks = queries.getConversationChunkCount(conversationId);
764
- let chunks;
765
- if (since > 0) {
766
- chunks = queries.getConversationChunksSince(conversationId, since);
767
- } else if (all) {
768
- chunks = queries.getConversationChunks(conversationId);
769
- } else {
770
- chunks = queries.getRecentConversationChunks(conversationId, 500);
771
- }
772
- debugLog(`[chunks] Conv ${conversationId}: ${chunks.length} chunks (total: ${totalChunks})`);
773
- sendJSON(req, res, 200, { ok: true, chunks, totalChunks });
774
- return;
775
- }
776
-
777
- const sessionChunksMatch = pathOnly.match(/^\/api\/sessions\/([^/]+)\/chunks$/);
778
- if (sessionChunksMatch && req.method === 'GET') {
779
- const sessionId = sessionChunksMatch[1];
780
- const sess = queries.getSession(sessionId);
781
- if (!sess) { sendJSON(req, res, 404, { error: 'Session not found' }); return; }
782
-
783
- const url = new URL(req.url, 'http://localhost');
784
- const sinceSeq = parseInt(url.searchParams.get('sinceSeq') || '-1');
785
- const since = parseInt(url.searchParams.get('since') || '0');
786
-
787
- let chunks;
788
- if (sinceSeq >= 0) {
789
- chunks = queries.getChunksSinceSeq(sessionId, sinceSeq);
790
- } else {
791
- chunks = queries.getChunksSince(sessionId, since);
792
- }
793
- sendJSON(req, res, 200, { ok: true, chunks });
794
- return;
795
- }
796
-
797
- if (pathOnly.match(/^\/api\/conversations\/([^/]+)\/sessions\/latest$/) && req.method === 'GET') {
798
- const convId = pathOnly.match(/^\/api\/conversations\/([^/]+)\/sessions\/latest$/)[1];
799
- const latestSession = queries.getLatestSession(convId);
800
- if (!latestSession) {
801
- sendJSON(req, res, 200, { session: null });
802
- return;
803
- }
804
- const events = queries.getSessionEvents(latestSession.id);
805
- sendJSON(req, res, 200, { session: latestSession, events });
806
- return;
807
- }
808
-
809
- const executionMatch = pathOnly.match(/^\/api\/sessions\/([^/]+)\/execution$/);
810
- if (executionMatch && req.method === 'GET') {
811
- const sessionId = executionMatch[1];
812
- const url = new URL(req.url, 'http://localhost');
813
- const limit = Math.min(parseInt(url.searchParams.get('limit') || '1000'), 5000);
814
- const offset = Math.max(parseInt(url.searchParams.get('offset') || '0'), 0);
815
- const filterType = url.searchParams.get('filterType');
816
-
817
- try {
818
- const session = queries.getSession(sessionId);
819
- const allChunks = session ? (queries.getChunksSince(sessionId, 0) || []) : [];
820
- const filtered = filterType ? allChunks.filter(e => e.type === filterType) : allChunks;
821
- const executionData = {
822
- sessionId,
823
- events: filtered.slice(offset, offset + limit),
824
- total: filtered.length,
825
- limit,
826
- offset,
827
- hasMore: offset + limit < filtered.length,
828
- metadata: {
829
- status: session?.status || 'unknown',
830
- startTime: session?.created_at || null,
831
- duration: session?.completed_at && session?.created_at ? session.completed_at - session.created_at : 0,
832
- eventCount: filtered.length
833
- }
834
- };
835
-
836
- sendJSON(req, res, 200, executionData);
837
- } catch (err) {
838
- sendJSON(req, res, 400, { error: err.message });
839
- }
840
- return;
841
- }
842
-
843
- const runsMatch = pathOnly.match(/^\/api\/runs$/);
844
- if (runsMatch && req.method === 'POST') {
845
- let body = '';
846
- for await (const chunk of req) { body += chunk; }
847
- let parsed = {};
848
- try { parsed = body ? JSON.parse(body) : {}; } catch {}
849
-
850
- const { input, agentId, webhook } = parsed;
851
- if (!input) {
852
- sendJSON(req, res, 400, { error: 'Missing input in request body' });
853
- return;
854
- }
855
-
856
- const resolvedAgentId = agentId || 'claude-code';
857
- const resolvedModel = parsed.model || null;
858
- const cwd = parsed.workingDirectory || STARTUP_CWD;
559
+ const messagesHandler = _messagesRoutes._match(req.method, pathOnly);
560
+ if (messagesHandler) { await messagesHandler(req, res); return; }
859
561
 
860
- const thread = queries.createConversation(resolvedAgentId, 'Stateless Run', cwd);
861
- const session = queries.createSession(thread.id, resolvedAgentId, 'pending');
862
- const message = queries.createMessage(thread.id, 'user', typeof input === 'string' ? input : JSON.stringify(input));
562
+ const sessionsHandler = _sessionsRoutes._match(req.method, pathOnly);
563
+ if (sessionsHandler) { await sessionsHandler(req, res); return; }
863
564
 
864
- processMessageWithStreaming(thread.id, message.id, session.id, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
565
+ const scriptsHandler = _scriptsRoutes._match(req.method, pathOnly);
566
+ if (scriptsHandler) { await scriptsHandler(req, res); return; }
865
567
 
866
- sendJSON(req, res, 200, {
867
- id: session.id,
868
- status: 'pending',
869
- started_at: session.started_at,
870
- agentId: resolvedAgentId
871
- });
872
- return;
873
- }
874
-
875
- const runsSearchMatch = pathOnly.match(/^\/api\/runs\/search$/);
876
- if (runsSearchMatch && req.method === 'POST') {
877
- const sessions = queries.getAllSessions();
878
- const runs = sessions.slice(0, 50).map(s => ({
879
- id: s.id,
880
- status: s.status,
881
- started_at: s.started_at,
882
- completed_at: s.completed_at,
883
- agentId: s.agentId,
884
- input: null,
885
- output: null
886
- })).reverse();
887
- sendJSON(req, res, 200, runs);
888
- return;
889
- }
890
-
891
- // POST /runs/stream - SSE removed, use WebSocket
892
- if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
893
- res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
894
- return;
895
- }
896
-
897
- const scriptsMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/scripts$/);
898
- if (scriptsMatch && req.method === 'GET') {
899
- const conv = queries.getConversation(scriptsMatch[1]);
900
- if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
901
- const wd = conv.workingDirectory || STARTUP_CWD;
902
- let hasStart = false, hasDev = false;
903
- try {
904
- const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
905
- const scripts = pkg.scripts || {};
906
- hasStart = !!scripts.start;
907
- hasDev = !!scripts.dev;
908
- } catch {}
909
- const running = activeScripts.has(scriptsMatch[1]);
910
- const runningScript = running ? activeScripts.get(scriptsMatch[1]).script : null;
911
- sendJSON(req, res, 200, { hasStart, hasDev, running, runningScript });
912
- return;
913
- }
914
-
915
- const runScriptMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/run-script$/);
916
- if (runScriptMatch && req.method === 'POST') {
917
- const conversationId = runScriptMatch[1];
918
- const conv = queries.getConversation(conversationId);
919
- if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
920
- if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Script already running' }); return; }
921
- const body = await parseBody(req);
922
- const script = body.script;
923
- if (script !== 'start' && script !== 'dev') { sendJSON(req, res, 400, { error: 'Invalid script' }); return; }
924
- const wd = conv.workingDirectory || STARTUP_CWD;
925
- try {
926
- const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
927
- if (!pkg.scripts || !pkg.scripts[script]) { sendJSON(req, res, 400, { error: `Script "${script}" not found` }); return; }
928
- } catch { sendJSON(req, res, 400, { error: 'No package.json' }); return; }
929
-
930
- const childEnv = { ...process.env, FORCE_COLOR: '1' };
931
- delete childEnv.PORT;
932
- delete childEnv.BASE_URL;
933
- delete childEnv.HOT_RELOAD;
934
- const isWindows = os.platform() === 'win32';
935
- const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
936
- activeScripts.set(conversationId, { process: child, script, startTime: Date.now() });
937
- broadcastSync({ type: 'script_started', conversationId, script, timestamp: Date.now() });
938
-
939
- const onData = (stream) => (chunk) => {
940
- broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
941
- };
942
- child.stdout.on('data', onData('stdout'));
943
- child.stderr.on('data', onData('stderr'));
944
- child.stdout.on('error', () => {});
945
- child.stderr.on('error', () => {});
946
- child.on('error', (err) => {
947
- activeScripts.delete(conversationId);
948
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
949
- });
950
- child.on('close', (code) => {
951
- activeScripts.delete(conversationId);
952
- broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
953
- });
954
- sendJSON(req, res, 200, { ok: true, script, pid: child.pid });
955
- return;
956
- }
957
-
958
- const stopScriptMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/stop-script$/);
959
- if (stopScriptMatch && req.method === 'POST') {
960
- const conversationId = stopScriptMatch[1];
961
- const entry = activeScripts.get(conversationId);
962
- if (!entry) { sendJSON(req, res, 404, { error: 'No running script' }); return; }
963
- try { process.kill(-entry.process.pid, 'SIGTERM'); } catch { try { entry.process.kill('SIGTERM'); } catch {} }
964
- sendJSON(req, res, 200, { ok: true });
965
- return;
966
- }
967
-
968
- const scriptStatusMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/script-status$/);
969
- if (scriptStatusMatch && req.method === 'GET') {
970
- const entry = activeScripts.get(scriptStatusMatch[1]);
971
- sendJSON(req, res, 200, { running: !!entry, script: entry?.script || null });
972
- return;
973
- }
974
-
975
- const cancelRunMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/cancel$/);
976
- if (cancelRunMatch && req.method === 'POST') {
977
- const conversationId = cancelRunMatch[1];
978
- const entry = activeExecutions.get(conversationId);
979
-
980
- if (!entry) {
981
- sendJSON(req, res, 404, { error: 'No active execution to cancel' });
982
- return;
983
- }
984
-
985
- const { pid, sessionId } = entry;
986
-
987
- if (pid) {
988
- try {
989
- process.kill(-pid, 'SIGKILL');
990
- } catch {
991
- try {
992
- process.kill(pid, 'SIGKILL');
993
- } catch (e) {}
994
- }
995
- }
996
-
997
- if (sessionId) {
998
- queries.updateSession(sessionId, {
999
- status: 'interrupted',
1000
- completed_at: Date.now()
1001
- });
1002
- }
1003
-
1004
- queries.setIsStreaming(conversationId, false);
1005
- activeExecutions.delete(conversationId);
1006
-
1007
- broadcastSync({
1008
- type: 'streaming_complete',
1009
- sessionId,
1010
- conversationId,
1011
- interrupted: true,
1012
- timestamp: Date.now()
1013
- });
1014
-
1015
- sendJSON(req, res, 200, { ok: true, cancelled: true, conversationId, sessionId });
1016
- return;
1017
- }
1018
-
1019
- const resumeRunMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/resume$/);
1020
- if (resumeRunMatch && req.method === 'POST') {
1021
- const conversationId = resumeRunMatch[1];
1022
- const conv = queries.getConversation(conversationId);
1023
-
1024
- if (!conv) {
1025
- sendJSON(req, res, 404, { error: 'Conversation not found' });
1026
- return;
1027
- }
1028
-
1029
- const activeEntry = activeExecutions.get(conversationId);
1030
- if (activeEntry) {
1031
- sendJSON(req, res, 409, { error: 'Conversation already has an active execution' });
1032
- return;
1033
- }
1034
-
1035
- let body = '';
1036
- for await (const chunk of req) { body += chunk; }
1037
- let parsed = {};
1038
- try { parsed = body ? JSON.parse(body) : {}; } catch {}
1039
-
1040
- const { content, agentId } = parsed;
1041
- if (!content) {
1042
- sendJSON(req, res, 400, { error: 'Missing content in request body' });
1043
- return;
1044
- }
1045
-
1046
- const resolvedAgentId = agentId || conv.agentId || 'claude-code';
1047
- const resolvedModel = parsed.model || conv.model || null;
1048
- const cwd = conv.workingDirectory || STARTUP_CWD;
1049
-
1050
- const session = queries.createSession(conversationId, resolvedAgentId, 'pending');
1051
-
1052
- const message = queries.createMessage(conversationId, 'user', content);
1053
-
1054
- processMessageWithStreaming(conversationId, message.id, session.id, content, resolvedAgentId, resolvedModel);
1055
-
1056
- sendJSON(req, res, 200, {
1057
- ok: true,
1058
- conversationId,
1059
- sessionId: session.id,
1060
- messageId: message.id,
1061
- resumed: true
1062
- });
1063
- return;
1064
- }
1065
-
1066
- const injectMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/inject$/);
1067
- if (injectMatch && req.method === 'POST') {
1068
- const conversationId = injectMatch[1];
1069
- const conv = queries.getConversation(conversationId);
1070
-
1071
- if (!conv) {
1072
- sendJSON(req, res, 404, { error: 'Conversation not found' });
1073
- return;
1074
- }
1075
-
1076
- let body = '';
1077
- for await (const chunk of req) { body += chunk; }
1078
- let parsed = {};
1079
- try { parsed = body ? JSON.parse(body) : {}; } catch {}
1080
-
1081
- const { content, eager } = parsed;
1082
- if (!content) {
1083
- sendJSON(req, res, 400, { error: 'Missing content in request body' });
1084
- return;
1085
- }
1086
-
1087
- const entry = activeExecutions.get(conversationId);
1088
-
1089
- if (entry && eager) {
1090
- sendJSON(req, res, 409, { error: 'Cannot eagerly inject while execution is running - message queued' });
1091
- return;
1092
- }
1093
-
1094
- const message = queries.createMessage(conversationId, 'user', '[INJECTED] ' + content);
1095
-
1096
- if (!entry) {
1097
- const resolvedAgentId = conv.agentId || 'claude-code';
1098
- const resolvedModel = conv.model || null;
1099
- const cwd = conv.workingDirectory || STARTUP_CWD;
1100
- const session = queries.createSession(conversationId, resolvedAgentId, 'pending');
1101
- processMessageWithStreaming(conversationId, message.id, session.id, message.content, resolvedAgentId, resolvedModel);
1102
- }
1103
-
1104
- sendJSON(req, res, 200, { ok: true, injected: true, conversationId, messageId: message.id });
1105
- return;
1106
- }
568
+ const runsHandlerA = _runsRoutes._match(req.method, pathOnly);
569
+ if (runsHandlerA) { await runsHandlerA(req, res); return; }
1107
570
 
1108
571
  const agentHandler = _agentRoutes._match(req.method, pathOnly);
1109
572
  if (agentHandler) { await agentHandler(req, res); return; }
1110
573
 
1111
- if (pathOnly === '/api/runs' && req.method === 'POST') {
1112
- const body = await parseBody(req);
1113
- const { agent_id, input, config, webhook_url } = body;
1114
- if (!agent_id) {
1115
- sendJSON(req, res, 422, { error: 'agent_id is required' });
1116
- return;
1117
- }
1118
- const agent = discoveredAgents.find(a => a.id === agent_id);
1119
- if (!agent) {
1120
- sendJSON(req, res, 404, { error: 'Agent not found' });
1121
- return;
1122
- }
1123
- const run = queries.createRun(agent_id, null, input, config, webhook_url);
1124
- sendJSON(req, res, 201, run);
1125
- return;
1126
- }
1127
-
1128
- if (pathOnly === '/api/runs/search' && req.method === 'POST') {
1129
- const body = await parseBody(req);
1130
- const result = queries.searchRuns(body);
1131
- sendJSON(req, res, 200, result);
1132
- return;
1133
- }
1134
-
1135
- if (pathOnly === '/api/runs/wait' && req.method === 'POST') {
1136
- const body = await parseBody(req);
1137
- const { agent_id, input, config } = body;
1138
- if (!agent_id) {
1139
- sendJSON(req, res, 422, { error: 'agent_id is required' });
1140
- return;
1141
- }
1142
- const agent = discoveredAgents.find(a => a.id === agent_id);
1143
- if (!agent) {
1144
- sendJSON(req, res, 404, { error: 'Agent not found' });
1145
- return;
1146
- }
1147
- const run = queries.createRun(agent_id, null, input, config);
1148
- const statelessThreadId = queries.getRun(run.run_id)?.thread_id;
1149
- if (statelessThreadId && input?.content) {
1150
- try {
1151
- await runClaudeWithStreaming(agent_id, statelessThreadId, input.content, config?.model || null);
1152
- const finalRun = queries.getRun(run.run_id);
1153
- sendJSON(req, res, 200, finalRun);
1154
- } catch (err) {
1155
- queries.updateRunStatus(run.run_id, 'error');
1156
- sendJSON(req, res, 500, { error: err.message });
1157
- }
1158
- } else {
1159
- sendJSON(req, res, 200, run);
1160
- }
1161
- return;
1162
- }
1163
-
1164
- const oldRunByIdMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
1165
- if (oldRunByIdMatch1) {
1166
- const runId = oldRunByIdMatch1[1];
1167
-
1168
- if (req.method === 'GET') {
1169
- const run = queries.getRun(runId);
1170
- if (!run) {
1171
- sendJSON(req, res, 404, { error: 'Run not found' });
1172
- return;
1173
- }
1174
- sendJSON(req, res, 200, run);
1175
- return;
1176
- }
1177
-
1178
- if (req.method === 'POST') {
1179
- const body = await parseBody(req);
1180
- const run = queries.getRun(runId);
1181
- if (!run) {
1182
- sendJSON(req, res, 404, { error: 'Run not found' });
1183
- return;
1184
- }
1185
- if (run.status !== 'pending') {
1186
- sendJSON(req, res, 409, { error: 'Run is not resumable' });
1187
- return;
1188
- }
1189
- if (body.input?.content && run.thread_id) {
1190
- runClaudeWithStreaming(run.agent_id, run.thread_id, body.input.content, null).catch(() => {});
1191
- }
1192
- sendJSON(req, res, 200, run);
1193
- return;
1194
- }
1195
-
1196
- if (req.method === 'DELETE') {
1197
- try {
1198
- queries.deleteRun(runId);
1199
- res.writeHead(204);
1200
- res.end();
1201
- } catch (err) {
1202
- sendJSON(req, res, 404, { error: 'Run not found' });
1203
- }
1204
- return;
1205
- }
1206
- }
1207
-
1208
- const runWaitMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/wait$/);
1209
- if (runWaitMatch && req.method === 'GET') {
1210
- const runId = runWaitMatch[1];
1211
- const run = queries.getRun(runId);
1212
- if (!run) {
1213
- sendJSON(req, res, 404, { error: 'Run not found' });
1214
- return;
1215
- }
1216
- const startTime = Date.now();
1217
- const pollInterval = setInterval(() => {
1218
- const currentRun = queries.getRun(runId);
1219
- const elapsed = Date.now() - startTime;
1220
- const done = currentRun && ['success', 'error', 'cancelled'].includes(currentRun.status);
1221
- if (done) {
1222
- clearInterval(pollInterval);
1223
- sendJSON(req, res, 200, currentRun);
1224
- } else if (elapsed > 30000) {
1225
- clearInterval(pollInterval);
1226
- sendJSON(req, res, 408, { error: 'Run still pending after 30s', run_id: runId, status: currentRun?.status || run.status });
1227
- }
1228
- }, 500);
1229
- req.on('close', () => clearInterval(pollInterval));
1230
- return;
1231
- }
1232
-
1233
- const runStreamMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/stream$/);
1234
- if (runStreamMatch && req.method === 'GET') {
1235
- res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
1236
- return;
1237
- }
1238
-
1239
- const oldRunCancelMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
1240
- if (oldRunCancelMatch1 && req.method === 'POST') {
1241
- const runId = oldRunCancelMatch1[1];
1242
- try {
1243
- const run = queries.getRun(runId);
1244
- if (!run) {
1245
- sendJSON(req, res, 404, { error: 'Run not found' });
1246
- return;
1247
- }
1248
-
1249
- if (['success', 'error', 'cancelled'].includes(run.status)) {
1250
- sendJSON(req, res, 409, { error: 'Run already completed or cancelled' });
1251
- return;
1252
- }
1253
-
1254
- const cancelledRun = queries.cancelRun(runId);
1255
-
1256
- const threadId = run.thread_id;
1257
- if (threadId) {
1258
- const execution = activeExecutions.get(threadId);
1259
- if (execution?.pid) {
1260
- try {
1261
- process.kill(-execution.pid, 'SIGTERM');
1262
- } catch {
1263
- try {
1264
- process.kill(execution.pid, 'SIGTERM');
1265
- } catch (e) {
1266
- console.error(`[cancel] Failed to SIGTERM PID ${execution.pid}:`, e.message);
1267
- }
1268
- }
1269
-
1270
- setTimeout(() => {
1271
- try {
1272
- process.kill(-execution.pid, 'SIGKILL');
1273
- } catch {
1274
- try {
1275
- process.kill(execution.pid, 'SIGKILL');
1276
- } catch (e) {}
1277
- }
1278
- }, 3000);
1279
- }
1280
-
1281
- if (execution?.sessionId) {
1282
- queries.updateSession(execution.sessionId, {
1283
- status: 'error',
1284
- error: 'Cancelled by user',
1285
- completed_at: Date.now()
1286
- });
1287
- }
1288
-
1289
- activeExecutions.delete(threadId);
1290
- queries.setIsStreaming(threadId, false);
1291
-
1292
- broadcastSync({
1293
- type: 'streaming_cancelled',
1294
- sessionId: execution?.sessionId || runId,
1295
- conversationId: threadId,
1296
- runId: runId,
1297
- timestamp: Date.now()
1298
- });
1299
- }
1300
-
1301
- sendJSON(req, res, 200, cancelledRun);
1302
- } catch (err) {
1303
- if (err.message === 'Run not found') {
1304
- sendJSON(req, res, 404, { error: err.message });
1305
- } else if (err.message.includes('already completed')) {
1306
- sendJSON(req, res, 409, { error: err.message });
1307
- } else {
1308
- sendJSON(req, res, 500, { error: err.message });
1309
- }
1310
- }
1311
- return;
1312
- }
1313
-
1314
- const threadRunCancelMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/cancel$/);
1315
- if (threadRunCancelMatch && req.method === 'POST') {
1316
- const threadId = threadRunCancelMatch[1];
1317
- const runId = threadRunCancelMatch[2];
1318
-
1319
- try {
1320
- const run = queries.getRun(runId);
1321
- if (!run) {
1322
- sendJSON(req, res, 404, { error: 'Run not found' });
1323
- return;
1324
- }
1325
-
1326
- if (run.thread_id !== threadId) {
1327
- sendJSON(req, res, 400, { error: 'Run does not belong to specified thread' });
1328
- return;
1329
- }
1330
-
1331
- if (['success', 'error', 'cancelled'].includes(run.status)) {
1332
- sendJSON(req, res, 409, { error: 'Run already completed or cancelled' });
1333
- return;
1334
- }
1335
-
1336
- const cancelledRun = queries.cancelRun(runId);
1337
-
1338
- const execution = activeExecutions.get(threadId);
1339
- if (execution?.pid) {
1340
- try {
1341
- process.kill(-execution.pid, 'SIGTERM');
1342
- } catch {
1343
- try {
1344
- process.kill(execution.pid, 'SIGTERM');
1345
- } catch (e) {
1346
- console.error(`[cancel] Failed to SIGTERM PID ${execution.pid}:`, e.message);
1347
- }
1348
- }
1349
-
1350
- setTimeout(() => {
1351
- try {
1352
- process.kill(-execution.pid, 'SIGKILL');
1353
- } catch {
1354
- try {
1355
- process.kill(execution.pid, 'SIGKILL');
1356
- } catch (e) {}
1357
- }
1358
- }, 3000);
1359
- }
1360
-
1361
- if (execution?.sessionId) {
1362
- queries.updateSession(execution.sessionId, {
1363
- status: 'error',
1364
- error: 'Cancelled by user',
1365
- completed_at: Date.now()
1366
- });
1367
- }
1368
-
1369
- activeExecutions.delete(threadId);
1370
- activeProcessesByRunId.delete(runId);
1371
- queries.setIsStreaming(threadId, false);
1372
-
1373
- broadcastSync({ type: 'run_cancelled', runId, threadId, sessionId: execution?.sessionId, timestamp: Date.now() });
1374
- broadcastSync({ type: 'streaming_cancelled', sessionId: execution?.sessionId || runId, conversationId: threadId, runId, timestamp: Date.now() });
1375
-
1376
- sendJSON(req, res, 200, cancelledRun);
1377
- } catch (err) {
1378
- if (err.message === 'Run not found') {
1379
- sendJSON(req, res, 404, { error: err.message });
1380
- } else if (err.message.includes('already completed')) {
1381
- sendJSON(req, res, 409, { error: err.message });
1382
- } else {
1383
- sendJSON(req, res, 500, { error: err.message });
1384
- }
1385
- }
1386
- return;
1387
- }
1388
-
1389
- const threadRunWaitMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/wait$/);
1390
- if (threadRunWaitMatch && req.method === 'GET') {
1391
- const threadId = threadRunWaitMatch[1];
1392
- const runId = threadRunWaitMatch[2];
1393
-
1394
- const run = queries.getRun(runId);
1395
- if (!run) {
1396
- sendJSON(req, res, 404, { error: 'Run not found' });
1397
- return;
1398
- }
1399
-
1400
- if (run.thread_id !== threadId) {
1401
- sendJSON(req, res, 400, { error: 'Run does not belong to specified thread' });
1402
- return;
1403
- }
1404
-
1405
- const startTime = Date.now();
1406
- const pollInterval = setInterval(() => {
1407
- const currentRun = queries.getRun(runId);
1408
- const elapsed = Date.now() - startTime;
1409
- const done = currentRun && ['success', 'error', 'cancelled'].includes(currentRun.status);
1410
- if (done) {
1411
- clearInterval(pollInterval);
1412
- sendJSON(req, res, 200, currentRun);
1413
- } else if (elapsed > 30000) {
1414
- clearInterval(pollInterval);
1415
- sendJSON(req, res, 408, { error: 'Run still pending after 30s', run_id: runId, status: currentRun?.status || run.status });
1416
- }
1417
- }, 500);
1418
- req.on('close', () => clearInterval(pollInterval));
1419
- return;
1420
- }
1421
-
1422
574
  const oauthHandler = _oauthRoutes._match(req.method, pathOnly);
1423
575
  if (oauthHandler) { await oauthHandler(req, res); return; }
1424
576
 
1425
- const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
1426
- if (agentAuthMatch && req.method === 'POST') {
1427
- const agentId = agentAuthMatch[1];
1428
- const agent = discoveredAgents.find(a => a.id === agentId);
1429
- if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
1430
-
1431
- if (agentId === 'codex' || agentId === 'cli-codex') {
1432
- try {
1433
- const result = await startCodexOAuth(req, { PORT, BASE_URL });
1434
- const conversationId = '__agent_auth__';
1435
- broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
1436
- 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() });
1437
-
1438
- const pollId = setInterval(() => {
1439
- if (getCodexOAuthState().status === 'success') {
1440
- clearInterval(pollId);
1441
- const email = getCodexOAuthState().email || '';
1442
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
1443
- broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
1444
- } else if (getCodexOAuthState().status === 'error') {
1445
- clearInterval(pollId);
1446
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getCodexOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
1447
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getCodexOAuthState().error, timestamp: Date.now() });
1448
- }
1449
- }, 1000);
1450
-
1451
- setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
1452
-
1453
- sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
1454
- return;
1455
- } catch (e) {
1456
- console.error('[codex-oauth] /api/agents/codex/auth failed:', e);
1457
- sendJSON(req, res, 500, { error: e.message });
1458
- return;
1459
- }
1460
- }
1461
-
1462
- if (agentId === 'gemini') {
1463
- try {
1464
- const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
1465
- const conversationId = '__agent_auth__';
1466
- broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
1467
- 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() });
1468
-
1469
- const pollId = setInterval(() => {
1470
- if (getGeminiOAuthState().status === 'success') {
1471
- clearInterval(pollId);
1472
- const email = getGeminiOAuthState().email || '';
1473
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
1474
- broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
1475
- } else if (getGeminiOAuthState().status === 'error') {
1476
- clearInterval(pollId);
1477
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getGeminiOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
1478
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getGeminiOAuthState().error, timestamp: Date.now() });
1479
- }
1480
- }, 1000);
1481
-
1482
- setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
1483
-
1484
- sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
1485
- return;
1486
- } catch (e) {
1487
- console.error('[gemini-oauth] /api/agents/gemini/auth failed:', e);
1488
- sendJSON(req, res, 500, { error: e.message });
1489
- return;
1490
- }
1491
- }
1492
-
1493
- const authCommands = {
1494
- 'claude-code': { cmd: 'claude', args: ['setup-token'] },
1495
- 'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
1496
- };
1497
- const authCmd = authCommands[agentId];
1498
- if (!authCmd) { sendJSON(req, res, 400, { error: 'No auth command for this agent' }); return; }
1499
-
1500
- const conversationId = '__agent_auth__';
1501
- if (activeScripts.has(conversationId)) {
1502
- sendJSON(req, res, 409, { error: 'Auth process already running' });
1503
- return;
1504
- }
1505
-
1506
- const child = spawn(authCmd.cmd, authCmd.args, {
1507
- stdio: ['pipe', 'pipe', 'pipe'],
1508
- env: { ...process.env, FORCE_COLOR: '1' },
1509
- shell: os.platform() === 'win32'
1510
- });
1511
- activeScripts.set(conversationId, { process: child, script: 'auth-' + agentId, startTime: Date.now() });
1512
- broadcastSync({ type: 'script_started', conversationId, script: 'auth-' + agentId, agentId, timestamp: Date.now() });
1513
-
1514
- const onData = (stream) => (chunk) => {
1515
- broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
1516
- };
1517
- child.stdout.on('data', onData('stdout'));
1518
- child.stderr.on('data', onData('stderr'));
1519
- child.stdout.on('error', () => {});
1520
- child.stderr.on('error', () => {});
1521
- child.on('error', (err) => {
1522
- activeScripts.delete(conversationId);
1523
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
1524
- });
1525
- child.on('close', (code) => {
1526
- activeScripts.delete(conversationId);
1527
- broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
1528
- });
1529
- sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
1530
- return;
1531
- }
1532
-
1533
- const agentUpdateMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/update$/);
1534
- if (agentUpdateMatch && req.method === 'POST') {
1535
- const agentId = agentUpdateMatch[1];
1536
- const updateCommands = {
1537
- 'claude-code': { cmd: 'claude', args: ['update', '--yes'] },
1538
- };
1539
- const updateCmd = updateCommands[agentId];
1540
- if (!updateCmd) { sendJSON(req, res, 400, { error: 'No update command for this agent' }); return; }
1541
- const conversationId = '__agent_update__';
1542
- if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Update already running' }); return; }
1543
- const child = spawn(updateCmd.cmd, updateCmd.args, {
1544
- stdio: ['pipe', 'pipe', 'pipe'],
1545
- env: { ...process.env, FORCE_COLOR: '1' },
1546
- shell: os.platform() === 'win32'
1547
- });
1548
- activeScripts.set(conversationId, { process: child, script: 'update-' + agentId, startTime: Date.now() });
1549
- broadcastSync({ type: 'script_started', conversationId, script: 'update-' + agentId, agentId, timestamp: Date.now() });
1550
- const onData = (stream) => (chunk) => {
1551
- broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
1552
- };
1553
- child.stdout.on('data', onData('stdout'));
1554
- child.stderr.on('data', onData('stderr'));
1555
- child.stdout.on('error', () => {});
1556
- child.stderr.on('error', () => {});
1557
- child.on('error', (err) => {
1558
- activeScripts.delete(conversationId);
1559
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
1560
- });
1561
- child.on('close', (code) => {
1562
- activeScripts.delete(conversationId);
1563
- modelCache.delete(agentId);
1564
- broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
1565
- });
1566
- sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
1567
- return;
1568
- }
1569
-
1570
- if (pathOnly === '/api/auth/configs' && req.method === 'GET') {
1571
- const configs = getProviderConfigs();
1572
- sendJSON(req, res, 200, configs);
1573
- return;
1574
- }
577
+ const agentActionsHandler = _agentActionsRoutes._match(req.method, pathOnly);
578
+ if (agentActionsHandler) { await agentActionsHandler(req, res); return; }
1575
579
 
1576
- if (pathOnly === '/api/auth/save-config' && req.method === 'POST') {
1577
- try {
1578
- const body = await parseBody(req);
1579
- const { providerId, apiKey, defaultModel } = body || {};
1580
- if (typeof providerId !== 'string' || !providerId.length || providerId.length > 100) {
1581
- sendJSON(req, res, 400, { error: 'Invalid providerId' }); return;
1582
- }
1583
- if (typeof apiKey !== 'string' || !apiKey.length || apiKey.length > 10000) {
1584
- sendJSON(req, res, 400, { error: 'Invalid apiKey' }); return;
1585
- }
1586
- if (defaultModel !== undefined && (typeof defaultModel !== 'string' || defaultModel.length > 200)) {
1587
- sendJSON(req, res, 400, { error: 'Invalid defaultModel' }); return;
1588
- }
1589
- const configPath = saveProviderConfig(providerId, apiKey, defaultModel || '');
1590
- sendJSON(req, res, 200, { success: true, path: configPath });
1591
- } catch (err) {
1592
- sendJSON(req, res, 400, { error: err.message });
1593
- }
1594
- return;
1595
- }
580
+ const authConfigHandler = _authConfigRoutes._match(req.method, pathOnly);
581
+ if (authConfigHandler) { await authConfigHandler(req, res); return; }
1596
582
 
1597
583
  const speechHandler = _speechRoutes._match(req.method, pathOnly);
1598
584
  if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
@@ -1784,6 +770,12 @@ const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
1784
770
  const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath });
1785
771
  const _convRoutes = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
1786
772
  const _agentRoutes = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
773
+ const _messagesRoutes = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });
774
+ const _sessionsRoutes = registerSessionsRoutes({ queries, sendJSON, activeExecutions, rateLimitState, debugLog });
775
+ const _runsRoutes = registerRunsRoutes({ sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD });
776
+ const _scriptsRoutes = registerScriptsRoutes({ sendJSON, parseBody, queries, broadcastSync, activeScripts, activeExecutions, processMessageWithStreaming, STARTUP_CWD });
777
+ const _agentActionsRoutes = registerAgentActionsRoutes({ sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir });
778
+ const _authConfigRoutes = registerAuthConfigRoutes({ sendJSON, parseBody, getProviderConfigs, saveProviderConfig });
1787
779
 
1788
780
  registerConvHandlers(wsRouter, {
1789
781
  queries, activeExecutions, rateLimitState,