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/CHANGELOG.md +2 -0
- package/lib/routes-agent-actions.js +117 -0
- package/lib/routes-auth-config.js +30 -0
- package/lib/routes-messages.js +139 -0
- package/lib/routes-runs.js +156 -0
- package/lib/routes-scripts.js +135 -0
- package/lib/routes-sessions.js +144 -0
- package/package.json +1 -1
- package/server.js +24 -1032
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
|
|
554
|
-
if (
|
|
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
|
-
|
|
861
|
-
|
|
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
|
-
|
|
565
|
+
const scriptsHandler = _scriptsRoutes._match(req.method, pathOnly);
|
|
566
|
+
if (scriptsHandler) { await scriptsHandler(req, res); return; }
|
|
865
567
|
|
|
866
|
-
|
|
867
|
-
|
|
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
|
|
1426
|
-
if (
|
|
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
|
-
|
|
1577
|
-
|
|
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,
|