agentgui 1.0.769 → 1.0.771
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/CLAUDE.md +3 -0
- package/lib/routes-agents.js +108 -0
- package/lib/routes-conversations.js +87 -0
- package/package.json +1 -1
- package/server.js +8 -250
package/CLAUDE.md
CHANGED
|
@@ -50,6 +50,9 @@ lib/routes-speech.js Speech/TTS HTTP route handlers (stt, tts, voices, speech-
|
|
|
50
50
|
lib/routes-oauth.js OAuth HTTP route handlers (gemini-oauth/*, codex-oauth/*)
|
|
51
51
|
lib/routes-tools.js Tool management HTTP route handlers (list, install, update, history, refresh)
|
|
52
52
|
lib/routes-util.js Utility HTTP route handlers (clone, folders, git, home, version, import)
|
|
53
|
+
lib/routes-agents.js Agent list/search/auth-status/descriptor/models HTTP route handlers
|
|
54
|
+
lib/routes-conversations.js Conversation CRUD HTTP route handlers (list, create, get, update, delete, archive, restore)
|
|
55
|
+
lib/routes-debug.js Debug/backup/restore/ws-stats HTTP route handlers
|
|
53
56
|
lib/routes-threads.js Thread CRUD HTTP route handlers (ACP v0.2.3 thread API)
|
|
54
57
|
lib/ws-protocol.js WebSocket RPC router (WsRouter class)
|
|
55
58
|
lib/ws-optimizer.js Per-client priority queue for WS event batching
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { getAgentDescriptor } from './agent-descriptors.js';
|
|
6
|
+
import { discoverExternalACPServers } from './agent-discovery.js';
|
|
7
|
+
|
|
8
|
+
export function register(deps) {
|
|
9
|
+
const { sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog } = deps;
|
|
10
|
+
const routes = {};
|
|
11
|
+
|
|
12
|
+
routes['GET /api/agents'] = async (req, res) => {
|
|
13
|
+
debugLog(`[API /api/agents] Returning ${discoveredAgents.length} agents`);
|
|
14
|
+
sendJSON(req, res, 200, { agents: discoveredAgents });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
routes['GET /api/acp/status'] = async (req, res) => {
|
|
18
|
+
sendJSON(req, res, 200, { tools: getACPStatus() });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
routes['POST /api/agents/search'] = async (req, res) => {
|
|
22
|
+
const body = await parseBody(req);
|
|
23
|
+
try {
|
|
24
|
+
const localResult = queries.searchAgents(discoveredAgents, body);
|
|
25
|
+
const externalAgents = await discoverExternalACPServers(discoveredAgents);
|
|
26
|
+
const externalResult = queries.searchAgents(externalAgents, body);
|
|
27
|
+
sendJSON(req, res, 200, {
|
|
28
|
+
agents: [...localResult.agents, ...externalResult.agents],
|
|
29
|
+
total: localResult.total + externalResult.total,
|
|
30
|
+
limit: body.limit || 50, offset: body.offset || 0,
|
|
31
|
+
hasMore: localResult.hasMore || externalResult.hasMore,
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Error searching agents:', error);
|
|
35
|
+
sendJSON(req, res, 200, queries.searchAgents(discoveredAgents, body));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
routes['GET /api/agents/auth-status'] = async (req, res) => {
|
|
40
|
+
const statuses = discoveredAgents.map(agent => {
|
|
41
|
+
const status = { id: agent.id, name: agent.name, authenticated: false, detail: '' };
|
|
42
|
+
try {
|
|
43
|
+
if (agent.id === 'claude-code') {
|
|
44
|
+
const credFile = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
45
|
+
if (fs.existsSync(credFile)) {
|
|
46
|
+
const creds = JSON.parse(fs.readFileSync(credFile, 'utf-8'));
|
|
47
|
+
if (creds.claudeAiOauth && creds.claudeAiOauth.expiresAt > Date.now()) {
|
|
48
|
+
status.authenticated = true;
|
|
49
|
+
status.detail = creds.claudeAiOauth.subscriptionType || 'authenticated';
|
|
50
|
+
} else { status.detail = 'expired'; }
|
|
51
|
+
} else { status.detail = 'no credentials'; }
|
|
52
|
+
} else if (agent.id === 'gemini') {
|
|
53
|
+
const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
|
54
|
+
const acctFile = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
55
|
+
let hasOAuth = false;
|
|
56
|
+
if (fs.existsSync(oauthFile)) {
|
|
57
|
+
try { const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf-8')); if (creds.refresh_token || creds.access_token) hasOAuth = true; } catch (_) {}
|
|
58
|
+
}
|
|
59
|
+
if (fs.existsSync(acctFile)) {
|
|
60
|
+
const accts = JSON.parse(fs.readFileSync(acctFile, 'utf-8'));
|
|
61
|
+
if (accts.active) { status.authenticated = true; status.detail = accts.active; }
|
|
62
|
+
else if (hasOAuth) { status.authenticated = true; status.detail = 'oauth'; }
|
|
63
|
+
else { status.detail = 'logged out'; }
|
|
64
|
+
} else if (hasOAuth) { status.authenticated = true; status.detail = 'oauth'; }
|
|
65
|
+
else { status.detail = 'no credentials'; }
|
|
66
|
+
} else if (agent.id === 'opencode') {
|
|
67
|
+
const out = execSync('opencode auth list 2>&1', { encoding: 'utf-8', timeout: 5000 });
|
|
68
|
+
const countMatch = out.match(/(\d+)\s+credentials?/);
|
|
69
|
+
if (countMatch && parseInt(countMatch[1], 10) > 0) { status.authenticated = true; status.detail = countMatch[1] + ' credential(s)'; }
|
|
70
|
+
else { status.detail = 'no credentials'; }
|
|
71
|
+
} else { status.detail = 'unknown'; }
|
|
72
|
+
} catch (e) { status.detail = 'check failed'; }
|
|
73
|
+
return status;
|
|
74
|
+
});
|
|
75
|
+
sendJSON(req, res, 200, { agents: statuses });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
routes['_match'] = (method, pathOnly) => {
|
|
79
|
+
const key = `${method} ${pathOnly}`;
|
|
80
|
+
if (routes[key]) return routes[key];
|
|
81
|
+
let m;
|
|
82
|
+
if (method === 'GET' && (m = pathOnly.match(/^\/api\/agents\/([^/]+)$/)))
|
|
83
|
+
return (req, res) => handleGetAgent(req, res, m[1]);
|
|
84
|
+
if (method === 'GET' && (m = pathOnly.match(/^\/api\/agents\/([^/]+)\/descriptor$/)))
|
|
85
|
+
return (req, res) => { const d = getAgentDescriptor(m[1]); d ? sendJSON(req, res, 200, d) : sendJSON(req, res, 404, { error: 'Agent not found' }); };
|
|
86
|
+
if (method === 'GET' && (m = pathOnly.match(/^\/api\/agents\/([^/]+)\/models$/)))
|
|
87
|
+
return (req, res) => handleGetModels(req, res, m[1]);
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
async function handleGetAgent(req, res, agentId) {
|
|
92
|
+
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
93
|
+
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
|
|
94
|
+
sendJSON(req, res, 200, { id: agent.id, name: agent.name, description: agent.description || '', icon: agent.icon || null, status: 'available' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handleGetModels(req, res, agentId) {
|
|
98
|
+
const cached = modelCache.get(agentId);
|
|
99
|
+
if (cached && (Date.now() - cached.timestamp) < 300000) { sendJSON(req, res, 200, { models: cached.models }); return; }
|
|
100
|
+
try {
|
|
101
|
+
const models = await getModelsForAgent(agentId);
|
|
102
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
103
|
+
sendJSON(req, res, 200, { models });
|
|
104
|
+
} catch (err) { sendJSON(req, res, 200, { models: [] }); }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return routes;
|
|
108
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
function expandTilde(p) { return p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p; }
|
|
5
|
+
|
|
6
|
+
export function register(deps) {
|
|
7
|
+
const { sendJSON, parseBody, queries, activeExecutions, broadcastSync } = deps;
|
|
8
|
+
const routes = {};
|
|
9
|
+
|
|
10
|
+
routes['GET /api/conversations'] = async (req, res) => {
|
|
11
|
+
const conversations = queries.getConversationsList();
|
|
12
|
+
const activeSessionConvIds = new Set(queries.getActiveSessionConversationIds());
|
|
13
|
+
for (const conv of conversations) {
|
|
14
|
+
if (conv.isStreaming && !activeExecutions.has(conv.id) && !activeSessionConvIds.has(conv.id)) conv.isStreaming = 0;
|
|
15
|
+
}
|
|
16
|
+
sendJSON(req, res, 200, { conversations });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
routes['POST /api/conversations'] = async (req, res) => {
|
|
20
|
+
const body = await parseBody(req);
|
|
21
|
+
const normalizedWorkingDir = body.workingDirectory ? path.resolve(expandTilde(body.workingDirectory)) : null;
|
|
22
|
+
const conversation = queries.createConversation(body.agentId, body.title, normalizedWorkingDir, body.model || null);
|
|
23
|
+
queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory, model: conversation.model }, conversation.id);
|
|
24
|
+
broadcastSync({ type: 'conversation_created', conversation });
|
|
25
|
+
sendJSON(req, res, 201, { conversation });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
routes['GET /api/conversations/archived'] = async (req, res) => {
|
|
29
|
+
sendJSON(req, res, 200, { conversations: queries.getArchivedConversations() });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
routes['_match'] = (method, pathOnly) => {
|
|
33
|
+
const key = `${method} ${pathOnly}`;
|
|
34
|
+
if (routes[key]) return routes[key];
|
|
35
|
+
let m;
|
|
36
|
+
if ((m = pathOnly.match(/^\/api\/conversations\/([^/]+)$/))) {
|
|
37
|
+
if (method === 'GET') return (req, res) => handleGetConv(req, res, m[1]);
|
|
38
|
+
if (method === 'POST' || method === 'PUT') return (req, res) => handleUpdateConv(req, res, m[1]);
|
|
39
|
+
if (method === 'DELETE') return (req, res) => handleDeleteConv(req, res, m[1]);
|
|
40
|
+
}
|
|
41
|
+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/conversations\/([^/]+)\/archive$/)))
|
|
42
|
+
return (req, res) => handleArchive(req, res, m[1]);
|
|
43
|
+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/conversations\/([^/]+)\/restore$/)))
|
|
44
|
+
return (req, res) => handleRestore(req, res, m[1]);
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function handleGetConv(req, res, id) {
|
|
49
|
+
const conv = queries.getConversation(id);
|
|
50
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
51
|
+
const latestSession = queries.getLatestSession(id);
|
|
52
|
+
sendJSON(req, res, 200, { conversation: conv, isActivelyStreaming: activeExecutions.has(id), latestSession });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handleUpdateConv(req, res, id) {
|
|
56
|
+
const body = await parseBody(req);
|
|
57
|
+
if (body.workingDirectory) body.workingDirectory = path.resolve(expandTilde(body.workingDirectory));
|
|
58
|
+
const conv = queries.updateConversation(id, body);
|
|
59
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
60
|
+
queries.createEvent('conversation.updated', body, id);
|
|
61
|
+
broadcastSync({ type: 'conversation_updated', conversation: conv });
|
|
62
|
+
sendJSON(req, res, 200, { conversation: conv });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleDeleteConv(req, res, id) {
|
|
66
|
+
const deleted = queries.deleteConversation(id);
|
|
67
|
+
if (!deleted) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
68
|
+
broadcastSync({ type: 'conversation_deleted', conversationId: id });
|
|
69
|
+
sendJSON(req, res, 200, { deleted: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function handleArchive(req, res, id) {
|
|
73
|
+
const conv = queries.archiveConversation(id);
|
|
74
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
75
|
+
broadcastSync({ type: 'conversation_deleted', conversationId: id });
|
|
76
|
+
sendJSON(req, res, 200, { conversation: conv });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function handleRestore(req, res, id) {
|
|
80
|
+
const conv = queries.restoreConversation(id);
|
|
81
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
82
|
+
broadcastSync({ type: 'conversation_created', conversation: conv });
|
|
83
|
+
sendJSON(req, res, 200, { conversation: conv });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return routes;
|
|
87
|
+
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -24,6 +24,8 @@ import { register as registerUtilRoutes } from './lib/routes-util.js';
|
|
|
24
24
|
import { register as registerToolRoutes } from './lib/routes-tools.js';
|
|
25
25
|
import { register as registerThreadRoutes } from './lib/routes-threads.js';
|
|
26
26
|
import { register as registerDebugRoutes } from './lib/routes-debug.js';
|
|
27
|
+
import { register as registerConvRoutes } from './lib/routes-conversations.js';
|
|
28
|
+
import { register as registerAgentRoutes } from './lib/routes-agents.js';
|
|
27
29
|
import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
|
|
28
30
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
29
31
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
@@ -546,94 +548,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
546
548
|
return;
|
|
547
549
|
}
|
|
548
550
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// Filter out stale streaming state using a single bulk query instead of N+1 per-conversation queries
|
|
552
|
-
const activeSessionConvIds = new Set(queries.getActiveSessionConversationIds());
|
|
553
|
-
for (const conv of conversations) {
|
|
554
|
-
if (conv.isStreaming && !activeExecutions.has(conv.id) && !activeSessionConvIds.has(conv.id)) {
|
|
555
|
-
conv.isStreaming = 0;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
sendJSON(req, res, 200, { conversations });
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (pathOnly === '/api/conversations' && req.method === 'POST') {
|
|
563
|
-
const body = await parseBody(req);
|
|
564
|
-
// Normalize working directory to avoid Windows path issues; expand ~ to home
|
|
565
|
-
const expandTilde = p => p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
|
|
566
|
-
const normalizedWorkingDir = body.workingDirectory ? path.resolve(expandTilde(body.workingDirectory)) : null;
|
|
567
|
-
const conversation = queries.createConversation(body.agentId, body.title, normalizedWorkingDir, body.model || null);
|
|
568
|
-
queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory, model: conversation.model }, conversation.id);
|
|
569
|
-
broadcastSync({ type: 'conversation_created', conversation });
|
|
570
|
-
sendJSON(req, res, 201, { conversation });
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const convMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)$/);
|
|
575
|
-
if (convMatch) {
|
|
576
|
-
if (req.method === 'GET') {
|
|
577
|
-
const conv = queries.getConversation(convMatch[1]);
|
|
578
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
579
|
-
|
|
580
|
-
const latestSession = queries.getLatestSession(convMatch[1]);
|
|
581
|
-
const isActivelyStreaming = activeExecutions.has(convMatch[1]);
|
|
582
|
-
|
|
583
|
-
sendJSON(req, res, 200, {
|
|
584
|
-
conversation: conv,
|
|
585
|
-
isActivelyStreaming,
|
|
586
|
-
latestSession
|
|
587
|
-
});
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (req.method === 'POST' || req.method === 'PUT') {
|
|
592
|
-
const body = await parseBody(req);
|
|
593
|
-
// Normalize working directory if present to avoid Windows path issues; expand ~ to home
|
|
594
|
-
if (body.workingDirectory) {
|
|
595
|
-
const expandTilde = p => p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
|
|
596
|
-
body.workingDirectory = path.resolve(expandTilde(body.workingDirectory));
|
|
597
|
-
}
|
|
598
|
-
const conv = queries.updateConversation(convMatch[1], body);
|
|
599
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
600
|
-
queries.createEvent('conversation.updated', body, convMatch[1]);
|
|
601
|
-
broadcastSync({ type: 'conversation_updated', conversation: conv });
|
|
602
|
-
sendJSON(req, res, 200, { conversation: conv });
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (req.method === 'DELETE') {
|
|
607
|
-
const deleted = queries.deleteConversation(convMatch[1]);
|
|
608
|
-
if (!deleted) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
609
|
-
broadcastSync({ type: 'conversation_deleted', conversationId: convMatch[1] });
|
|
610
|
-
sendJSON(req, res, 200, { deleted: true });
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (pathOnly === '/api/conversations/archived' && req.method === 'GET') {
|
|
616
|
-
sendJSON(req, res, 200, { conversations: queries.getArchivedConversations() });
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const archiveMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/archive$/);
|
|
621
|
-
if (archiveMatch && req.method === 'POST') {
|
|
622
|
-
const conv = queries.archiveConversation(archiveMatch[1]);
|
|
623
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
624
|
-
broadcastSync({ type: 'conversation_deleted', conversationId: archiveMatch[1] });
|
|
625
|
-
sendJSON(req, res, 200, { conversation: conv });
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const restoreMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/restore$/);
|
|
630
|
-
if (restoreMatch && req.method === 'POST') {
|
|
631
|
-
const conv = queries.restoreConversation(restoreMatch[1]);
|
|
632
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
633
|
-
broadcastSync({ type: 'conversation_created', conversation: conv });
|
|
634
|
-
sendJSON(req, res, 200, { conversation: conv });
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
551
|
+
const convHandler = _convRoutes._match(req.method, pathOnly);
|
|
552
|
+
if (convHandler) { await convHandler(req, res); return; }
|
|
637
553
|
|
|
638
554
|
const messagesMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages$/);
|
|
639
555
|
if (messagesMatch) {
|
|
@@ -1194,168 +1110,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1194
1110
|
return;
|
|
1195
1111
|
}
|
|
1196
1112
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
sendJSON(req, res, 200, { agents: discoveredAgents });
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
if (pathOnly === '/api/acp/status' && req.method === 'GET') {
|
|
1204
|
-
sendJSON(req, res, 200, { tools: getACPStatus() });
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const debugHandler = _debugRoutes._match(req.method, pathOnly);
|
|
1211
|
-
if (debugHandler) { await debugHandler(req, res); return; }
|
|
1212
|
-
|
|
1213
|
-
if (pathOnly === '/api/agents/search' && req.method === 'POST') {
|
|
1214
|
-
const body = await parseBody(req);
|
|
1215
|
-
try {
|
|
1216
|
-
// Get local agents
|
|
1217
|
-
const localResult = queries.searchAgents(discoveredAgents, body);
|
|
1218
|
-
|
|
1219
|
-
// Get external agents from ACP servers
|
|
1220
|
-
const externalAgents = await discoverExternalACPServers(discoveredAgents);
|
|
1221
|
-
const externalResult = queries.searchAgents(externalAgents, body);
|
|
1222
|
-
|
|
1223
|
-
// Combine results
|
|
1224
|
-
const combinedAgents = [...localResult.agents, ...externalResult.agents];
|
|
1225
|
-
const total = localResult.total + externalResult.total;
|
|
1226
|
-
const hasMore = localResult.hasMore || externalResult.hasMore;
|
|
1227
|
-
|
|
1228
|
-
sendJSON(req, res, 200, {
|
|
1229
|
-
agents: combinedAgents,
|
|
1230
|
-
total,
|
|
1231
|
-
limit: body.limit || 50,
|
|
1232
|
-
offset: body.offset || 0,
|
|
1233
|
-
hasMore,
|
|
1234
|
-
});
|
|
1235
|
-
} catch (error) {
|
|
1236
|
-
console.error('Error searching agents:', error);
|
|
1237
|
-
const result = queries.searchAgents(discoveredAgents, body);
|
|
1238
|
-
sendJSON(req, res, 200, result);
|
|
1239
|
-
}
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
if (pathOnly === '/api/agents/auth-status' && req.method === 'GET') {
|
|
1244
|
-
const statuses = discoveredAgents.map(agent => {
|
|
1245
|
-
const status = { id: agent.id, name: agent.name, authenticated: false, detail: '' };
|
|
1246
|
-
try {
|
|
1247
|
-
if (agent.id === 'claude-code') {
|
|
1248
|
-
const credFile = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
1249
|
-
if (fs.existsSync(credFile)) {
|
|
1250
|
-
const creds = JSON.parse(fs.readFileSync(credFile, 'utf-8'));
|
|
1251
|
-
if (creds.claudeAiOauth && creds.claudeAiOauth.expiresAt > Date.now()) {
|
|
1252
|
-
status.authenticated = true;
|
|
1253
|
-
status.detail = creds.claudeAiOauth.subscriptionType || 'authenticated';
|
|
1254
|
-
} else {
|
|
1255
|
-
status.detail = 'expired';
|
|
1256
|
-
}
|
|
1257
|
-
} else {
|
|
1258
|
-
status.detail = 'no credentials';
|
|
1259
|
-
}
|
|
1260
|
-
} else if (agent.id === 'gemini') {
|
|
1261
|
-
const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
|
1262
|
-
const acctFile = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
1263
|
-
let hasOAuth = false;
|
|
1264
|
-
if (fs.existsSync(oauthFile)) {
|
|
1265
|
-
try {
|
|
1266
|
-
const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf-8'));
|
|
1267
|
-
if (creds.refresh_token || creds.access_token) hasOAuth = true;
|
|
1268
|
-
} catch (_) {}
|
|
1269
|
-
}
|
|
1270
|
-
if (fs.existsSync(acctFile)) {
|
|
1271
|
-
const accts = JSON.parse(fs.readFileSync(acctFile, 'utf-8'));
|
|
1272
|
-
if (accts.active) {
|
|
1273
|
-
status.authenticated = true;
|
|
1274
|
-
status.detail = accts.active;
|
|
1275
|
-
} else if (hasOAuth) {
|
|
1276
|
-
status.authenticated = true;
|
|
1277
|
-
status.detail = 'oauth';
|
|
1278
|
-
} else {
|
|
1279
|
-
status.detail = 'logged out';
|
|
1280
|
-
}
|
|
1281
|
-
} else if (hasOAuth) {
|
|
1282
|
-
status.authenticated = true;
|
|
1283
|
-
status.detail = 'oauth';
|
|
1284
|
-
} else {
|
|
1285
|
-
status.detail = 'no credentials';
|
|
1286
|
-
}
|
|
1287
|
-
} else if (agent.id === 'opencode') {
|
|
1288
|
-
const out = execSync('opencode auth list 2>&1', { encoding: 'utf-8', timeout: 5000 });
|
|
1289
|
-
const countMatch = out.match(/(\d+)\s+credentials?/);
|
|
1290
|
-
if (countMatch && parseInt(countMatch[1], 10) > 0) {
|
|
1291
|
-
status.authenticated = true;
|
|
1292
|
-
status.detail = countMatch[1] + ' credential(s)';
|
|
1293
|
-
} else {
|
|
1294
|
-
status.detail = 'no credentials';
|
|
1295
|
-
}
|
|
1296
|
-
} else {
|
|
1297
|
-
status.detail = 'unknown';
|
|
1298
|
-
}
|
|
1299
|
-
} catch (e) {
|
|
1300
|
-
status.detail = 'check failed';
|
|
1301
|
-
}
|
|
1302
|
-
return status;
|
|
1303
|
-
});
|
|
1304
|
-
sendJSON(req, res, 200, { agents: statuses });
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
const agentByIdMatch = pathOnly.match(/^\/api\/agents\/([^/]+)$/);
|
|
1309
|
-
if (agentByIdMatch && req.method === 'GET') {
|
|
1310
|
-
const agentId = agentByIdMatch[1];
|
|
1311
|
-
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
1312
|
-
|
|
1313
|
-
if (!agent) {
|
|
1314
|
-
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
sendJSON(req, res, 200, {
|
|
1319
|
-
id: agent.id,
|
|
1320
|
-
name: agent.name,
|
|
1321
|
-
description: agent.description || '',
|
|
1322
|
-
icon: agent.icon || null,
|
|
1323
|
-
status: 'available'
|
|
1324
|
-
});
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const agentDescriptorMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/descriptor$/);
|
|
1329
|
-
if (agentDescriptorMatch && req.method === 'GET') {
|
|
1330
|
-
const agentId = agentDescriptorMatch[1];
|
|
1331
|
-
const descriptor = getAgentDescriptor(agentId);
|
|
1332
|
-
|
|
1333
|
-
if (!descriptor) {
|
|
1334
|
-
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
sendJSON(req, res, 200, descriptor);
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const modelsMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/models$/);
|
|
1343
|
-
if (modelsMatch && req.method === 'GET') {
|
|
1344
|
-
const agentId = modelsMatch[1];
|
|
1345
|
-
const cached = modelCache.get(agentId);
|
|
1346
|
-
if (cached && (Date.now() - cached.timestamp) < 300000) {
|
|
1347
|
-
sendJSON(req, res, 200, { models: cached.models });
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
try {
|
|
1351
|
-
const models = await getModelsForAgent(agentId);
|
|
1352
|
-
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
1353
|
-
sendJSON(req, res, 200, { models });
|
|
1354
|
-
} catch (err) {
|
|
1355
|
-
sendJSON(req, res, 200, { models: [] });
|
|
1356
|
-
}
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1113
|
+
const agentHandler = _agentRoutes._match(req.method, pathOnly);
|
|
1114
|
+
if (agentHandler) { await agentHandler(req, res); return; }
|
|
1359
1115
|
|
|
1360
1116
|
if (pathOnly === '/api/runs' && req.method === 'POST') {
|
|
1361
1117
|
const body = await parseBody(req);
|
|
@@ -2850,6 +2606,8 @@ const _utilRoutes = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_C
|
|
|
2850
2606
|
const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
|
|
2851
2607
|
const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
|
|
2852
2608
|
const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath });
|
|
2609
|
+
const _convRoutes = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
|
|
2610
|
+
const _agentRoutes = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
|
|
2853
2611
|
|
|
2854
2612
|
registerConvHandlers(wsRouter, {
|
|
2855
2613
|
queries, activeExecutions, rateLimitState,
|