agentgui 1.0.845 → 1.0.847
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 +9 -0
- package/lib/broadcast.js +17 -0
- package/lib/http-handler.js +137 -0
- package/lib/routes-registry.js +62 -0
- package/lib/server-startup.js +116 -0
- package/lib/server-startup2.js +83 -0
- package/package.json +1 -1
- package/server.js +24 -573
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## [Unreleased] - refactor: extract routes registry + wire tool/debug routes
|
|
2
|
+
|
|
3
|
+
- Extract all route and WS handler registrations from server.js L201-270 to lib/routes-registry.js (63L, createRegistry factory)
|
|
4
|
+
- Move BROADCAST_TYPES set to lib/broadcast.js as exported const; server.js imports it
|
|
5
|
+
- Refactor http-handler.js to accept routes object (routes.conv, routes.tools, routes.debug etc.) instead of individual named parameters; wire previously-unwired tool and debug routes
|
|
6
|
+
- Inline _mqDeps object into createMessageQueue call; compact process.on error handlers
|
|
7
|
+
- server.js reduced from 337L to 200L; all lib files ≤200L
|
|
8
|
+
|
|
9
|
+
|
|
1
10
|
## [Unreleased]
|
|
2
11
|
|
|
3
12
|
### Refactor
|
package/lib/broadcast.js
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
export const BROADCAST_TYPES = new Set([
|
|
2
|
+
'message_created', 'conversation_created', 'conversation_updated',
|
|
3
|
+
'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated', 'queue_item_dequeued',
|
|
4
|
+
'rate_limit_hit', 'rate_limit_clear',
|
|
5
|
+
'script_started', 'script_stopped', 'script_output',
|
|
6
|
+
'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list',
|
|
7
|
+
'streaming_start', 'streaming_progress', 'streaming_complete', 'streaming_error',
|
|
8
|
+
'tool_install_started', 'tool_install_progress', 'tool_install_complete', 'tool_install_failed',
|
|
9
|
+
'tool_update_progress', 'tool_update_complete', 'tool_update_failed',
|
|
10
|
+
'tool_status_update', 'tool_update_available',
|
|
11
|
+
'tools_update_started', 'tools_update_complete', 'tools_refresh_complete',
|
|
12
|
+
'pm2_monit_update', 'pm2_monitoring_started', 'pm2_monitoring_stopped',
|
|
13
|
+
'pm2_list_response', 'pm2_start_response', 'pm2_stop_response',
|
|
14
|
+
'pm2_restart_response', 'pm2_delete_response', 'pm2_logs_response',
|
|
15
|
+
'pm2_flush_logs_response', 'pm2_ping_response', 'pm2_unavailable'
|
|
16
|
+
]);
|
|
17
|
+
|
|
1
18
|
export function createBroadcast({ syncClients, subscriptionIndex, wsOptimizer, broadcastTypes, getSeq }) {
|
|
2
19
|
return function broadcastSync(event) {
|
|
3
20
|
try {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
|
|
6
|
+
export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, wss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT }) {
|
|
7
|
+
return async function httpHandler(req, res) {
|
|
8
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
9
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
10
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
11
|
+
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
|
12
|
+
if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return;
|
|
13
|
+
|
|
14
|
+
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;
|
|
15
|
+
const hits = (rateLimitMap.get(clientIp) || 0) + 1;
|
|
16
|
+
rateLimitMap.set(clientIp, hits);
|
|
17
|
+
res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
|
|
18
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX - hits));
|
|
19
|
+
if (hits > RATE_LIMIT_MAX) { res.writeHead(429, { 'Retry-After': '60' }); res.end('Too Many Requests'); return; }
|
|
20
|
+
|
|
21
|
+
const _pwd = process.env.PASSWORD;
|
|
22
|
+
if (_pwd) {
|
|
23
|
+
const _auth = req.headers['authorization'] || '';
|
|
24
|
+
let _ok = false;
|
|
25
|
+
if (_auth.startsWith('Basic ')) {
|
|
26
|
+
try {
|
|
27
|
+
const _decoded = Buffer.from(_auth.slice(6), 'base64').toString('utf8');
|
|
28
|
+
const _ci = _decoded.indexOf(':');
|
|
29
|
+
if (_ci !== -1) { const _p = _decoded.slice(_ci + 1); try { _ok = _p.length === _pwd.length && crypto.timingSafeEqual(Buffer.from(_p), Buffer.from(_pwd)); } catch { _ok = false; } }
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
}
|
|
32
|
+
if (!_ok) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="agentgui"' }); res.end('Unauthorized'); return; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pathOnly = req.url.split('?')[0];
|
|
36
|
+
if (pathOnly.startsWith(BASE_URL + '/api/upload/') || pathOnly.startsWith(BASE_URL + '/files/')) return expressApp(req, res);
|
|
37
|
+
|
|
38
|
+
if (req.url === '/favicon.ico' || req.url === BASE_URL + '/favicon.ico') {
|
|
39
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#3b82f6"/><text x="50" y="68" font-size="50" font-family="sans-serif" font-weight="bold" fill="white" text-anchor="middle">G</text></svg>';
|
|
40
|
+
res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' });
|
|
41
|
+
res.end(svg); return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; }
|
|
45
|
+
|
|
46
|
+
let routePath = req.url;
|
|
47
|
+
if (req.url.startsWith(BASE_URL + '/')) { routePath = req.url.slice(BASE_URL.length); }
|
|
48
|
+
else if (req.url === BASE_URL) { routePath = '/'; }
|
|
49
|
+
else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') ||
|
|
50
|
+
req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' ||
|
|
51
|
+
req.url.startsWith('/conversations/')) { routePath = req.url; }
|
|
52
|
+
else { res.writeHead(404); res.end('Not found'); return; }
|
|
53
|
+
|
|
54
|
+
routePath = routePath || '/';
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const pathOnly = routePath.split('?')[0];
|
|
58
|
+
|
|
59
|
+
if (pathOnly === '/oauth2callback' && req.method === 'GET') { await handleGeminiOAuthCallback(req, res, PORT); return; }
|
|
60
|
+
if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') { await handleCodexOAuthCallback(req, res, PORT); return; }
|
|
61
|
+
|
|
62
|
+
if (pathOnly === '/api/health' && req.method === 'GET') {
|
|
63
|
+
let dbStatus = { ok: true };
|
|
64
|
+
try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; }
|
|
65
|
+
const queueSizes = {};
|
|
66
|
+
for (const [k, v] of messageQueues) queueSizes[k] = v.length;
|
|
67
|
+
sendJSON(req, res, 200, { status: 'ok', version: PKG_VERSION, uptime: process.uptime(), agents: discoveredAgents.length, activeExecutions: activeExecutions.size, wsClients: wss.clients.size, memory: process.memoryUsage(), acp: getACPStatus(), db: dbStatus, queueSizes });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const convHandler = routes.conv._match(req.method, pathOnly);
|
|
72
|
+
if (convHandler) { await convHandler(req, res); return; }
|
|
73
|
+
const messagesHandler = routes.messages._match(req.method, pathOnly);
|
|
74
|
+
if (messagesHandler) { await messagesHandler(req, res); return; }
|
|
75
|
+
const sessionsHandler = routes.sessions._match(req.method, pathOnly);
|
|
76
|
+
if (sessionsHandler) { await sessionsHandler(req, res); return; }
|
|
77
|
+
const scriptsHandler = routes.scripts._match(req.method, pathOnly);
|
|
78
|
+
if (scriptsHandler) { await scriptsHandler(req, res); return; }
|
|
79
|
+
const runsHandler = routes.runs._match(req.method, pathOnly);
|
|
80
|
+
if (runsHandler) { await runsHandler(req, res); return; }
|
|
81
|
+
const agentHandler = routes.agents._match(req.method, pathOnly);
|
|
82
|
+
if (agentHandler) { await agentHandler(req, res); return; }
|
|
83
|
+
const oauthHandler = routes.oauth._match(req.method, pathOnly);
|
|
84
|
+
if (oauthHandler) { await oauthHandler(req, res); return; }
|
|
85
|
+
const agentActionsHandler = routes.agentActions._match(req.method, pathOnly);
|
|
86
|
+
if (agentActionsHandler) { await agentActionsHandler(req, res); return; }
|
|
87
|
+
const authConfigHandler = routes.authConfig._match(req.method, pathOnly);
|
|
88
|
+
if (authConfigHandler) { await authConfigHandler(req, res); return; }
|
|
89
|
+
const speechHandler = routes.speech._match(req.method, pathOnly);
|
|
90
|
+
if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
|
|
91
|
+
const utilHandler = routes.util._match(req.method, pathOnly);
|
|
92
|
+
if (utilHandler) { await utilHandler(req, res); return; }
|
|
93
|
+
const threadHandler = routes.threads._match(req.method, pathOnly);
|
|
94
|
+
if (threadHandler) { await threadHandler(req, res); return; }
|
|
95
|
+
const toolHandler = routes.tools._match(req.method, pathOnly);
|
|
96
|
+
if (toolHandler) { await toolHandler(req, res); return; }
|
|
97
|
+
const debugHandler = routes.debug._match(req.method, pathOnly);
|
|
98
|
+
if (debugHandler) { await debugHandler(req, res); return; }
|
|
99
|
+
if (routePath.startsWith('/api/image/')) {
|
|
100
|
+
const imagePath = routePath.slice('/api/image/'.length);
|
|
101
|
+
const decodedPath = decodeURIComponent(imagePath);
|
|
102
|
+
const expandedPath = decodedPath.startsWith('~') ? decodedPath.replace('~', os.homedir()) : decodedPath;
|
|
103
|
+
const normalizedPath = path.normalize(expandedPath);
|
|
104
|
+
const isWindows = os.platform() === 'win32';
|
|
105
|
+
const isAbsolute = isWindows ? /^[A-Za-z]:[\\\/]/.test(normalizedPath) : normalizedPath.startsWith('/');
|
|
106
|
+
if (!isAbsolute || normalizedPath.includes('..')) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(normalizedPath)) { res.writeHead(404); res.end('Not found'); return; }
|
|
109
|
+
const ext = path.extname(normalizedPath).toLowerCase();
|
|
110
|
+
const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
|
|
111
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
112
|
+
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
|
|
113
|
+
res.end(fs.readFileSync(normalizedPath));
|
|
114
|
+
} catch (err) { sendJSON(req, res, 400, { error: err.message }); }
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (pathOnly.match(/^\/conversations\/[^\/]+$/)) { serveFile(path.join(staticDir, 'index.html'), res, req); return; }
|
|
119
|
+
|
|
120
|
+
let filePath = routePath === '/' ? '/index.html' : routePath;
|
|
121
|
+
filePath = path.join(staticDir, filePath);
|
|
122
|
+
const normalizedPath = path.normalize(filePath);
|
|
123
|
+
if (!normalizedPath.startsWith(staticDir)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
124
|
+
|
|
125
|
+
fs.stat(filePath, (err, stats) => {
|
|
126
|
+
if (err) { res.writeHead(404); res.end('Not found'); return; }
|
|
127
|
+
if (stats.isDirectory()) {
|
|
128
|
+
filePath = path.join(filePath, 'index.html');
|
|
129
|
+
fs.stat(filePath, (err2) => { if (err2) { res.writeHead(404); res.end('Not found'); return; } serveFile(filePath, res, req); });
|
|
130
|
+
} else { serveFile(filePath, res, req); }
|
|
131
|
+
});
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error('Server error:', e.message);
|
|
134
|
+
sendJSON(req, res, 500, { error: e.message });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { register as registerSpeechRoutes } from './routes-speech.js';
|
|
2
|
+
import { register as registerOAuthRoutes } from './routes-oauth.js';
|
|
3
|
+
import { register as registerUtilRoutes } from './routes-util.js';
|
|
4
|
+
import { register as registerToolRoutes } from './routes-tools.js';
|
|
5
|
+
import { register as registerThreadRoutes } from './routes-threads.js';
|
|
6
|
+
import { register as registerDebugRoutes } from './routes-debug.js';
|
|
7
|
+
import { register as registerConvRoutes } from './routes-conversations.js';
|
|
8
|
+
import { register as registerAgentRoutes } from './routes-agents.js';
|
|
9
|
+
import { register as registerMessagesRoutes } from './routes-messages.js';
|
|
10
|
+
import { register as registerSessionsRoutes } from './routes-sessions.js';
|
|
11
|
+
import { register as registerRunsRoutes } from './routes-runs.js';
|
|
12
|
+
import { register as registerScriptsRoutes } from './routes-scripts.js';
|
|
13
|
+
import { register as registerAgentActionsRoutes } from './routes-agent-actions.js';
|
|
14
|
+
import { register as registerAuthConfigRoutes } from './routes-auth-config.js';
|
|
15
|
+
import { register as registerConvHandlers } from './ws-handlers-conv.js';
|
|
16
|
+
import { register as registerConvHandlers2 } from './ws-handlers-conv2.js';
|
|
17
|
+
import { register as registerSessionHandlers } from './ws-handlers-session.js';
|
|
18
|
+
import { register as registerSessionHandlers2 } from './ws-handlers-session2.js';
|
|
19
|
+
import { register as registerRunHandlers } from './ws-handlers-run.js';
|
|
20
|
+
import { register as registerUtilHandlers } from './ws-handlers-util.js';
|
|
21
|
+
import { register as registerOAuthHandlers } from './ws-handlers-oauth.js';
|
|
22
|
+
import { register as registerScriptHandlers } from './ws-handlers-scripts.js';
|
|
23
|
+
import { register as registerQueueHandlers } from './ws-handlers-queue.js';
|
|
24
|
+
import { register as registerMsgHandlers } from './ws-handlers-msg.js';
|
|
25
|
+
import { initSpeechManager, getSpeech, voiceCacheManager, modelDownloadState, ensureModelsDownloaded } from './speech-manager.js';
|
|
26
|
+
import { getAgentDescriptor } from './agent-descriptors.js';
|
|
27
|
+
import { startGeminiOAuth, exchangeGeminiOAuthCode, getGeminiOAuthState } from './oauth-gemini.js';
|
|
28
|
+
import { startCodexOAuth, exchangeCodexOAuthCode, getCodexOAuthState } from './oauth-codex.js';
|
|
29
|
+
import { getProviderConfigs, saveProviderConfig } from './provider-config.js';
|
|
30
|
+
|
|
31
|
+
export function createRegistry(wsRouter, deps) {
|
|
32
|
+
const { queries, sendJSON, parseBody, broadcastSync, debugLog, PORT, BASE_URL, rootDir, STARTUP_CWD, PKG_VERSION, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, activeScripts, messageQueues, rateLimitState, cleanupExecution, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, logError, toolManager, syncClients, wsOptimizer, errLogPath, getJsonlWatcher, routes } = deps;
|
|
33
|
+
|
|
34
|
+
initSpeechManager({ broadcastSync, syncClients, queries });
|
|
35
|
+
routes.speech = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync, debugLog });
|
|
36
|
+
routes.oauth = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
|
|
37
|
+
routes.util = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
|
|
38
|
+
routes.tools = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
|
|
39
|
+
routes.threads = registerThreadRoutes({ sendJSON, parseBody, queries });
|
|
40
|
+
routes.debug = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath: errLogPath });
|
|
41
|
+
routes.conv = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
|
|
42
|
+
routes.agents = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
|
|
43
|
+
routes.messages = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });
|
|
44
|
+
routes.sessions = registerSessionsRoutes({ queries, sendJSON, activeExecutions, rateLimitState, debugLog });
|
|
45
|
+
routes.runs = registerRunsRoutes({ sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD });
|
|
46
|
+
routes.scripts = registerScriptsRoutes({ sendJSON, parseBody, queries, broadcastSync, activeScripts, activeExecutions, processMessageWithStreaming, STARTUP_CWD });
|
|
47
|
+
routes.agentActions = registerAgentActionsRoutes({ sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir });
|
|
48
|
+
routes.authConfig = registerAuthConfigRoutes({ sendJSON, parseBody, getProviderConfigs, saveProviderConfig });
|
|
49
|
+
|
|
50
|
+
registerConvHandlers(wsRouter, { queries, activeExecutions, rateLimitState, broadcastSync, processMessageWithStreaming, cleanupExecution, getJsonlWatcher });
|
|
51
|
+
registerConvHandlers2(wsRouter, { queries, activeExecutions, rateLimitState, broadcastSync, processMessageWithStreaming, cleanupExecution, getJsonlWatcher });
|
|
52
|
+
registerMsgHandlers(wsRouter, { queries, activeExecutions, messageQueues, broadcastSync, processMessageWithStreaming, logError });
|
|
53
|
+
registerQueueHandlers(wsRouter, { queries, messageQueues, broadcastSync });
|
|
54
|
+
debugLog('[INIT] registerSessionHandlers, agents: ' + discoveredAgents.length);
|
|
55
|
+
registerSessionHandlers(wsRouter, { db: queries, discoveredAgents, modelCache, getAgentDescriptor, activeScripts, broadcastSync, startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState });
|
|
56
|
+
registerSessionHandlers2(wsRouter, { discoveredAgents, modelCache, activeScripts, broadcastSync, startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState });
|
|
57
|
+
debugLog('[INIT] registerSessionHandlers completed');
|
|
58
|
+
registerRunHandlers(wsRouter, { queries, discoveredAgents, activeExecutions, activeProcessesByRunId, broadcastSync, processMessageWithStreaming, cleanupExecution });
|
|
59
|
+
registerUtilHandlers(wsRouter, { queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded, broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig, STARTUP_CWD, voiceCacheManager, toolManager, discoveredAgents });
|
|
60
|
+
registerScriptHandlers(wsRouter, { queries, broadcastSync, STARTUP_CWD, activeScripts });
|
|
61
|
+
registerOAuthHandlers(wsRouter, { startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), exchangeGeminiOAuthCode, geminiOAuthState: getGeminiOAuthState, startCodexOAuth: (req) => startCodexOAuth(req, { PORT, BASE_URL }), exchangeCodexOAuthCode, codexOAuthState: getCodexOAuthState });
|
|
62
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { JsonlWatcher } from './jsonl-watcher.js';
|
|
2
|
+
|
|
3
|
+
export function createOnServerReady({ queries, broadcastSync, warmAssetCache, staticDir, toolManager, discoveredAgents, PORT, BASE_URL, watch, ownedSessionIds, resumeInterruptedStreams, activeExecutions, debugLog, installGMAgentConfigs, startACPTools, getACPStatus, execMachine, toolInstallMachine, getSpeech, ensureModelsDownloaded, performAutoImport, performAgentHealthCheck, pm2Manager, pm2Subscribers, recoverStaleSessions }) {
|
|
4
|
+
let jsonlWatcher = null;
|
|
5
|
+
|
|
6
|
+
function getJsonlWatcher() { return jsonlWatcher; }
|
|
7
|
+
|
|
8
|
+
function onServerReady() {
|
|
9
|
+
toolManager.clearStatusCache();
|
|
10
|
+
console.log(`GMGUI running on http://localhost:${PORT}${BASE_URL}/`);
|
|
11
|
+
console.log(`Agents: ${discoveredAgents.map(a => a.name).join(', ') || 'none'}`);
|
|
12
|
+
console.log(`Hot reload: ${watch ? 'on' : 'off'}`);
|
|
13
|
+
|
|
14
|
+
const deletedCount = queries.cleanupEmptyConversations();
|
|
15
|
+
if (deletedCount > 0) console.log(`Cleaned up ${deletedCount} empty conversation(s) on startup`);
|
|
16
|
+
|
|
17
|
+
recoverStaleSessions();
|
|
18
|
+
warmAssetCache(staticDir);
|
|
19
|
+
|
|
20
|
+
try { queries.cleanup(); console.log('[cleanup] Initial DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
|
|
21
|
+
setInterval(() => {
|
|
22
|
+
try { queries.cleanup(); console.log('[cleanup] Scheduled DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
|
|
23
|
+
}, 6 * 60 * 60 * 1000);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
|
|
27
|
+
jsonlWatcher.start();
|
|
28
|
+
console.log('[JSONL] Watcher started');
|
|
29
|
+
} catch (err) { console.error('[JSONL] Watcher failed to start:', err.message); }
|
|
30
|
+
|
|
31
|
+
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
32
|
+
|
|
33
|
+
setInterval(() => {
|
|
34
|
+
try {
|
|
35
|
+
const streaming = queries.getStreamingConversations();
|
|
36
|
+
let cleared = 0;
|
|
37
|
+
for (const c of streaming) { if (!activeExecutions.has(c.id)) { queries.setIsStreaming(c.id, false); cleared++; } }
|
|
38
|
+
if (cleared > 0) debugLog(`[HEALTH] Cleared ${cleared} stale streaming flag(s)`);
|
|
39
|
+
} catch (e) { debugLog(`[HEALTH] Error: ${e.message}`); }
|
|
40
|
+
}, 5 * 60 * 1000);
|
|
41
|
+
|
|
42
|
+
installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
|
|
43
|
+
|
|
44
|
+
startACPTools().then(() => {
|
|
45
|
+
console.log('[ACP] On-demand startup enabled (ACP tools start when first used)');
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
const acpStatus = getACPStatus();
|
|
48
|
+
for (const s of acpStatus) { if (s.healthy) { const agent = discoveredAgents.find(a => a.id === s.id); if (agent) agent.acpPort = s.port; } }
|
|
49
|
+
if (acpStatus.length > 0) console.log(`[ACP] Tools ready: ${acpStatus.filter(s => s.healthy).map(s => s.id + ':' + s.port).join(', ') || 'none healthy yet'}`);
|
|
50
|
+
}, 6000);
|
|
51
|
+
}).catch(err => console.error('[ACP] Startup error:', err.message));
|
|
52
|
+
|
|
53
|
+
const toolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
|
|
54
|
+
queries.initializeToolInstallations(toolIds.map(id => ({ id })));
|
|
55
|
+
console.log('[TOOLS] Starting background provisioning...');
|
|
56
|
+
|
|
57
|
+
const toolBroadcaster = (evt) => {
|
|
58
|
+
broadcastSync(evt);
|
|
59
|
+
if (evt.type === 'tool_install_complete' || evt.type === 'tool_update_complete') {
|
|
60
|
+
const d = evt.data || {};
|
|
61
|
+
queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.version || null, installed_at: Date.now() });
|
|
62
|
+
queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'success', null);
|
|
63
|
+
} else if (evt.type === 'tool_install_failed' || evt.type === 'tool_update_failed') {
|
|
64
|
+
queries.updateToolStatus(evt.toolId, { status: 'failed', error_message: evt.data?.error });
|
|
65
|
+
queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'failed', evt.data?.error);
|
|
66
|
+
} else if (evt.type === 'tool_status_update') {
|
|
67
|
+
const d = evt.data || {};
|
|
68
|
+
if (d.installed) queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.installedVersion || null, installed_at: Date.now() });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
toolManager.autoProvision(toolBroadcaster)
|
|
73
|
+
.catch(err => console.error('[TOOLS] Auto-provision error:', err.message))
|
|
74
|
+
.then(() => {
|
|
75
|
+
const acpActors = ['opencode', 'kilo', 'codex'];
|
|
76
|
+
console.log(`[MACHINES] tool-install: ${toolInstallMachine.getMachineActors().size} actors, acp-server: ${acpActors.length} configured`);
|
|
77
|
+
console.log('[TOOLS] Starting periodic update checker...');
|
|
78
|
+
toolManager.startPeriodicUpdateCheck(toolBroadcaster);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
ensureModelsDownloaded().then(async ok => {
|
|
82
|
+
if (ok) console.log('[MODELS] Speech models ready');
|
|
83
|
+
else console.log('[MODELS] Speech model download failed');
|
|
84
|
+
try { const { getVoices } = await getSpeech(); broadcastSync({ type: 'voice_list', voices: getVoices() }); }
|
|
85
|
+
catch (err) { debugLog('[VOICE] Failed to broadcast voices: ' + err.message); broadcastSync({ type: 'voice_list', voices: [] }); }
|
|
86
|
+
}).catch(async err => {
|
|
87
|
+
console.error('[MODELS] Download error:', err.message);
|
|
88
|
+
try { const { getVoices } = await getSpeech(); broadcastSync({ type: 'voice_list', voices: getVoices() }); }
|
|
89
|
+
catch (err2) { debugLog('[VOICE] Failed to broadcast voices: ' + err2.message); broadcastSync({ type: 'voice_list', voices: [] }); }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
getSpeech().then(s => s.preloadTTS()).catch(e => debugLog('[TTS] Preload failed: ' + e.message));
|
|
93
|
+
performAutoImport();
|
|
94
|
+
setInterval(performAutoImport, 30000);
|
|
95
|
+
setInterval(performAgentHealthCheck, 30000);
|
|
96
|
+
|
|
97
|
+
const broadcastPM2 = (update) => {
|
|
98
|
+
const msg = JSON.stringify(update);
|
|
99
|
+
for (const client of pm2Subscribers) { if (client.readyState === 1) { try { client.send(msg); } catch (_) {} } }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const startPM2Monitoring = async () => {
|
|
103
|
+
try { await pm2Manager.connect(); await pm2Manager.startMonitoring(broadcastPM2); console.log('[PM2] Monitoring started'); }
|
|
104
|
+
catch (err) { console.log('[PM2] Not available:', err.message); broadcastPM2({ type: 'pm2_unavailable', reason: err.message, timestamp: Date.now() }); }
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
setTimeout(startPM2Monitoring, 2000);
|
|
108
|
+
setInterval(async () => {
|
|
109
|
+
if (!pm2Manager.connected && !pm2Manager.monitoring) {
|
|
110
|
+
try { const healed = await pm2Manager.heal(); if (healed.success) await pm2Manager.startMonitoring(broadcastPM2); } catch (_) {}
|
|
111
|
+
}
|
|
112
|
+
}, 30000);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { onServerReady, getJsonlWatcher };
|
|
116
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import PluginLoader from './plugin-loader.js';
|
|
5
|
+
|
|
6
|
+
export function createAutoImport({ queries, broadcastSync }) {
|
|
7
|
+
const importMtimeCache = new Map();
|
|
8
|
+
|
|
9
|
+
function hasIndexFilesChanged() {
|
|
10
|
+
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
11
|
+
if (!fs.existsSync(projectsDir)) return false;
|
|
12
|
+
let changed = false;
|
|
13
|
+
try {
|
|
14
|
+
const dirs = fs.readdirSync(projectsDir);
|
|
15
|
+
for (const d of dirs) {
|
|
16
|
+
const indexPath = path.join(projectsDir, d, 'sessions-index.json');
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs.statSync(indexPath);
|
|
19
|
+
const cached = importMtimeCache.get(indexPath);
|
|
20
|
+
if (!cached || cached < stat.mtimeMs) { importMtimeCache.set(indexPath, stat.mtimeMs); changed = true; }
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
}
|
|
23
|
+
} catch (_) {}
|
|
24
|
+
return changed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function performAutoImport() {
|
|
28
|
+
try {
|
|
29
|
+
if (!hasIndexFilesChanged()) return;
|
|
30
|
+
const imported = queries.importClaudeCodeConversations();
|
|
31
|
+
if (imported.length > 0) {
|
|
32
|
+
const importedCount = imported.filter(i => i.status === 'imported').length;
|
|
33
|
+
if (importedCount > 0) {
|
|
34
|
+
console.log(`[AUTO-IMPORT] Imported ${importedCount} new Claude Code conversations`);
|
|
35
|
+
broadcastSync({ type: 'conversations_updated', count: importedCount });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (err) { console.error('[AUTO-IMPORT] Error:', err.message); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { performAutoImport };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createDbRecovery({ queries, debugLog }) {
|
|
45
|
+
function performDbRecovery() {
|
|
46
|
+
try {
|
|
47
|
+
const cleanedUp = queries.cleanupOrphanedSessions(7);
|
|
48
|
+
if (cleanedUp > 0) debugLog(`[RECOVERY] Cleaned up ${cleanedUp} orphaned sessions`);
|
|
49
|
+
const longRunning = queries.getSessionsProcessingLongerThan(120);
|
|
50
|
+
if (longRunning.length > 0) {
|
|
51
|
+
for (const session of longRunning) queries.markSessionIncomplete(session.id, 'Timeout: processing exceeded 2 hours');
|
|
52
|
+
debugLog(`[RECOVERY] Marked ${longRunning.length} long-running sessions as incomplete`);
|
|
53
|
+
}
|
|
54
|
+
} catch (err) { console.error('[RECOVERY] Error:', err.message); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { performDbRecovery };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createPluginLoader({ pluginsDir, expressApp, BASE_URL }) {
|
|
61
|
+
const pluginLoader = new PluginLoader(pluginsDir);
|
|
62
|
+
|
|
63
|
+
async function loadPluginExtensions() {
|
|
64
|
+
try {
|
|
65
|
+
await pluginLoader.loadAllPlugins({ router: expressApp, baseUrl: BASE_URL, logger: console, env: process.env });
|
|
66
|
+
const names = Array.from(pluginLoader.registry.keys());
|
|
67
|
+
if (names.length > 0) {
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
const state = pluginLoader.get(name);
|
|
70
|
+
if (!state || !state.routes) continue;
|
|
71
|
+
for (const route of state.routes) {
|
|
72
|
+
const fullPath = BASE_URL + route.path;
|
|
73
|
+
const method = (route.method || 'GET').toLowerCase();
|
|
74
|
+
if (expressApp[method]) expressApp[method](fullPath, route.handler);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.log(`[PLUGINS] Loaded extensions: ${names.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
} catch (err) { console.error('[PLUGINS] Extension loading failed (non-fatal):', err.message); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { loadPluginExtensions };
|
|
83
|
+
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
6
5
|
import { LRUCache } from 'lru-cache';
|
|
7
|
-
import crypto from 'crypto';
|
|
8
6
|
const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
|
9
7
|
import { createExpressApp } from './lib/routes-upload.js';
|
|
10
8
|
import { queries } from './database.js';
|
|
@@ -13,37 +11,18 @@ import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descripto
|
|
|
13
11
|
import { discoverExternalACPServers, initializeAgentDiscovery } from './lib/agent-discovery.js';
|
|
14
12
|
import { startGeminiOAuth, exchangeGeminiOAuthCode, handleGeminiOAuthCallback, getGeminiOAuthStatus, getGeminiOAuthState } from './lib/oauth-gemini.js';
|
|
15
13
|
import { initSpeechManager, getSpeech, voiceCacheManager, modelDownloadState, ensureModelsDownloaded, eagerTTS } from './lib/speech-manager.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { register as registerUtilRoutes } from './lib/routes-util.js';
|
|
19
|
-
import { register as registerToolRoutes } from './lib/routes-tools.js';
|
|
20
|
-
import { register as registerThreadRoutes } from './lib/routes-threads.js';
|
|
21
|
-
import { register as registerDebugRoutes } from './lib/routes-debug.js';
|
|
22
|
-
import { register as registerConvRoutes } from './lib/routes-conversations.js';
|
|
23
|
-
import { register as registerAgentRoutes } from './lib/routes-agents.js';
|
|
24
|
-
import { register as registerMessagesRoutes } from './lib/routes-messages.js';
|
|
25
|
-
import { register as registerSessionsRoutes } from './lib/routes-sessions.js';
|
|
26
|
-
import { register as registerRunsRoutes } from './lib/routes-runs.js';
|
|
27
|
-
import { register as registerScriptsRoutes } from './lib/routes-scripts.js';
|
|
28
|
-
import { register as registerAgentActionsRoutes } from './lib/routes-agent-actions.js';
|
|
29
|
-
import { register as registerAuthConfigRoutes } from './lib/routes-auth-config.js';
|
|
14
|
+
import { createRegistry } from './lib/routes-registry.js';
|
|
15
|
+
import { BROADCAST_TYPES } from './lib/broadcast.js';
|
|
30
16
|
import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
|
|
31
17
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
32
18
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
33
19
|
import { encode as wsEncode } from './lib/codec.js';
|
|
34
20
|
import { parseBody, acceptsEncoding, compressAndSend, sendJSON } from './lib/http-utils.js';
|
|
35
21
|
import { createWsSetup } from './lib/ws-setup.js';
|
|
22
|
+
import { createHttpHandler } from './lib/http-handler.js';
|
|
23
|
+
import { createOnServerReady } from './lib/server-startup.js';
|
|
24
|
+
import { createAutoImport, createDbRecovery, createPluginLoader } from './lib/server-startup2.js';
|
|
36
25
|
const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); };
|
|
37
|
-
import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
|
|
38
|
-
import { register as registerConvHandlers2 } from './lib/ws-handlers-conv2.js';
|
|
39
|
-
import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
|
|
40
|
-
import { register as registerSessionHandlers2 } from './lib/ws-handlers-session2.js';
|
|
41
|
-
import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
|
|
42
|
-
import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
|
|
43
|
-
import { register as registerOAuthHandlers } from './lib/ws-handlers-oauth.js';
|
|
44
|
-
import { register as registerScriptHandlers } from './lib/ws-handlers-scripts.js';
|
|
45
|
-
import { register as registerQueueHandlers } from './lib/ws-handlers-queue.js';
|
|
46
|
-
import { register as registerMsgHandlers } from './lib/ws-handlers-msg.js';
|
|
47
26
|
import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-sdk-manager.js';
|
|
48
27
|
import * as execMachine from './lib/execution-machine.js';
|
|
49
28
|
import * as toolInstallMachine from './lib/tool-install-machine.js';
|
|
@@ -52,7 +31,6 @@ import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
|
|
|
52
31
|
import * as toolManager from './lib/tool-manager.js';
|
|
53
32
|
import { pm2Manager } from './lib/pm2-manager.js';
|
|
54
33
|
import CheckpointManager from './lib/checkpoint-manager.js';
|
|
55
|
-
import { JsonlWatcher } from './lib/jsonl-watcher.js';
|
|
56
34
|
import { createBroadcast } from './lib/broadcast.js';
|
|
57
35
|
import { createRecovery } from './lib/recovery.js';
|
|
58
36
|
import { parseRateLimitResetTime } from './lib/process-message-rate-limit.js';
|
|
@@ -63,17 +41,10 @@ import { buildSystemPrompt, getProviderConfigs, saveProviderConfig } from './lib
|
|
|
63
41
|
import { logError, errLogPath, makeCleanupExecution, makeGetModelsForAgent } from './lib/server-utils.js';
|
|
64
42
|
|
|
65
43
|
|
|
66
|
-
process.on('uncaughtException', (err, origin) => {
|
|
67
|
-
console.error('[FATAL] Uncaught exception (contained):', err.message, '| origin:', origin);
|
|
68
|
-
console.error(err.stack);
|
|
69
|
-
});
|
|
44
|
+
process.on('uncaughtException', (err, origin) => { console.error('[FATAL] Uncaught exception:', err.message, '| origin:', origin); console.error(err.stack); });
|
|
70
45
|
|
|
71
|
-
process.on('unhandledRejection', (reason,
|
|
72
|
-
console.error('[FATAL] Unhandled rejection (contained):', reason instanceof Error ? reason.message : reason);
|
|
73
|
-
if (reason instanceof Error) console.error(reason.stack);
|
|
74
|
-
});
|
|
46
|
+
process.on('unhandledRejection', (reason) => { console.error('[FATAL] Unhandled rejection:', reason instanceof Error ? reason.message : reason); if (reason instanceof Error) console.error(reason.stack); });
|
|
75
47
|
|
|
76
|
-
// Signal handlers registered after server initialization (see bottom of file)
|
|
77
48
|
process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - uncrashable)'); });
|
|
78
49
|
process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
|
|
79
50
|
process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
|
|
@@ -87,7 +58,6 @@ const activeProcessesByRunId = new Map();
|
|
|
87
58
|
const checkpointManager = new CheckpointManager(queries);
|
|
88
59
|
const STUCK_AGENT_THRESHOLD_MS = 1800000;
|
|
89
60
|
const NO_PID_GRACE_PERIOD_MS = 60000;
|
|
90
|
-
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 60000;
|
|
91
61
|
|
|
92
62
|
const debugLog = (msg) => {
|
|
93
63
|
const timestamp = new Date().toISOString();
|
|
@@ -124,226 +94,17 @@ const getModelsForAgent = makeGetModelsForAgent({ modelCache, discoveredAgents,
|
|
|
124
94
|
const _rateLimitMap = new LRUCache({ max: 1000, ttl: 60000 });
|
|
125
95
|
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '300', 10);
|
|
126
96
|
|
|
127
|
-
const server = http.createServer(async (req, res) => {
|
|
128
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
129
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
130
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
131
|
-
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
|
132
|
-
if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return;
|
|
133
|
-
|
|
134
|
-
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;
|
|
135
|
-
const hits = (_rateLimitMap.get(clientIp) || 0) + 1;
|
|
136
|
-
_rateLimitMap.set(clientIp, hits);
|
|
137
|
-
res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
|
|
138
|
-
res.setHeader('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX - hits));
|
|
139
|
-
if (hits > RATE_LIMIT_MAX) {
|
|
140
|
-
res.writeHead(429, { 'Retry-After': '60' });
|
|
141
|
-
res.end('Too Many Requests');
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const _pwd = process.env.PASSWORD;
|
|
146
|
-
if (_pwd) {
|
|
147
|
-
const _auth = req.headers['authorization'] || '';
|
|
148
|
-
let _ok = false;
|
|
149
|
-
if (_auth.startsWith('Basic ')) {
|
|
150
|
-
try {
|
|
151
|
-
const _decoded = Buffer.from(_auth.slice(6), 'base64').toString('utf8');
|
|
152
|
-
const _ci = _decoded.indexOf(':');
|
|
153
|
-
if (_ci !== -1) { const _p = _decoded.slice(_ci + 1); try { _ok = _p.length === _pwd.length && crypto.timingSafeEqual(Buffer.from(_p), Buffer.from(_pwd)); } catch { _ok = false; } }
|
|
154
|
-
} catch (_) {}
|
|
155
|
-
}
|
|
156
|
-
if (!_ok) {
|
|
157
|
-
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="agentgui"' });
|
|
158
|
-
res.end('Unauthorized');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const pathOnly = req.url.split('?')[0];
|
|
164
|
-
|
|
165
|
-
// Route file upload and fsbrowse requests through Express sub-app
|
|
166
|
-
if (pathOnly.startsWith(BASE_URL + '/api/upload/') || pathOnly.startsWith(BASE_URL + '/files/')) {
|
|
167
|
-
return expressApp(req, res);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (req.url === '/favicon.ico' || req.url === BASE_URL + '/favicon.ico') {
|
|
171
|
-
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#3b82f6"/><text x="50" y="68" font-size="50" font-family="sans-serif" font-weight="bold" fill="white" text-anchor="middle">G</text></svg>';
|
|
172
|
-
res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' });
|
|
173
|
-
res.end(svg);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; }
|
|
178
|
-
|
|
179
|
-
// Handle requests with or without BASE_URL prefix (for reverse proxy compatibility)
|
|
180
|
-
let routePath = req.url;
|
|
181
|
-
if (req.url.startsWith(BASE_URL + '/')) {
|
|
182
|
-
routePath = req.url.slice(BASE_URL.length);
|
|
183
|
-
} else if (req.url === BASE_URL) {
|
|
184
|
-
routePath = '/';
|
|
185
|
-
} else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') ||
|
|
186
|
-
req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' ||
|
|
187
|
-
req.url.startsWith('/conversations/')) {
|
|
188
|
-
// Allow requests without BASE_URL prefix for static files and known routes
|
|
189
|
-
// This supports reverse proxies that strip the BASE_URL prefix
|
|
190
|
-
routePath = req.url;
|
|
191
|
-
} else {
|
|
192
|
-
res.writeHead(404); res.end('Not found'); return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
routePath = routePath || '/';
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
// Remove query parameters from routePath for matching
|
|
199
|
-
const pathOnly = routePath.split('?')[0];
|
|
200
|
-
|
|
201
|
-
if (pathOnly === '/oauth2callback' && req.method === 'GET') {
|
|
202
|
-
await handleGeminiOAuthCallback(req, res, PORT);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
|
|
207
|
-
await handleCodexOAuthCallback(req, res, PORT);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (pathOnly === '/api/health' && req.method === 'GET') {
|
|
212
|
-
let dbStatus = { ok: true };
|
|
213
|
-
try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; }
|
|
214
|
-
const queueSizes = {};
|
|
215
|
-
for (const [k, v] of messageQueues) queueSizes[k] = v.length;
|
|
216
|
-
sendJSON(req, res, 200, {
|
|
217
|
-
status: 'ok',
|
|
218
|
-
version: PKG_VERSION,
|
|
219
|
-
uptime: process.uptime(),
|
|
220
|
-
agents: discoveredAgents.length,
|
|
221
|
-
activeExecutions: activeExecutions.size,
|
|
222
|
-
wsClients: wss.clients.size,
|
|
223
|
-
memory: process.memoryUsage(),
|
|
224
|
-
acp: getACPStatus(),
|
|
225
|
-
db: dbStatus,
|
|
226
|
-
queueSizes
|
|
227
|
-
});
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const convHandler = _convRoutes._match(req.method, pathOnly);
|
|
232
|
-
if (convHandler) { await convHandler(req, res); return; }
|
|
233
|
-
|
|
234
|
-
const messagesHandler = _messagesRoutes._match(req.method, pathOnly);
|
|
235
|
-
if (messagesHandler) { await messagesHandler(req, res); return; }
|
|
236
|
-
|
|
237
|
-
const sessionsHandler = _sessionsRoutes._match(req.method, pathOnly);
|
|
238
|
-
if (sessionsHandler) { await sessionsHandler(req, res); return; }
|
|
239
|
-
|
|
240
|
-
const scriptsHandler = _scriptsRoutes._match(req.method, pathOnly);
|
|
241
|
-
if (scriptsHandler) { await scriptsHandler(req, res); return; }
|
|
242
|
-
|
|
243
|
-
const runsHandlerA = _runsRoutes._match(req.method, pathOnly);
|
|
244
|
-
if (runsHandlerA) { await runsHandlerA(req, res); return; }
|
|
245
|
-
|
|
246
|
-
const agentHandler = _agentRoutes._match(req.method, pathOnly);
|
|
247
|
-
if (agentHandler) { await agentHandler(req, res); return; }
|
|
248
|
-
|
|
249
|
-
const oauthHandler = _oauthRoutes._match(req.method, pathOnly);
|
|
250
|
-
if (oauthHandler) { await oauthHandler(req, res); return; }
|
|
251
|
-
|
|
252
|
-
const agentActionsHandler = _agentActionsRoutes._match(req.method, pathOnly);
|
|
253
|
-
if (agentActionsHandler) { await agentActionsHandler(req, res); return; }
|
|
254
|
-
|
|
255
|
-
const authConfigHandler = _authConfigRoutes._match(req.method, pathOnly);
|
|
256
|
-
if (authConfigHandler) { await authConfigHandler(req, res); return; }
|
|
257
|
-
|
|
258
|
-
const speechHandler = _speechRoutes._match(req.method, pathOnly);
|
|
259
|
-
if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
|
|
260
|
-
|
|
261
|
-
const utilHandler = _utilRoutes._match(req.method, pathOnly);
|
|
262
|
-
if (utilHandler) { await utilHandler(req, res); return; }
|
|
263
|
-
|
|
264
|
-
const threadHandler = _threadRoutes._match(req.method, pathOnly);
|
|
265
|
-
if (threadHandler) { await threadHandler(req, res); return; }
|
|
266
|
-
|
|
267
|
-
if (routePath.startsWith('/api/image/')) {
|
|
268
|
-
const imagePath = routePath.slice('/api/image/'.length);
|
|
269
|
-
const decodedPath = decodeURIComponent(imagePath);
|
|
270
|
-
const expandedPath = decodedPath.startsWith('~') ?
|
|
271
|
-
decodedPath.replace('~', os.homedir()) : decodedPath;
|
|
272
|
-
const normalizedPath = path.normalize(expandedPath);
|
|
273
|
-
const isWindows = os.platform() === 'win32';
|
|
274
|
-
const isAbsolute = isWindows ? /^[A-Za-z]:[\\\/]/.test(normalizedPath) : normalizedPath.startsWith('/');
|
|
275
|
-
if (!isAbsolute || normalizedPath.includes('..')) {
|
|
276
|
-
res.writeHead(403); res.end('Forbidden'); return;
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
if (!fs.existsSync(normalizedPath)) { res.writeHead(404); res.end('Not found'); return; }
|
|
280
|
-
const ext = path.extname(normalizedPath).toLowerCase();
|
|
281
|
-
const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
|
|
282
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
283
|
-
const fileContent = fs.readFileSync(normalizedPath);
|
|
284
|
-
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
|
|
285
|
-
res.end(fileContent);
|
|
286
|
-
} catch (err) {
|
|
287
|
-
sendJSON(req, res, 400, { error: err.message });
|
|
288
|
-
}
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Handle conversation detail routes - serve index.html for client-side routing
|
|
293
|
-
if (pathOnly.match(/^\/conversations\/[^\/]+$/)) {
|
|
294
|
-
const indexPath = path.join(staticDir, 'index.html');
|
|
295
|
-
serveFile(indexPath, res, req);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
let filePath = routePath === '/' ? '/index.html' : routePath;
|
|
300
|
-
filePath = path.join(staticDir, filePath);
|
|
301
|
-
const normalizedPath = path.normalize(filePath);
|
|
302
|
-
if (!normalizedPath.startsWith(staticDir)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
303
|
-
|
|
304
|
-
fs.stat(filePath, (err, stats) => {
|
|
305
|
-
if (err) { res.writeHead(404); res.end('Not found'); return; }
|
|
306
|
-
if (stats.isDirectory()) {
|
|
307
|
-
filePath = path.join(filePath, 'index.html');
|
|
308
|
-
fs.stat(filePath, (err2) => {
|
|
309
|
-
if (err2) { res.writeHead(404); res.end('Not found'); return; }
|
|
310
|
-
serveFile(filePath, res, req);
|
|
311
|
-
});
|
|
312
|
-
} else {
|
|
313
|
-
serveFile(filePath, res, req);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
} catch (e) {
|
|
317
|
-
console.error('Server error:', e.message);
|
|
318
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
|
|
322
97
|
const _assetDeps = { compressAndSend, acceptsEncoding, watch, BASE_URL, PKG_VERSION };
|
|
323
98
|
function serveFile(filePath, res, req) { return _serveFile(filePath, res, req, _assetDeps); }
|
|
324
99
|
|
|
100
|
+
const _routes = {};
|
|
101
|
+
const server = http.createServer(createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, get wss() { return wss; }, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap: _rateLimitMap, routes: _routes, handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT }));
|
|
102
|
+
|
|
325
103
|
let broadcastSeq = 0;
|
|
326
104
|
const syncClients = new Set();
|
|
327
105
|
const subscriptionIndex = new Map();
|
|
328
106
|
const pm2Subscribers = new Set();
|
|
329
107
|
|
|
330
|
-
const BROADCAST_TYPES = new Set([
|
|
331
|
-
'message_created', 'conversation_created', 'conversation_updated',
|
|
332
|
-
'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated', 'queue_item_dequeued',
|
|
333
|
-
'rate_limit_hit', 'rate_limit_clear',
|
|
334
|
-
'script_started', 'script_stopped', 'script_output',
|
|
335
|
-
'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list',
|
|
336
|
-
'streaming_start', 'streaming_progress', 'streaming_complete', 'streaming_error',
|
|
337
|
-
'tool_install_started', 'tool_install_progress', 'tool_install_complete', 'tool_install_failed',
|
|
338
|
-
'tool_update_progress', 'tool_update_complete', 'tool_update_failed',
|
|
339
|
-
'tool_status_update', 'tool_update_available',
|
|
340
|
-
'tools_update_started', 'tools_update_complete', 'tools_refresh_complete',
|
|
341
|
-
'pm2_monit_update', 'pm2_monitoring_started', 'pm2_monitoring_stopped',
|
|
342
|
-
'pm2_list_response', 'pm2_start_response', 'pm2_stop_response',
|
|
343
|
-
'pm2_restart_response', 'pm2_delete_response', 'pm2_logs_response',
|
|
344
|
-
'pm2_flush_logs_response', 'pm2_ping_response', 'pm2_unavailable'
|
|
345
|
-
]);
|
|
346
|
-
|
|
347
108
|
const wsOptimizer = new WSOptimizer();
|
|
348
109
|
|
|
349
110
|
const broadcastSync = createBroadcast({
|
|
@@ -355,14 +116,7 @@ const broadcastSync = createBroadcast({
|
|
|
355
116
|
});
|
|
356
117
|
|
|
357
118
|
const cleanupExecution = makeCleanupExecution({ execMachine, activeExecutions, queries, broadcastSync, debugLog });
|
|
358
|
-
|
|
359
|
-
// Wire up process-message factories now that broadcastSync and all deps are available
|
|
360
|
-
const _mqDeps = {
|
|
361
|
-
queries, messageQueues, activeExecutions, rateLimitState, execMachine,
|
|
362
|
-
broadcastSync, cleanupExecution, debugLog,
|
|
363
|
-
getProcessMessageWithStreaming: () => processMessageWithStreaming
|
|
364
|
-
};
|
|
365
|
-
const { scheduleRetry, drainMessageQueue } = createMessageQueue(_mqDeps);
|
|
119
|
+
const { scheduleRetry, drainMessageQueue } = createMessageQueue({ queries, messageQueues, activeExecutions, rateLimitState, execMachine, broadcastSync, cleanupExecution, debugLog, getProcessMessageWithStreaming: () => processMessageWithStreaming });
|
|
366
120
|
|
|
367
121
|
const { processMessageWithStreaming } = createProcessMessage({
|
|
368
122
|
queries, activeExecutions, rateLimitState, execMachine,
|
|
@@ -373,78 +127,9 @@ const { processMessageWithStreaming } = createProcessMessage({
|
|
|
373
127
|
scheduleRetry, drainMessageQueue, createEventHandler
|
|
374
128
|
});
|
|
375
129
|
|
|
376
|
-
// WebSocket protocol router
|
|
377
130
|
const wsRouter = new WsRouter();
|
|
131
|
+
createRegistry(wsRouter, { queries, sendJSON, parseBody, broadcastSync, debugLog, PORT, BASE_URL, rootDir, STARTUP_CWD, PKG_VERSION, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, activeScripts, messageQueues, rateLimitState, cleanupExecution, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, logError, toolManager, syncClients, wsOptimizer, errLogPath, getJsonlWatcher: () => getJsonlWatcher(), routes: _routes });
|
|
378
132
|
|
|
379
|
-
initSpeechManager({ broadcastSync, syncClients, queries });
|
|
380
|
-
const _speechRoutes = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync, debugLog });
|
|
381
|
-
const _oauthRoutes = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
|
|
382
|
-
const _utilRoutes = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
|
|
383
|
-
const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
|
|
384
|
-
const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
|
|
385
|
-
const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath: errLogPath });
|
|
386
|
-
const _convRoutes = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
|
|
387
|
-
const _agentRoutes = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
|
|
388
|
-
const _messagesRoutes = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });
|
|
389
|
-
const _sessionsRoutes = registerSessionsRoutes({ queries, sendJSON, activeExecutions, rateLimitState, debugLog });
|
|
390
|
-
const _runsRoutes = registerRunsRoutes({ sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD });
|
|
391
|
-
const _scriptsRoutes = registerScriptsRoutes({ sendJSON, parseBody, queries, broadcastSync, activeScripts, activeExecutions, processMessageWithStreaming, STARTUP_CWD });
|
|
392
|
-
const _agentActionsRoutes = registerAgentActionsRoutes({ sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir });
|
|
393
|
-
const _authConfigRoutes = registerAuthConfigRoutes({ sendJSON, parseBody, getProviderConfigs, saveProviderConfig });
|
|
394
|
-
|
|
395
|
-
registerConvHandlers(wsRouter, {
|
|
396
|
-
queries, activeExecutions, rateLimitState,
|
|
397
|
-
broadcastSync, processMessageWithStreaming, cleanupExecution,
|
|
398
|
-
getJsonlWatcher: () => jsonlWatcher
|
|
399
|
-
});
|
|
400
|
-
registerConvHandlers2(wsRouter, {
|
|
401
|
-
queries, activeExecutions, rateLimitState,
|
|
402
|
-
broadcastSync, processMessageWithStreaming, cleanupExecution,
|
|
403
|
-
getJsonlWatcher: () => jsonlWatcher
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
registerMsgHandlers(wsRouter, {
|
|
407
|
-
queries, activeExecutions, messageQueues,
|
|
408
|
-
broadcastSync, processMessageWithStreaming, logError
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
registerQueueHandlers(wsRouter, { queries, messageQueues, broadcastSync });
|
|
412
|
-
|
|
413
|
-
debugLog('[INIT] registerSessionHandlers, agents: ' + discoveredAgents.length);
|
|
414
|
-
registerSessionHandlers(wsRouter, {
|
|
415
|
-
db: queries, discoveredAgents, modelCache,
|
|
416
|
-
getAgentDescriptor, activeScripts, broadcastSync,
|
|
417
|
-
startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
|
|
418
|
-
});
|
|
419
|
-
registerSessionHandlers2(wsRouter, {
|
|
420
|
-
discoveredAgents, modelCache, activeScripts, broadcastSync,
|
|
421
|
-
startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
|
|
422
|
-
});
|
|
423
|
-
debugLog('[INIT] registerSessionHandlers completed');
|
|
424
|
-
|
|
425
|
-
registerRunHandlers(wsRouter, {
|
|
426
|
-
queries, discoveredAgents, activeExecutions, activeProcessesByRunId,
|
|
427
|
-
broadcastSync, processMessageWithStreaming, cleanupExecution
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
registerUtilHandlers(wsRouter, {
|
|
431
|
-
queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
|
|
432
|
-
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
433
|
-
STARTUP_CWD, voiceCacheManager, toolManager, discoveredAgents
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
registerScriptHandlers(wsRouter, {
|
|
437
|
-
queries, broadcastSync, STARTUP_CWD, activeScripts
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
registerOAuthHandlers(wsRouter, {
|
|
441
|
-
startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }),
|
|
442
|
-
exchangeGeminiOAuthCode,
|
|
443
|
-
geminiOAuthState: getGeminiOAuthState,
|
|
444
|
-
startCodexOAuth: (req) => startCodexOAuth(req, { PORT, BASE_URL }),
|
|
445
|
-
exchangeCodexOAuthCode,
|
|
446
|
-
codexOAuthState: getCodexOAuthState,
|
|
447
|
-
});
|
|
448
133
|
|
|
449
134
|
const { wss, hotReloadClients } = createWsSetup(server, {
|
|
450
135
|
BASE_URL, watch, staticDir, _assetCache, htmlState, sendWs, wsRouter, debugLog,
|
|
@@ -470,7 +155,7 @@ const { killActiveExecutions, recoverStaleSessions, resumeInterruptedStreams, is
|
|
|
470
155
|
process.on('SIGTERM', () => {
|
|
471
156
|
console.log('[SIGNAL] SIGTERM received - graceful shutdown');
|
|
472
157
|
killActiveExecutions();
|
|
473
|
-
if (
|
|
158
|
+
const _jw = getJsonlWatcher(); if (_jw) try { _jw.stop(); } catch (_) {}
|
|
474
159
|
try { pm2Manager.disconnect(); } catch (_) {}
|
|
475
160
|
stopACPTools().catch(() => {}).finally(() => {
|
|
476
161
|
try { wss.close(() => server.close(() => process.exit(0))); } catch (_) { process.exit(0); }
|
|
@@ -494,253 +179,19 @@ server.on('error', (err) => {
|
|
|
494
179
|
}
|
|
495
180
|
});
|
|
496
181
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// Clear tool status cache on startup to ensure fresh detection
|
|
501
|
-
toolManager.clearStatusCache();
|
|
502
|
-
|
|
503
|
-
console.log(`GMGUI running on http://localhost:${PORT}${BASE_URL}/`);
|
|
504
|
-
console.log(`Agents: ${discoveredAgents.map(a => a.name).join(', ') || 'none'}`);
|
|
505
|
-
console.log(`Hot reload: ${watch ? 'on' : 'off'}`);
|
|
506
|
-
|
|
507
|
-
const deletedCount = queries.cleanupEmptyConversations();
|
|
508
|
-
if (deletedCount > 0) {
|
|
509
|
-
console.log(`Cleaned up ${deletedCount} empty conversation(s) on startup`);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
recoverStaleSessions();
|
|
513
|
-
warmAssetCache(staticDir);
|
|
514
|
-
|
|
515
|
-
// Run DB cleanup on startup and every 6 hours
|
|
516
|
-
try { queries.cleanup(); console.log('[cleanup] Initial DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
|
|
517
|
-
setInterval(() => {
|
|
518
|
-
try { queries.cleanup(); console.log('[cleanup] Scheduled DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
|
|
519
|
-
}, 6 * 60 * 60 * 1000);
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
|
|
523
|
-
jsonlWatcher.start();
|
|
524
|
-
console.log('[JSONL] Watcher started');
|
|
525
|
-
} catch (err) {
|
|
526
|
-
console.error('[JSONL] Watcher failed to start:', err.message);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
530
|
-
|
|
531
|
-
setInterval(() => {
|
|
532
|
-
try {
|
|
533
|
-
const streaming = queries.getStreamingConversations();
|
|
534
|
-
let cleared = 0;
|
|
535
|
-
for (const c of streaming) {
|
|
536
|
-
if (!activeExecutions.has(c.id)) {
|
|
537
|
-
queries.setIsStreaming(c.id, false);
|
|
538
|
-
cleared++;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (cleared > 0) debugLog(`[HEALTH] Cleared ${cleared} stale streaming flag(s)`);
|
|
542
|
-
} catch (e) { debugLog(`[HEALTH] Error: ${e.message}`); }
|
|
543
|
-
}, 5 * 60 * 1000);
|
|
544
|
-
|
|
545
|
-
installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
|
|
546
|
-
|
|
547
|
-
startACPTools().then(() => {
|
|
548
|
-
console.log('[ACP] On-demand startup enabled (ACP tools start when first used)');
|
|
549
|
-
setTimeout(() => {
|
|
550
|
-
const acpStatus = getACPStatus();
|
|
551
|
-
for (const s of acpStatus) {
|
|
552
|
-
if (s.healthy) {
|
|
553
|
-
const agent = discoveredAgents.find(a => a.id === s.id);
|
|
554
|
-
if (agent) { agent.acpPort = s.port; }
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (acpStatus.length > 0) {
|
|
558
|
-
console.log(`[ACP] Tools ready: ${acpStatus.filter(s => s.healthy).map(s => s.id + ':' + s.port).join(', ') || 'none healthy yet'}`);
|
|
559
|
-
}
|
|
560
|
-
}, 6000);
|
|
561
|
-
}).catch(err => console.error('[ACP] Startup error:', err.message));
|
|
562
|
-
|
|
563
|
-
const toolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
|
|
564
|
-
queries.initializeToolInstallations(toolIds.map(id => ({ id })));
|
|
565
|
-
console.log('[TOOLS] Starting background provisioning...');
|
|
566
|
-
|
|
567
|
-
// Create broadcast handler for tool events
|
|
568
|
-
const toolBroadcaster = (evt) => {
|
|
569
|
-
broadcastSync(evt);
|
|
570
|
-
if (evt.type === 'tool_install_complete' || evt.type === 'tool_update_complete') {
|
|
571
|
-
const d = evt.data || {};
|
|
572
|
-
queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.version || null, installed_at: Date.now() });
|
|
573
|
-
queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'success', null);
|
|
574
|
-
} else if (evt.type === 'tool_install_failed' || evt.type === 'tool_update_failed') {
|
|
575
|
-
queries.updateToolStatus(evt.toolId, { status: 'failed', error_message: evt.data?.error });
|
|
576
|
-
queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'failed', evt.data?.error);
|
|
577
|
-
} else if (evt.type === 'tool_status_update') {
|
|
578
|
-
const d = evt.data || {};
|
|
579
|
-
if (d.installed) {
|
|
580
|
-
queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.installedVersion || null, installed_at: Date.now() });
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
// Initial provisioning (blocks until complete)
|
|
586
|
-
toolManager.autoProvision(toolBroadcaster)
|
|
587
|
-
.catch(err => console.error('[TOOLS] Auto-provision error:', err.message))
|
|
588
|
-
.then(() => {
|
|
589
|
-
const acpActors = ['opencode', 'kilo', 'codex'];
|
|
590
|
-
const execActorCount = execMachine.stopAll ? 0 : 0;
|
|
591
|
-
console.log(`[MACHINES] tool-install: ${toolInstallMachine.getMachineActors().size} actors, acp-server: ${acpActors.length} configured`);
|
|
592
|
-
console.log('[TOOLS] Starting periodic update checker...');
|
|
593
|
-
toolManager.startPeriodicUpdateCheck(toolBroadcaster);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
ensureModelsDownloaded().then(async ok => {
|
|
597
|
-
if (ok) console.log('[MODELS] Speech models ready');
|
|
598
|
-
else console.log('[MODELS] Speech model download failed');
|
|
599
|
-
try {
|
|
600
|
-
const { getVoices } = await getSpeech();
|
|
601
|
-
const voices = getVoices();
|
|
602
|
-
broadcastSync({ type: 'voice_list', voices });
|
|
603
|
-
} catch (err) {
|
|
604
|
-
debugLog('[VOICE] Failed to broadcast voices: ' + err.message);
|
|
605
|
-
broadcastSync({ type: 'voice_list', voices: [] });
|
|
606
|
-
}
|
|
607
|
-
}).catch(async err => {
|
|
608
|
-
console.error('[MODELS] Download error:', err.message);
|
|
609
|
-
try {
|
|
610
|
-
const { getVoices } = await getSpeech();
|
|
611
|
-
const voices = getVoices();
|
|
612
|
-
broadcastSync({ type: 'voice_list', voices });
|
|
613
|
-
} catch (err2) {
|
|
614
|
-
debugLog('[VOICE] Failed to broadcast voices: ' + err2.message);
|
|
615
|
-
broadcastSync({ type: 'voice_list', voices: [] });
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
getSpeech().then(s => s.preloadTTS()).catch(e => debugLog('[TTS] Preload failed: ' + e.message));
|
|
620
|
-
|
|
621
|
-
performAutoImport();
|
|
622
|
-
|
|
623
|
-
setInterval(performAutoImport, 30000);
|
|
624
|
-
|
|
625
|
-
setInterval(performAgentHealthCheck, 30000);
|
|
626
|
-
|
|
627
|
-
// Initialize PM2 monitoring - only when PM2 daemon is available
|
|
628
|
-
const broadcastPM2 = (update) => {
|
|
629
|
-
const msg = JSON.stringify(update);
|
|
630
|
-
for (const client of pm2Subscribers) {
|
|
631
|
-
if (client.readyState === 1) { try { client.send(msg); } catch (_) {} }
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
const startPM2Monitoring = async () => {
|
|
636
|
-
try {
|
|
637
|
-
await pm2Manager.connect();
|
|
638
|
-
await pm2Manager.startMonitoring(broadcastPM2);
|
|
639
|
-
console.log('[PM2] Monitoring started');
|
|
640
|
-
} catch (err) {
|
|
641
|
-
console.log('[PM2] Not available:', err.message);
|
|
642
|
-
broadcastPM2({ type: 'pm2_unavailable', reason: err.message, timestamp: Date.now() });
|
|
643
|
-
}
|
|
644
|
-
};
|
|
182
|
+
const { performAutoImport } = createAutoImport({ queries, broadcastSync });
|
|
183
|
+
const { performDbRecovery } = createDbRecovery({ queries, debugLog });
|
|
184
|
+
const { loadPluginExtensions } = createPluginLoader({ pluginsDir: path.join(__dirname, 'lib', 'plugins'), expressApp, BASE_URL });
|
|
645
185
|
|
|
646
|
-
|
|
186
|
+
setInterval(performDbRecovery, 300000);
|
|
647
187
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}, 30000);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const importMtimeCache = new Map();
|
|
659
|
-
|
|
660
|
-
function hasIndexFilesChanged() {
|
|
661
|
-
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
662
|
-
if (!fs.existsSync(projectsDir)) return false;
|
|
663
|
-
let changed = false;
|
|
664
|
-
try {
|
|
665
|
-
const dirs = fs.readdirSync(projectsDir);
|
|
666
|
-
for (const d of dirs) {
|
|
667
|
-
const indexPath = path.join(projectsDir, d, 'sessions-index.json');
|
|
668
|
-
try {
|
|
669
|
-
const stat = fs.statSync(indexPath);
|
|
670
|
-
const cached = importMtimeCache.get(indexPath);
|
|
671
|
-
if (!cached || cached < stat.mtimeMs) {
|
|
672
|
-
importMtimeCache.set(indexPath, stat.mtimeMs);
|
|
673
|
-
changed = true;
|
|
674
|
-
}
|
|
675
|
-
} catch (_) {}
|
|
676
|
-
}
|
|
677
|
-
} catch (_) {}
|
|
678
|
-
return changed;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function performAutoImport() {
|
|
682
|
-
try {
|
|
683
|
-
if (!hasIndexFilesChanged()) return;
|
|
684
|
-
const imported = queries.importClaudeCodeConversations();
|
|
685
|
-
if (imported.length > 0) {
|
|
686
|
-
const importedCount = imported.filter(i => i.status === 'imported').length;
|
|
687
|
-
if (importedCount > 0) {
|
|
688
|
-
console.log(`[AUTO-IMPORT] Imported ${importedCount} new Claude Code conversations`);
|
|
689
|
-
broadcastSync({ type: 'conversations_updated', count: importedCount });
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
} catch (err) {
|
|
693
|
-
console.error('[AUTO-IMPORT] Error:', err.message);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function performRecovery() {
|
|
698
|
-
try {
|
|
699
|
-
// Cleanup orphaned sessions (older than 7 days)
|
|
700
|
-
const cleanedUp = queries.cleanupOrphanedSessions(7);
|
|
701
|
-
if (cleanedUp > 0) {
|
|
702
|
-
debugLog(`[RECOVERY] Cleaned up ${cleanedUp} orphaned sessions`);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Mark sessions incomplete if they've been processing too long (>2 hours)
|
|
706
|
-
const longRunning = queries.getSessionsProcessingLongerThan(120);
|
|
707
|
-
if (longRunning.length > 0) {
|
|
708
|
-
for (const session of longRunning) {
|
|
709
|
-
queries.markSessionIncomplete(session.id, 'Timeout: processing exceeded 2 hours');
|
|
710
|
-
}
|
|
711
|
-
debugLog(`[RECOVERY] Marked ${longRunning.length} long-running sessions as incomplete`);
|
|
712
|
-
}
|
|
713
|
-
} catch (err) {
|
|
714
|
-
console.error('[RECOVERY] Error:', err.message);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Run recovery every 5 minutes
|
|
719
|
-
setInterval(performRecovery, 300000);
|
|
720
|
-
|
|
721
|
-
// Load plugins as extensions (additive, not replacing core routes)
|
|
722
|
-
import PluginLoader from './lib/plugin-loader.js';
|
|
723
|
-
const pluginLoader = new PluginLoader(path.join(__dirname, 'lib', 'plugins'));
|
|
724
|
-
async function loadPluginExtensions() {
|
|
725
|
-
try {
|
|
726
|
-
await pluginLoader.loadAllPlugins({ router: expressApp, baseUrl: BASE_URL, logger: console, env: process.env });
|
|
727
|
-
const names = Array.from(pluginLoader.registry.keys());
|
|
728
|
-
if (names.length > 0) {
|
|
729
|
-
for (const name of names) {
|
|
730
|
-
const state = pluginLoader.get(name);
|
|
731
|
-
if (!state || !state.routes) continue;
|
|
732
|
-
for (const route of state.routes) {
|
|
733
|
-
const fullPath = BASE_URL + route.path;
|
|
734
|
-
const method = (route.method || 'GET').toLowerCase();
|
|
735
|
-
if (expressApp[method]) expressApp[method](fullPath, route.handler);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
console.log(`[PLUGINS] Loaded extensions: ${names.join(', ')}`);
|
|
739
|
-
}
|
|
740
|
-
} catch (err) {
|
|
741
|
-
console.error('[PLUGINS] Extension loading failed (non-fatal):', err.message);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
188
|
+
const { onServerReady, getJsonlWatcher } = createOnServerReady({
|
|
189
|
+
queries, broadcastSync, warmAssetCache, staticDir, toolManager, discoveredAgents,
|
|
190
|
+
PORT, BASE_URL, watch, ownedSessionIds, resumeInterruptedStreams, activeExecutions,
|
|
191
|
+
debugLog, installGMAgentConfigs, startACPTools, getACPStatus, execMachine,
|
|
192
|
+
toolInstallMachine, getSpeech, ensureModelsDownloaded, performAutoImport,
|
|
193
|
+
performAgentHealthCheck, pm2Manager, pm2Subscribers, recoverStaleSessions
|
|
194
|
+
});
|
|
744
195
|
|
|
745
196
|
server.listen(PORT, () => {
|
|
746
197
|
onServerReady();
|