agentgui 1.0.844 → 1.0.846

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -1,13 +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
- import { WebSocketServer } from 'ws';
7
- import { execSync, spawn } from 'child_process';
8
5
  import { LRUCache } from 'lru-cache';
9
- import { createRequire } from 'module';
10
- import crypto from 'crypto';
11
6
  const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
12
7
  import { createExpressApp } from './lib/routes-upload.js';
13
8
  import { queries } from './database.js';
@@ -35,6 +30,10 @@ import { WSOptimizer } from './lib/ws-optimizer.js';
35
30
  import { WsRouter } from './lib/ws-protocol.js';
36
31
  import { encode as wsEncode } from './lib/codec.js';
37
32
  import { parseBody, acceptsEncoding, compressAndSend, sendJSON } from './lib/http-utils.js';
33
+ import { createWsSetup } from './lib/ws-setup.js';
34
+ import { createHttpHandler } from './lib/http-handler.js';
35
+ import { createOnServerReady } from './lib/server-startup.js';
36
+ import { createAutoImport, createDbRecovery, createPluginLoader } from './lib/server-startup2.js';
38
37
  const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); };
39
38
  import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
40
39
  import { register as registerConvHandlers2 } from './lib/ws-handlers-conv2.js';
@@ -54,7 +53,6 @@ import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
54
53
  import * as toolManager from './lib/tool-manager.js';
55
54
  import { pm2Manager } from './lib/pm2-manager.js';
56
55
  import CheckpointManager from './lib/checkpoint-manager.js';
57
- import { JsonlWatcher } from './lib/jsonl-watcher.js';
58
56
  import { createBroadcast } from './lib/broadcast.js';
59
57
  import { createRecovery } from './lib/recovery.js';
60
58
  import { parseRateLimitResetTime } from './lib/process-message-rate-limit.js';
@@ -75,7 +73,6 @@ process.on('unhandledRejection', (reason, promise) => {
75
73
  if (reason instanceof Error) console.error(reason.stack);
76
74
  });
77
75
 
78
- // Signal handlers registered after server initialization (see bottom of file)
79
76
  process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - uncrashable)'); });
80
77
  process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
81
78
  process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
@@ -126,271 +123,34 @@ const getModelsForAgent = makeGetModelsForAgent({ modelCache, discoveredAgents,
126
123
  const _rateLimitMap = new LRUCache({ max: 1000, ttl: 60000 });
127
124
  const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '300', 10);
128
125
 
129
- const server = http.createServer(async (req, res) => {
130
- res.setHeader('Access-Control-Allow-Origin', '*');
131
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
132
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
133
- if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
134
- if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return;
135
-
136
- const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;
137
- const hits = (_rateLimitMap.get(clientIp) || 0) + 1;
138
- _rateLimitMap.set(clientIp, hits);
139
- res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
140
- res.setHeader('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX - hits));
141
- if (hits > RATE_LIMIT_MAX) {
142
- res.writeHead(429, { 'Retry-After': '60' });
143
- res.end('Too Many Requests');
144
- return;
145
- }
146
-
147
- const _pwd = process.env.PASSWORD;
148
- if (_pwd) {
149
- const _auth = req.headers['authorization'] || '';
150
- let _ok = false;
151
- if (_auth.startsWith('Basic ')) {
152
- try {
153
- const _decoded = Buffer.from(_auth.slice(6), 'base64').toString('utf8');
154
- const _ci = _decoded.indexOf(':');
155
- 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; } }
156
- } catch (_) {}
157
- }
158
- if (!_ok) {
159
- res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="agentgui"' });
160
- res.end('Unauthorized');
161
- return;
162
- }
163
- }
164
-
165
- const pathOnly = req.url.split('?')[0];
166
-
167
- // Route file upload and fsbrowse requests through Express sub-app
168
- if (pathOnly.startsWith(BASE_URL + '/api/upload/') || pathOnly.startsWith(BASE_URL + '/files/')) {
169
- return expressApp(req, res);
170
- }
171
-
172
- if (req.url === '/favicon.ico' || req.url === BASE_URL + '/favicon.ico') {
173
- 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>';
174
- res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' });
175
- res.end(svg);
176
- return;
177
- }
178
-
179
- if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; }
180
-
181
- // Handle requests with or without BASE_URL prefix (for reverse proxy compatibility)
182
- let routePath = req.url;
183
- if (req.url.startsWith(BASE_URL + '/')) {
184
- routePath = req.url.slice(BASE_URL.length);
185
- } else if (req.url === BASE_URL) {
186
- routePath = '/';
187
- } else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') ||
188
- req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' ||
189
- req.url.startsWith('/conversations/')) {
190
- // Allow requests without BASE_URL prefix for static files and known routes
191
- // This supports reverse proxies that strip the BASE_URL prefix
192
- routePath = req.url;
193
- } else {
194
- res.writeHead(404); res.end('Not found'); return;
195
- }
196
-
197
- routePath = routePath || '/';
198
-
199
- try {
200
- // Remove query parameters from routePath for matching
201
- const pathOnly = routePath.split('?')[0];
202
-
203
- if (pathOnly === '/oauth2callback' && req.method === 'GET') {
204
- await handleGeminiOAuthCallback(req, res, PORT);
205
- return;
206
- }
207
-
208
- if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
209
- await handleCodexOAuthCallback(req, res, PORT);
210
- return;
211
- }
212
-
213
- if (pathOnly === '/api/health' && req.method === 'GET') {
214
- let dbStatus = { ok: true };
215
- try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; }
216
- const queueSizes = {};
217
- for (const [k, v] of messageQueues) queueSizes[k] = v.length;
218
- sendJSON(req, res, 200, {
219
- status: 'ok',
220
- version: PKG_VERSION,
221
- uptime: process.uptime(),
222
- agents: discoveredAgents.length,
223
- activeExecutions: activeExecutions.size,
224
- wsClients: wss.clients.size,
225
- memory: process.memoryUsage(),
226
- acp: getACPStatus(),
227
- db: dbStatus,
228
- queueSizes
229
- });
230
- return;
231
- }
232
-
233
- const convHandler = _convRoutes._match(req.method, pathOnly);
234
- if (convHandler) { await convHandler(req, res); return; }
235
-
236
- const messagesHandler = _messagesRoutes._match(req.method, pathOnly);
237
- if (messagesHandler) { await messagesHandler(req, res); return; }
238
-
239
- const sessionsHandler = _sessionsRoutes._match(req.method, pathOnly);
240
- if (sessionsHandler) { await sessionsHandler(req, res); return; }
241
-
242
- const scriptsHandler = _scriptsRoutes._match(req.method, pathOnly);
243
- if (scriptsHandler) { await scriptsHandler(req, res); return; }
244
-
245
- const runsHandlerA = _runsRoutes._match(req.method, pathOnly);
246
- if (runsHandlerA) { await runsHandlerA(req, res); return; }
247
-
248
- const agentHandler = _agentRoutes._match(req.method, pathOnly);
249
- if (agentHandler) { await agentHandler(req, res); return; }
250
-
251
- const oauthHandler = _oauthRoutes._match(req.method, pathOnly);
252
- if (oauthHandler) { await oauthHandler(req, res); return; }
253
-
254
- const agentActionsHandler = _agentActionsRoutes._match(req.method, pathOnly);
255
- if (agentActionsHandler) { await agentActionsHandler(req, res); return; }
256
-
257
- const authConfigHandler = _authConfigRoutes._match(req.method, pathOnly);
258
- if (authConfigHandler) { await authConfigHandler(req, res); return; }
259
-
260
- const speechHandler = _speechRoutes._match(req.method, pathOnly);
261
- if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
262
-
263
- const utilHandler = _utilRoutes._match(req.method, pathOnly);
264
- if (utilHandler) { await utilHandler(req, res); return; }
265
-
266
- const threadHandler = _threadRoutes._match(req.method, pathOnly);
267
- if (threadHandler) { await threadHandler(req, res); return; }
268
-
269
- if (routePath.startsWith('/api/image/')) {
270
- const imagePath = routePath.slice('/api/image/'.length);
271
- const decodedPath = decodeURIComponent(imagePath);
272
- const expandedPath = decodedPath.startsWith('~') ?
273
- decodedPath.replace('~', os.homedir()) : decodedPath;
274
- const normalizedPath = path.normalize(expandedPath);
275
- const isWindows = os.platform() === 'win32';
276
- const isAbsolute = isWindows ? /^[A-Za-z]:[\\\/]/.test(normalizedPath) : normalizedPath.startsWith('/');
277
- if (!isAbsolute || normalizedPath.includes('..')) {
278
- res.writeHead(403); res.end('Forbidden'); return;
279
- }
280
- try {
281
- if (!fs.existsSync(normalizedPath)) { res.writeHead(404); res.end('Not found'); return; }
282
- const ext = path.extname(normalizedPath).toLowerCase();
283
- const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
284
- const contentType = mimeTypes[ext] || 'application/octet-stream';
285
- const fileContent = fs.readFileSync(normalizedPath);
286
- res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
287
- res.end(fileContent);
288
- } catch (err) {
289
- sendJSON(req, res, 400, { error: err.message });
290
- }
291
- return;
292
- }
293
-
294
- // Handle conversation detail routes - serve index.html for client-side routing
295
- if (pathOnly.match(/^\/conversations\/[^\/]+$/)) {
296
- const indexPath = path.join(staticDir, 'index.html');
297
- serveFile(indexPath, res, req);
298
- return;
299
- }
300
-
301
- let filePath = routePath === '/' ? '/index.html' : routePath;
302
- filePath = path.join(staticDir, filePath);
303
- const normalizedPath = path.normalize(filePath);
304
- if (!normalizedPath.startsWith(staticDir)) { res.writeHead(403); res.end('Forbidden'); return; }
305
-
306
- fs.stat(filePath, (err, stats) => {
307
- if (err) { res.writeHead(404); res.end('Not found'); return; }
308
- if (stats.isDirectory()) {
309
- filePath = path.join(filePath, 'index.html');
310
- fs.stat(filePath, (err2) => {
311
- if (err2) { res.writeHead(404); res.end('Not found'); return; }
312
- serveFile(filePath, res, req);
313
- });
314
- } else {
315
- serveFile(filePath, res, req);
316
- }
317
- });
318
- } catch (e) {
319
- console.error('Server error:', e.message);
320
- sendJSON(req, res, 500, { error: e.message });
321
- }
322
- });
323
-
324
126
  const _assetDeps = { compressAndSend, acceptsEncoding, watch, BASE_URL, PKG_VERSION };
325
127
  function serveFile(filePath, res, req) { return _serveFile(filePath, res, req, _assetDeps); }
326
128
 
327
- let broadcastSeq = 0;
328
-
129
+ const _routes = {};
130
+ const server = http.createServer(createHttpHandler({
131
+ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues,
132
+ get wss() { return wss; }, activeExecutions, getACPStatus, discoveredAgents,
133
+ PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap: _rateLimitMap,
134
+ get convRoutes() { return _routes.conv; },
135
+ get messagesRoutes() { return _routes.messages; },
136
+ get sessionsRoutes() { return _routes.sessions; },
137
+ get scriptsRoutes() { return _routes.scripts; },
138
+ get runsRoutes() { return _routes.runs; },
139
+ get agentRoutes() { return _routes.agents; },
140
+ get oauthRoutes() { return _routes.oauth; },
141
+ get agentActionsRoutes() { return _routes.agentActions; },
142
+ get authConfigRoutes() { return _routes.authConfig; },
143
+ get speechRoutes() { return _routes.speech; },
144
+ get utilRoutes() { return _routes.util; },
145
+ get threadRoutes() { return _routes.threads; },
146
+ handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT
147
+ }));
329
148
 
330
-
331
- const wss = new WebSocketServer({
332
- server,
333
- perMessageDeflate: false // Disabled: msgpack binary doesn't compress well, and
334
- // synchronous zlib on every frame blocks the event loop.
335
- // HTTP-layer gzip already handles static assets; WS
336
- // streaming events are small and latency-sensitive.
337
- });
338
- wss.on('error', (err) => {
339
- console.error('[WSS] WebSocket server error (contained):', err.message);
340
- });
341
- const hotReloadClients = [];
149
+ let broadcastSeq = 0;
342
150
  const syncClients = new Set();
343
151
  const subscriptionIndex = new Map();
344
152
  const pm2Subscribers = new Set();
345
153
 
346
- wss.on('connection', (ws, req) => {
347
- const _pwd = process.env.PASSWORD;
348
- if (_pwd) {
349
- const url = new URL(req.url, 'http://localhost');
350
- const token = url.searchParams.get('token');
351
- if (token !== _pwd) { ws.close(4001, 'Unauthorized'); return; }
352
- }
353
- const wsPath = req.url.split('?')[0];
354
- const wsRoute = wsPath.startsWith(BASE_URL) ? wsPath.slice(BASE_URL.length) : wsPath;
355
- if (wsRoute === '/hot-reload') {
356
- hotReloadClients.push(ws);
357
- ws.on('close', () => { const i = hotReloadClients.indexOf(ws); if (i > -1) hotReloadClients.splice(i, 1); });
358
- } else if (wsRoute === '/sync') {
359
- syncClients.add(ws);
360
- ws.isAlive = true;
361
- ws.subscriptions = new Set();
362
- ws.clientId = `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
363
-
364
- sendWs(ws, ({
365
- type: 'sync_connected',
366
- clientId: ws.clientId,
367
- timestamp: Date.now()
368
- }));
369
-
370
- ws.on('error', (err) => {
371
- console.error('[WS] Client error (contained):', ws.clientId, err.message);
372
- });
373
- ws.on('message', (msg) => {
374
- try { wsRouter.onMessage(ws, msg); } catch (e) { console.error('[WS] Message handler error (contained):', e.message); }
375
- });
376
-
377
- ws.on('pong', () => { ws.isAlive = true; });
378
- ws.on('close', () => {
379
- if (ws.terminalProc) { try { ws.terminalProc.kill(); } catch(e) {} ws.terminalProc = null; }
380
- syncClients.delete(ws);
381
- wsOptimizer.removeClient(ws);
382
- for (const sub of ws.subscriptions) {
383
- const idx = subscriptionIndex.get(sub);
384
- if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(sub); }
385
- }
386
- if (ws.pm2Subscribed) {
387
- pm2Subscribers.delete(ws);
388
- }
389
- debugLog(`[WebSocket] Client ${ws.clientId} disconnected`);
390
- });
391
- }
392
- });
393
-
394
154
  const BROADCAST_TYPES = new Set([
395
155
  'message_created', 'conversation_created', 'conversation_updated',
396
156
  'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated', 'queue_item_dequeued',
@@ -420,7 +180,6 @@ const broadcastSync = createBroadcast({
420
180
 
421
181
  const cleanupExecution = makeCleanupExecution({ execMachine, activeExecutions, queries, broadcastSync, debugLog });
422
182
 
423
- // Wire up process-message factories now that broadcastSync and all deps are available
424
183
  const _mqDeps = {
425
184
  queries, messageQueues, activeExecutions, rateLimitState, execMachine,
426
185
  broadcastSync, cleanupExecution, debugLog,
@@ -437,34 +196,33 @@ const { processMessageWithStreaming } = createProcessMessage({
437
196
  scheduleRetry, drainMessageQueue, createEventHandler
438
197
  });
439
198
 
440
- // WebSocket protocol router
441
199
  const wsRouter = new WsRouter();
442
200
 
443
201
  initSpeechManager({ broadcastSync, syncClients, queries });
444
- const _speechRoutes = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync, debugLog });
445
- const _oauthRoutes = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
446
- const _utilRoutes = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
202
+ _routes.speech = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync, debugLog });
203
+ _routes.oauth = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
204
+ _routes.util = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
447
205
  const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
448
- const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
206
+ _routes.threads = registerThreadRoutes({ sendJSON, parseBody, queries });
449
207
  const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath: errLogPath });
450
- const _convRoutes = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
451
- const _agentRoutes = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
452
- const _messagesRoutes = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });
453
- const _sessionsRoutes = registerSessionsRoutes({ queries, sendJSON, activeExecutions, rateLimitState, debugLog });
454
- const _runsRoutes = registerRunsRoutes({ sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD });
455
- const _scriptsRoutes = registerScriptsRoutes({ sendJSON, parseBody, queries, broadcastSync, activeScripts, activeExecutions, processMessageWithStreaming, STARTUP_CWD });
456
- const _agentActionsRoutes = registerAgentActionsRoutes({ sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir });
457
- const _authConfigRoutes = registerAuthConfigRoutes({ sendJSON, parseBody, getProviderConfigs, saveProviderConfig });
208
+ _routes.conv = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
209
+ _routes.agents = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
210
+ _routes.messages = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });
211
+ _routes.sessions = registerSessionsRoutes({ queries, sendJSON, activeExecutions, rateLimitState, debugLog });
212
+ _routes.runs = registerRunsRoutes({ sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD });
213
+ _routes.scripts = registerScriptsRoutes({ sendJSON, parseBody, queries, broadcastSync, activeScripts, activeExecutions, processMessageWithStreaming, STARTUP_CWD });
214
+ _routes.agentActions = registerAgentActionsRoutes({ sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir });
215
+ _routes.authConfig = registerAuthConfigRoutes({ sendJSON, parseBody, getProviderConfigs, saveProviderConfig });
458
216
 
459
217
  registerConvHandlers(wsRouter, {
460
218
  queries, activeExecutions, rateLimitState,
461
219
  broadcastSync, processMessageWithStreaming, cleanupExecution,
462
- getJsonlWatcher: () => jsonlWatcher
220
+ getJsonlWatcher
463
221
  });
464
222
  registerConvHandlers2(wsRouter, {
465
223
  queries, activeExecutions, rateLimitState,
466
224
  broadcastSync, processMessageWithStreaming, cleanupExecution,
467
- getJsonlWatcher: () => jsonlWatcher
225
+ getJsonlWatcher
468
226
  });
469
227
 
470
228
  registerMsgHandlers(wsRouter, {
@@ -510,281 +268,16 @@ registerOAuthHandlers(wsRouter, {
510
268
  codexOAuthState: getCodexOAuthState,
511
269
  });
512
270
 
513
- wsRouter.onLegacy((data, ws) => {
514
- try {
515
- if (data.type === 'subscribe') {
516
- if (data.sessionId) {
517
- ws.subscriptions.add(data.sessionId);
518
- if (!subscriptionIndex.has(data.sessionId)) subscriptionIndex.set(data.sessionId, new Set());
519
- subscriptionIndex.get(data.sessionId).add(ws);
520
- }
521
- if (data.conversationId) {
522
- const key = `conv-${data.conversationId}`;
523
- ws.subscriptions.add(key);
524
- if (!subscriptionIndex.has(key)) subscriptionIndex.set(key, new Set());
525
- subscriptionIndex.get(key).add(ws);
526
- }
527
- const subTarget = data.sessionId || data.conversationId;
528
- debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
529
- sendWs(ws, ({
530
- type: 'subscription_confirmed',
531
- sessionId: data.sessionId,
532
- conversationId: data.conversationId,
533
- timestamp: Date.now()
534
- }));
535
-
536
- // Notify client if this conversation has an active streaming execution
537
- // Machine is authoritative for streaming state check on subscribe
538
- if (data.conversationId && execMachine.isActive(data.conversationId)) {
539
- const ctx = execMachine.getContext(data.conversationId);
540
- const execution = activeExecutions.get(data.conversationId);
541
- const sessionId = ctx?.sessionId || execution?.sessionId;
542
- const conv = queries.getConversation(data.conversationId);
543
- const queueLength = execMachine.getQueue(data.conversationId).length || messageQueues.get(data.conversationId)?.length || 0;
544
- sendWs(ws, ({
545
- type: 'streaming_start',
546
- sessionId,
547
- conversationId: data.conversationId,
548
- agentId: conv?.agentType || conv?.agentId || 'claude-code',
549
- queueLength,
550
- resumed: true,
551
- seq: ++broadcastSeq,
552
- timestamp: Date.now()
553
- }));
554
- }
555
-
556
- // Inject pending checkpoint events if this is a conversation subscription
557
- if (data.conversationId && checkpointManager.hasPendingCheckpoint(data.conversationId)) {
558
- const checkpoint = checkpointManager.getPendingCheckpoint(data.conversationId);
559
- if (checkpoint) {
560
- debugLog(`[checkpoint] Injecting ${checkpoint.events.length} events to client for ${data.conversationId}`);
561
-
562
- const latestSession = queries.getLatestSession(data.conversationId);
563
- if (latestSession) {
564
- sendWs(ws, ({
565
- type: 'streaming_resumed',
566
- sessionId: latestSession.id,
567
- conversationId: data.conversationId,
568
- resumeFrom: checkpoint.sessionId,
569
- eventCount: checkpoint.events.length,
570
- chunkCount: checkpoint.chunks.length,
571
- timestamp: Date.now()
572
- }));
573
-
574
- checkpointManager.injectCheckpointEvents(latestSession.id, checkpoint, (evt) => {
575
- sendWs(ws, ({
576
- ...evt,
577
- sessionId: latestSession.id,
578
- conversationId: data.conversationId
579
- }));
580
- });
581
- }
582
- }
583
- }
584
- } else if (data.type === 'unsubscribe') {
585
- if (data.sessionId) {
586
- ws.subscriptions.delete(data.sessionId);
587
- const idx = subscriptionIndex.get(data.sessionId);
588
- if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(data.sessionId); }
589
- }
590
- if (data.conversationId) {
591
- const key = `conv-${data.conversationId}`;
592
- ws.subscriptions.delete(key);
593
- const idx = subscriptionIndex.get(key);
594
- if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(key); }
595
- }
596
- debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId || data.conversationId}`);
597
- } else if (data.type === 'get_subscriptions') {
598
- sendWs(ws, ({
599
- type: 'subscriptions',
600
- subscriptions: Array.from(ws.subscriptions),
601
- timestamp: Date.now()
602
- }));
603
- } else if (data.type === 'set_voice') {
604
- ws.ttsVoiceId = data.voiceId || 'default';
605
- } else if (data.type === 'latency_report') {
606
- ws.latencyTier = data.quality || 'good';
607
- ws.latencyAvg = data.avg || 0;
608
- ws.latencyTrend = data.trend || 'stable';
609
- } else if (data.type === 'ping') {
610
- sendWs(ws, ({
611
- type: 'pong',
612
- requestId: data.requestId,
613
- timestamp: Date.now()
614
- }));
615
- } else if (data.type === 'terminal_start') {
616
- if (ws.terminalProc) {
617
- try { ws.terminalProc.kill(); } catch(e) {}
618
- }
619
- try {
620
- const _req = createRequire(import.meta.url);
621
- const pty = _req('node-pty');
622
- const shell = process.env.SHELL || '/bin/bash';
623
- const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
624
- const proc = pty.spawn(shell, [], {
625
- name: 'xterm-256color',
626
- cols: data.cols || 80,
627
- rows: data.rows || 24,
628
- cwd: cwd,
629
- env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }
630
- });
631
- ws.terminalProc = proc;
632
- ws.terminalPty = true;
633
- proc.on('data', (chunk) => {
634
- if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: Buffer.from(chunk).toString('base64'), encoding: 'base64' }));
635
- });
636
- proc.on('exit', (code) => {
637
- if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
638
- ws.terminalProc = null;
639
- });
640
- proc.on('error', (err) => {
641
- console.error('[TERMINAL] PTY error (contained):', err.message);
642
- if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
643
- ws.terminalProc = null;
644
- });
645
- sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
646
- } catch (e) {
647
- console.error('[TERMINAL] Failed to spawn PTY, falling back to pipes:', e.message);
648
- const shell = process.env.SHELL || '/bin/bash';
649
- const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
650
- const proc = spawn(shell, ['-i'], { cwd, env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }, stdio: ['pipe', 'pipe', 'pipe'] });
651
- ws.terminalProc = proc;
652
- ws.terminalPty = false;
653
- proc.stdout.on('data', (chunk) => {
654
- if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
655
- });
656
- proc.stderr.on('data', (chunk) => {
657
- if (ws.readyState === 1) sendWs(ws, ({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
658
- });
659
- proc.on('exit', (code) => {
660
- if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code });
661
- ws.terminalProc = null;
662
- });
663
- proc.on('error', (err) => {
664
- console.error('[TERMINAL] Spawn error (contained):', err.message);
665
- if (ws.readyState === 1) sendWs(ws, { type: 'terminal_exit', code: 1, error: err.message });
666
- ws.terminalProc = null;
667
- });
668
- proc.stdin.on('error', () => {});
669
- proc.stdout.on('error', () => {});
670
- proc.stderr.on('error', () => {});
671
- sendWs(ws, ({ type: 'terminal_started', timestamp: Date.now() }));
672
- }
673
- } else if (data.type === 'terminal_input') {
674
- if (ws.terminalProc) {
675
- try {
676
- const input = Buffer.from(data.data, 'base64');
677
- if (ws.terminalPty) {
678
- ws.terminalProc.write(input);
679
- } else if (ws.terminalProc.stdin && ws.terminalProc.stdin.writable) {
680
- ws.terminalProc.stdin.write(input);
681
- }
682
- } catch (e) {}
683
- }
684
- } else if (data.type === 'terminal_resize') {
685
- if (ws.terminalProc && ws.terminalPty) {
686
- try {
687
- const { cols, rows } = data;
688
- if (cols && rows && typeof ws.terminalProc.resize === 'function') {
689
- ws.terminalProc.resize(cols, rows);
690
- }
691
- } catch (e) {}
692
- }
693
- } else if (data.type === 'terminal_stop') {
694
- if (ws.terminalProc) {
695
- try { ws.terminalProc.kill(); } catch(e) {}
696
- ws.terminalProc = null;
697
- }
698
- } else if (data.type === 'pm2_list') {
699
- if (!pm2Manager.connected) {
700
- if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
701
- } else {
702
- pm2Manager.listProcesses().then(processes => {
703
- if (ws.readyState === 1) {
704
- const hasActive = processes.some(p => ['online','launching','stopping','waiting restart'].includes(p.status));
705
- sendWs(ws, { type: 'pm2_list_response', processes, hasActive });
706
- }
707
- }).catch(() => {
708
- if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'list failed', timestamp: Date.now() }));
709
- });
710
- }
711
- } else if (data.type === 'pm2_start_monitoring') {
712
- pm2Subscribers.add(ws);
713
- ws.pm2Subscribed = true;
714
- if (!pm2Manager.connected) {
715
- if (ws.readyState === 1) sendWs(ws, ({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
716
- } else {
717
- sendWs(ws, { type: 'pm2_monitoring_started' });
718
- }
719
- } else if (data.type === 'pm2_stop_monitoring') {
720
- pm2Subscribers.delete(ws);
721
- ws.pm2Subscribed = false;
722
- sendWs(ws, { type: 'pm2_monitoring_stopped' });
723
- } else if (data.type === 'pm2_start') {
724
- pm2Manager.startProcess(data.name).then(result => {
725
- sendWs(ws, { type: 'pm2_start_response', name: data.name, ...result });
726
- });
727
- } else if (data.type === 'pm2_stop') {
728
- pm2Manager.stopProcess(data.name).then(result => {
729
- sendWs(ws, { type: 'pm2_stop_response', name: data.name, ...result });
730
- });
731
- } else if (data.type === 'pm2_restart') {
732
- pm2Manager.restartProcess(data.name).then(result => {
733
- sendWs(ws, { type: 'pm2_restart_response', name: data.name, ...result });
734
- });
735
- } else if (data.type === 'pm2_delete') {
736
- pm2Manager.deleteProcess(data.name).then(result => {
737
- sendWs(ws, { type: 'pm2_delete_response', name: data.name, ...result });
738
- });
739
- } else if (data.type === 'pm2_logs') {
740
- pm2Manager.getLogs(data.name, { lines: data.lines || 100 }).then(result => {
741
- sendWs(ws, { type: 'pm2_logs_response', name: data.name, ...result });
742
- });
743
- } else if (data.type === 'pm2_flush_logs') {
744
- pm2Manager.flushLogs(data.name).then(result => {
745
- sendWs(ws, { type: 'pm2_flush_logs_response', name: data.name, ...result });
746
- });
747
- } else if (data.type === 'pm2_ping') {
748
- pm2Manager.ping().then(result => {
749
- sendWs(ws, { type: 'pm2_ping_response', ...result });
750
- });
751
- }
752
-
753
- } catch (err) { console.error('[WS-LEGACY] Handler error (contained):', err.message); }
271
+ const { wss, hotReloadClients } = createWsSetup(server, {
272
+ BASE_URL, watch, staticDir, _assetCache, htmlState, sendWs, wsRouter, debugLog,
273
+ subscriptionIndex, syncClients, pm2Subscribers, wsOptimizer,
274
+ legacyDeps: {
275
+ subscriptionIndex, execMachine, activeExecutions, messageQueues,
276
+ checkpointManager, queries, pm2Manager, pm2Subscribers,
277
+ getSeq: () => ++broadcastSeq, sendWs, debugLog
278
+ }
754
279
  });
755
280
 
756
- // Heartbeat interval to detect stale connections
757
- const heartbeatInterval = setInterval(() => {
758
- syncClients.forEach(ws => {
759
- if (!ws.isAlive) {
760
- syncClients.delete(ws);
761
- wsOptimizer.removeClient(ws);
762
- return ws.terminate();
763
- }
764
- ws.isAlive = false;
765
- ws.ping();
766
- });
767
- }, 30000);
768
-
769
- if (watch) {
770
- const watchedFiles = new Map();
771
- try {
772
- fs.readdirSync(staticDir).forEach(file => {
773
- const fp = path.join(staticDir, file);
774
- if (watchedFiles.has(fp)) return;
775
- fs.watchFile(fp, { interval: 100 }, (curr, prev) => {
776
- if (curr.mtime > prev.mtime) {
777
- _assetCache.clear();
778
- htmlState.cache = null;
779
- htmlState.etag = null;
780
- hotReloadClients.forEach(c => { if (c.readyState === 1) c.send(JSON.stringify({ type: 'reload' })); });
781
- }
782
- });
783
- watchedFiles.set(fp, true);
784
- });
785
- } catch (e) { console.error('Watch error:', e.message); }
786
- }
787
-
788
281
  const { killActiveExecutions, recoverStaleSessions, resumeInterruptedStreams, isProcessAlive, markAgentDead, resumeConversation, performAgentHealthCheck } = createRecovery({
789
282
  activeExecutions,
790
283
  processMessageWithStreaming,
@@ -799,7 +292,7 @@ const { killActiveExecutions, recoverStaleSessions, resumeInterruptedStreams, is
799
292
  process.on('SIGTERM', () => {
800
293
  console.log('[SIGNAL] SIGTERM received - graceful shutdown');
801
294
  killActiveExecutions();
802
- if (jsonlWatcher) try { jsonlWatcher.stop(); } catch (_) {}
295
+ const _jw = getJsonlWatcher(); if (_jw) try { _jw.stop(); } catch (_) {}
803
296
  try { pm2Manager.disconnect(); } catch (_) {}
804
297
  stopACPTools().catch(() => {}).finally(() => {
805
298
  try { wss.close(() => server.close(() => process.exit(0))); } catch (_) { process.exit(0); }
@@ -823,253 +316,19 @@ server.on('error', (err) => {
823
316
  }
824
317
  });
825
318
 
826
- let jsonlWatcher = null;
827
-
828
- function onServerReady() {
829
- // Clear tool status cache on startup to ensure fresh detection
830
- toolManager.clearStatusCache();
831
-
832
- console.log(`GMGUI running on http://localhost:${PORT}${BASE_URL}/`);
833
- console.log(`Agents: ${discoveredAgents.map(a => a.name).join(', ') || 'none'}`);
834
- console.log(`Hot reload: ${watch ? 'on' : 'off'}`);
835
-
836
- const deletedCount = queries.cleanupEmptyConversations();
837
- if (deletedCount > 0) {
838
- console.log(`Cleaned up ${deletedCount} empty conversation(s) on startup`);
839
- }
840
-
841
- recoverStaleSessions();
842
- warmAssetCache(staticDir);
843
-
844
- // Run DB cleanup on startup and every 6 hours
845
- try { queries.cleanup(); console.log('[cleanup] Initial DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
846
- setInterval(() => {
847
- try { queries.cleanup(); console.log('[cleanup] Scheduled DB cleanup complete'); } catch (e) { console.error('[cleanup] Error:', e.message); }
848
- }, 6 * 60 * 60 * 1000);
849
-
850
- try {
851
- jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
852
- jsonlWatcher.start();
853
- console.log('[JSONL] Watcher started');
854
- } catch (err) {
855
- console.error('[JSONL] Watcher failed to start:', err.message);
856
- }
857
-
858
- resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
859
-
860
- setInterval(() => {
861
- try {
862
- const streaming = queries.getStreamingConversations();
863
- let cleared = 0;
864
- for (const c of streaming) {
865
- if (!activeExecutions.has(c.id)) {
866
- queries.setIsStreaming(c.id, false);
867
- cleared++;
868
- }
869
- }
870
- if (cleared > 0) debugLog(`[HEALTH] Cleared ${cleared} stale streaming flag(s)`);
871
- } catch (e) { debugLog(`[HEALTH] Error: ${e.message}`); }
872
- }, 5 * 60 * 1000);
873
-
874
- installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
875
-
876
- startACPTools().then(() => {
877
- console.log('[ACP] On-demand startup enabled (ACP tools start when first used)');
878
- setTimeout(() => {
879
- const acpStatus = getACPStatus();
880
- for (const s of acpStatus) {
881
- if (s.healthy) {
882
- const agent = discoveredAgents.find(a => a.id === s.id);
883
- if (agent) { agent.acpPort = s.port; }
884
- }
885
- }
886
- if (acpStatus.length > 0) {
887
- console.log(`[ACP] Tools ready: ${acpStatus.filter(s => s.healthy).map(s => s.id + ':' + s.port).join(', ') || 'none healthy yet'}`);
888
- }
889
- }, 6000);
890
- }).catch(err => console.error('[ACP] Startup error:', err.message));
319
+ const { performAutoImport } = createAutoImport({ queries, broadcastSync });
320
+ const { performDbRecovery } = createDbRecovery({ queries, debugLog });
321
+ const { loadPluginExtensions } = createPluginLoader({ pluginsDir: path.join(__dirname, 'lib', 'plugins'), expressApp, BASE_URL });
891
322
 
892
- const toolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
893
- queries.initializeToolInstallations(toolIds.map(id => ({ id })));
894
- console.log('[TOOLS] Starting background provisioning...');
323
+ setInterval(performDbRecovery, 300000);
895
324
 
896
- // Create broadcast handler for tool events
897
- const toolBroadcaster = (evt) => {
898
- broadcastSync(evt);
899
- if (evt.type === 'tool_install_complete' || evt.type === 'tool_update_complete') {
900
- const d = evt.data || {};
901
- queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.version || null, installed_at: Date.now() });
902
- queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'success', null);
903
- } else if (evt.type === 'tool_install_failed' || evt.type === 'tool_update_failed') {
904
- queries.updateToolStatus(evt.toolId, { status: 'failed', error_message: evt.data?.error });
905
- queries.addToolInstallHistory(evt.toolId, evt.type.includes('update') ? 'update' : 'install', 'failed', evt.data?.error);
906
- } else if (evt.type === 'tool_status_update') {
907
- const d = evt.data || {};
908
- if (d.installed) {
909
- queries.updateToolStatus(evt.toolId, { status: 'installed', version: d.installedVersion || null, installed_at: Date.now() });
910
- }
911
- }
912
- };
913
-
914
- // Initial provisioning (blocks until complete)
915
- toolManager.autoProvision(toolBroadcaster)
916
- .catch(err => console.error('[TOOLS] Auto-provision error:', err.message))
917
- .then(() => {
918
- const acpActors = ['opencode', 'kilo', 'codex'];
919
- const execActorCount = execMachine.stopAll ? 0 : 0;
920
- console.log(`[MACHINES] tool-install: ${toolInstallMachine.getMachineActors().size} actors, acp-server: ${acpActors.length} configured`);
921
- console.log('[TOOLS] Starting periodic update checker...');
922
- toolManager.startPeriodicUpdateCheck(toolBroadcaster);
923
- });
924
-
925
- ensureModelsDownloaded().then(async ok => {
926
- if (ok) console.log('[MODELS] Speech models ready');
927
- else console.log('[MODELS] Speech model download failed');
928
- try {
929
- const { getVoices } = await getSpeech();
930
- const voices = getVoices();
931
- broadcastSync({ type: 'voice_list', voices });
932
- } catch (err) {
933
- debugLog('[VOICE] Failed to broadcast voices: ' + err.message);
934
- broadcastSync({ type: 'voice_list', voices: [] });
935
- }
936
- }).catch(async err => {
937
- console.error('[MODELS] Download error:', err.message);
938
- try {
939
- const { getVoices } = await getSpeech();
940
- const voices = getVoices();
941
- broadcastSync({ type: 'voice_list', voices });
942
- } catch (err2) {
943
- debugLog('[VOICE] Failed to broadcast voices: ' + err2.message);
944
- broadcastSync({ type: 'voice_list', voices: [] });
945
- }
946
- });
947
-
948
- getSpeech().then(s => s.preloadTTS()).catch(e => debugLog('[TTS] Preload failed: ' + e.message));
949
-
950
- performAutoImport();
951
-
952
- setInterval(performAutoImport, 30000);
953
-
954
- setInterval(performAgentHealthCheck, 30000);
955
-
956
- // Initialize PM2 monitoring - only when PM2 daemon is available
957
- const broadcastPM2 = (update) => {
958
- const msg = JSON.stringify(update);
959
- for (const client of pm2Subscribers) {
960
- if (client.readyState === 1) { try { client.send(msg); } catch (_) {} }
961
- }
962
- };
963
-
964
- const startPM2Monitoring = async () => {
965
- try {
966
- await pm2Manager.connect();
967
- await pm2Manager.startMonitoring(broadcastPM2);
968
- console.log('[PM2] Monitoring started');
969
- } catch (err) {
970
- console.log('[PM2] Not available:', err.message);
971
- broadcastPM2({ type: 'pm2_unavailable', reason: err.message, timestamp: Date.now() });
972
- }
973
- };
974
-
975
- setTimeout(startPM2Monitoring, 2000);
976
-
977
- setInterval(async () => {
978
- if (!pm2Manager.connected && !pm2Manager.monitoring) {
979
- try {
980
- const healed = await pm2Manager.heal();
981
- if (healed.success) await pm2Manager.startMonitoring(broadcastPM2);
982
- } catch (_) {}
983
- }
984
- }, 30000);
985
- }
986
-
987
- const importMtimeCache = new Map();
988
-
989
- function hasIndexFilesChanged() {
990
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
991
- if (!fs.existsSync(projectsDir)) return false;
992
- let changed = false;
993
- try {
994
- const dirs = fs.readdirSync(projectsDir);
995
- for (const d of dirs) {
996
- const indexPath = path.join(projectsDir, d, 'sessions-index.json');
997
- try {
998
- const stat = fs.statSync(indexPath);
999
- const cached = importMtimeCache.get(indexPath);
1000
- if (!cached || cached < stat.mtimeMs) {
1001
- importMtimeCache.set(indexPath, stat.mtimeMs);
1002
- changed = true;
1003
- }
1004
- } catch (_) {}
1005
- }
1006
- } catch (_) {}
1007
- return changed;
1008
- }
1009
-
1010
- function performAutoImport() {
1011
- try {
1012
- if (!hasIndexFilesChanged()) return;
1013
- const imported = queries.importClaudeCodeConversations();
1014
- if (imported.length > 0) {
1015
- const importedCount = imported.filter(i => i.status === 'imported').length;
1016
- if (importedCount > 0) {
1017
- console.log(`[AUTO-IMPORT] Imported ${importedCount} new Claude Code conversations`);
1018
- broadcastSync({ type: 'conversations_updated', count: importedCount });
1019
- }
1020
- }
1021
- } catch (err) {
1022
- console.error('[AUTO-IMPORT] Error:', err.message);
1023
- }
1024
- }
1025
-
1026
- function performRecovery() {
1027
- try {
1028
- // Cleanup orphaned sessions (older than 7 days)
1029
- const cleanedUp = queries.cleanupOrphanedSessions(7);
1030
- if (cleanedUp > 0) {
1031
- debugLog(`[RECOVERY] Cleaned up ${cleanedUp} orphaned sessions`);
1032
- }
1033
-
1034
- // Mark sessions incomplete if they've been processing too long (>2 hours)
1035
- const longRunning = queries.getSessionsProcessingLongerThan(120);
1036
- if (longRunning.length > 0) {
1037
- for (const session of longRunning) {
1038
- queries.markSessionIncomplete(session.id, 'Timeout: processing exceeded 2 hours');
1039
- }
1040
- debugLog(`[RECOVERY] Marked ${longRunning.length} long-running sessions as incomplete`);
1041
- }
1042
- } catch (err) {
1043
- console.error('[RECOVERY] Error:', err.message);
1044
- }
1045
- }
1046
-
1047
- // Run recovery every 5 minutes
1048
- setInterval(performRecovery, 300000);
1049
-
1050
- // Load plugins as extensions (additive, not replacing core routes)
1051
- import PluginLoader from './lib/plugin-loader.js';
1052
- const pluginLoader = new PluginLoader(path.join(__dirname, 'lib', 'plugins'));
1053
- async function loadPluginExtensions() {
1054
- try {
1055
- await pluginLoader.loadAllPlugins({ router: expressApp, baseUrl: BASE_URL, logger: console, env: process.env });
1056
- const names = Array.from(pluginLoader.registry.keys());
1057
- if (names.length > 0) {
1058
- for (const name of names) {
1059
- const state = pluginLoader.get(name);
1060
- if (!state || !state.routes) continue;
1061
- for (const route of state.routes) {
1062
- const fullPath = BASE_URL + route.path;
1063
- const method = (route.method || 'GET').toLowerCase();
1064
- if (expressApp[method]) expressApp[method](fullPath, route.handler);
1065
- }
1066
- }
1067
- console.log(`[PLUGINS] Loaded extensions: ${names.join(', ')}`);
1068
- }
1069
- } catch (err) {
1070
- console.error('[PLUGINS] Extension loading failed (non-fatal):', err.message);
1071
- }
1072
- }
325
+ const { onServerReady, getJsonlWatcher } = createOnServerReady({
326
+ queries, broadcastSync, warmAssetCache, staticDir, toolManager, discoveredAgents,
327
+ PORT, BASE_URL, watch, ownedSessionIds, resumeInterruptedStreams, activeExecutions,
328
+ debugLog, installGMAgentConfigs, startACPTools, getACPStatus, execMachine,
329
+ toolInstallMachine, getSpeech, ensureModelsDownloaded, performAutoImport,
330
+ performAgentHealthCheck, pm2Manager, pm2Subscribers, recoverStaleSessions
331
+ });
1073
332
 
1074
333
  server.listen(PORT, () => {
1075
334
  onServerReady();