agentgui 1.0.762 → 1.0.764

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -50,6 +50,7 @@ lib/routes-speech.js Speech/TTS HTTP route handlers (stt, tts, voices, speech-
50
50
  lib/routes-oauth.js OAuth HTTP route handlers (gemini-oauth/*, codex-oauth/*)
51
51
  lib/routes-tools.js Tool management HTTP route handlers (list, install, update, history, refresh)
52
52
  lib/routes-util.js Utility HTTP route handlers (clone, folders, git, home, version, import)
53
+ lib/routes-threads.js Thread CRUD HTTP route handlers (ACP v0.2.3 thread API)
53
54
  lib/ws-protocol.js WebSocket RPC router (WsRouter class)
54
55
  lib/ws-optimizer.js Per-client priority queue for WS event batching
55
56
  lib/ws-handlers-conv.js Conversation CRUD, chunks, cancel, steer, inject RPC handlers
@@ -0,0 +1,99 @@
1
+ export function register(deps) {
2
+ const { sendJSON, parseBody, queries } = deps;
3
+ const routes = {};
4
+
5
+ routes['POST /api/threads'] = async (req, res) => {
6
+ try {
7
+ const body = await parseBody(req);
8
+ sendJSON(req, res, 201, queries.createThread(body.metadata || {}));
9
+ } catch (err) {
10
+ sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
11
+ }
12
+ };
13
+
14
+ routes['POST /api/threads/search'] = async (req, res) => {
15
+ try {
16
+ sendJSON(req, res, 200, queries.searchThreads(await parseBody(req)));
17
+ } catch (err) {
18
+ sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
19
+ }
20
+ };
21
+
22
+ routes['_match'] = (method, pathOnly) => {
23
+ const key = `${method} ${pathOnly}`;
24
+ if (routes[key]) return routes[key];
25
+ let m;
26
+ if ((m = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})$/))) {
27
+ if (method === 'GET') return (req, res) => handleGetThread(req, res, m[1]);
28
+ if (method === 'PATCH') return (req, res) => handlePatchThread(req, res, m[1]);
29
+ if (method === 'DELETE') return (req, res) => handleDeleteThread(req, res, m[1]);
30
+ }
31
+ if (method === 'GET' && (m = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/history$/)))
32
+ return (req, res) => handleThreadHistory(req, res, m[1]);
33
+ if (method === 'POST' && (m = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/copy$/)))
34
+ return (req, res) => handleThreadCopy(req, res, m[1]);
35
+ if (method === 'POST' && pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/stream$/))
36
+ return (req, res) => { res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' })); };
37
+ if (method === 'GET' && pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/([a-f0-9-]{36})\/stream$/))
38
+ return (req, res) => { res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' })); };
39
+ return null;
40
+ };
41
+
42
+ async function handleGetThread(req, res, threadId) {
43
+ try {
44
+ const thread = queries.getThread(threadId);
45
+ if (!thread) sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
46
+ else sendJSON(req, res, 200, thread);
47
+ } catch (err) {
48
+ sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
49
+ }
50
+ }
51
+
52
+ async function handlePatchThread(req, res, threadId) {
53
+ try {
54
+ sendJSON(req, res, 200, queries.patchThread(threadId, await parseBody(req)));
55
+ } catch (err) {
56
+ const code = err.message.includes('not found') ? 404 : 422;
57
+ sendJSON(req, res, code, { error: err.message, type: code === 404 ? 'not_found' : 'validation_error' });
58
+ }
59
+ }
60
+
61
+ async function handleDeleteThread(req, res, threadId) {
62
+ try {
63
+ queries.deleteThread(threadId);
64
+ res.writeHead(204); res.end();
65
+ } catch (err) {
66
+ if (err.message.includes('not found')) sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
67
+ else if (err.message.includes('pending runs')) sendJSON(req, res, 409, { error: err.message, type: 'conflict' });
68
+ else sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
69
+ }
70
+ }
71
+
72
+ async function handleThreadHistory(req, res, threadId) {
73
+ try {
74
+ const url = new URL(req.url, `http://${req.headers.host}`);
75
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
76
+ const before = url.searchParams.get('before') || null;
77
+ let offset = before ? parseInt(before, 10) : 0;
78
+ const result = queries.getThreadHistory(threadId, limit, offset);
79
+ sendJSON(req, res, 200, { states: result.states, next_cursor: result.hasMore ? String(offset + limit) : null });
80
+ } catch (err) {
81
+ const code = err.message.includes('not found') ? 404 : 422;
82
+ sendJSON(req, res, code, { error: err.message, type: code === 404 ? 'not_found' : 'validation_error' });
83
+ }
84
+ }
85
+
86
+ async function handleThreadCopy(req, res, sourceThreadId) {
87
+ try {
88
+ const body = await parseBody(req);
89
+ const newThread = queries.copyThread(sourceThreadId);
90
+ if (body.metadata) sendJSON(req, res, 201, queries.patchThread(newThread.thread_id, { metadata: body.metadata }));
91
+ else sendJSON(req, res, 201, newThread);
92
+ } catch (err) {
93
+ const code = err.message.includes('not found') ? 404 : 500;
94
+ sendJSON(req, res, code, { error: err.message, type: code === 404 ? 'not_found' : 'internal_error' });
95
+ }
96
+ }
97
+
98
+ return routes;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.762",
3
+ "version": "1.0.764",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/server.js CHANGED
@@ -23,6 +23,7 @@ import { register as registerSpeechRoutes } from './lib/routes-speech.js';
23
23
  import { register as registerOAuthRoutes } from './lib/routes-oauth.js';
24
24
  import { register as registerUtilRoutes } from './lib/routes-util.js';
25
25
  import { register as registerToolRoutes } from './lib/routes-tools.js';
26
+ import { register as registerThreadRoutes } from './lib/routes-threads.js';
26
27
  import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
27
28
  import { WSOptimizer } from './lib/ws-optimizer.js';
28
29
  import { WsRouter } from './lib/ws-protocol.js';
@@ -526,6 +527,10 @@ const server = http.createServer(async (req, res) => {
526
527
  }
527
528
 
528
529
  if (pathOnly === '/api/health' && req.method === 'GET') {
530
+ let dbStatus = { ok: true };
531
+ try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; }
532
+ const queueSizes = {};
533
+ for (const [k, v] of messageQueues) queueSizes[k] = v.length;
529
534
  sendJSON(req, res, 200, {
530
535
  status: 'ok',
531
536
  version: PKG_VERSION,
@@ -534,7 +539,9 @@ const server = http.createServer(async (req, res) => {
534
539
  activeExecutions: activeExecutions.size,
535
540
  wsClients: wss.clients.size,
536
541
  memory: process.memoryUsage(),
537
- acp: getACPStatus()
542
+ acp: getACPStatus(),
543
+ db: dbStatus,
544
+ queueSizes
538
545
  });
539
546
  return;
540
547
  }
@@ -1188,7 +1195,7 @@ const server = http.createServer(async (req, res) => {
1188
1195
  }
1189
1196
 
1190
1197
  if (pathOnly === '/api/agents' && req.method === 'GET') {
1191
- console.log(`[API /api/agents] Returning ${discoveredAgents.length} agents:`, discoveredAgents.map(a => a.id).join(', '));
1198
+ debugLog(`[API /api/agents] Returning ${discoveredAgents.length} agents`);
1192
1199
  sendJSON(req, res, 200, { agents: discoveredAgents });
1193
1200
  return;
1194
1201
  }
@@ -1198,21 +1205,6 @@ const server = http.createServer(async (req, res) => {
1198
1205
  return;
1199
1206
  }
1200
1207
 
1201
- if (pathOnly === '/api/health' && req.method === 'GET') {
1202
- let dbStatus = { ok: true };
1203
- try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; }
1204
- const queueSizes = {};
1205
- for (const [k, v] of messageQueues) queueSizes[k] = v.length;
1206
- sendJSON(req, res, 200, {
1207
- uptime: process.uptime(),
1208
- db: dbStatus,
1209
- activeExecutionCount: activeExecutions.size,
1210
- queueSizes,
1211
- wsClientCount: syncClients.size,
1212
- memory: process.memoryUsage()
1213
- });
1214
- return;
1215
- }
1216
1208
 
1217
1209
  if (pathOnly === '/api/debug' && req.method === 'GET') {
1218
1210
  const execSnap = {};
@@ -1927,161 +1919,8 @@ const server = http.createServer(async (req, res) => {
1927
1919
  const utilHandler = _utilRoutes._match(req.method, pathOnly);
1928
1920
  if (utilHandler) { await utilHandler(req, res); return; }
1929
1921
 
1930
- // THREAD API ENDPOINTS (ACP v0.2.3)
1931
- // ============================================================
1932
-
1933
- // POST /threads - Create empty thread
1934
- if (pathOnly === '/api/threads' && req.method === 'POST') {
1935
- console.log('[ACP] POST /api/threads HIT');
1936
- try {
1937
- const body = await parseBody(req);
1938
- const metadata = body.metadata || {};
1939
- const thread = queries.createThread(metadata);
1940
- sendJSON(req, res, 201, thread);
1941
- } catch (err) {
1942
- sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
1943
- }
1944
- return;
1945
- }
1946
-
1947
- // POST /threads/search - Search threads
1948
- if (pathOnly === '/api/threads/search' && req.method === 'POST') {
1949
- try {
1950
- const body = await parseBody(req);
1951
- const result = queries.searchThreads(body);
1952
- sendJSON(req, res, 200, result);
1953
- } catch (err) {
1954
- sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
1955
- }
1956
- return;
1957
- }
1958
-
1959
- // GET /threads/{thread_id} - Get thread by ID
1960
- const acpThreadMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})$/);
1961
- if (acpThreadMatch && req.method === 'GET') {
1962
- const threadId = acpThreadMatch[1];
1963
- try {
1964
- const thread = queries.getThread(threadId);
1965
- if (!thread) {
1966
- sendJSON(req, res, 404, { error: 'Thread not found', type: 'not_found' });
1967
- } else {
1968
- sendJSON(req, res, 200, thread);
1969
- }
1970
- } catch (err) {
1971
- sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
1972
- }
1973
- return;
1974
- }
1975
-
1976
- // PATCH /threads/{thread_id} - Update thread metadata
1977
- if (acpThreadMatch && req.method === 'PATCH') {
1978
- const threadId = acpThreadMatch[1];
1979
- try {
1980
- const body = await parseBody(req);
1981
- const thread = queries.patchThread(threadId, body);
1982
- sendJSON(req, res, 200, thread);
1983
- } catch (err) {
1984
- if (err.message.includes('not found')) {
1985
- sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
1986
- } else {
1987
- sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
1988
- }
1989
- }
1990
- return;
1991
- }
1992
-
1993
- // DELETE /threads/{thread_id} - Delete thread (fail if pending runs exist)
1994
- if (acpThreadMatch && req.method === 'DELETE') {
1995
- const threadId = acpThreadMatch[1];
1996
- try {
1997
- queries.deleteThread(threadId);
1998
- res.writeHead(204);
1999
- res.end();
2000
- } catch (err) {
2001
- if (err.message.includes('not found')) {
2002
- sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
2003
- } else if (err.message.includes('pending runs')) {
2004
- sendJSON(req, res, 409, { error: err.message, type: 'conflict' });
2005
- } else {
2006
- sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
2007
- }
2008
- }
2009
- return;
2010
- }
2011
-
2012
- // GET /threads/{thread_id}/history - Get thread state history with pagination
2013
- const threadHistoryMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/history$/);
2014
- if (threadHistoryMatch && req.method === 'GET') {
2015
- const threadId = threadHistoryMatch[1];
2016
- try {
2017
- const url = new URL(req.url, `http://${req.headers.host}`);
2018
- const limit = parseInt(url.searchParams.get('limit') || '50', 10);
2019
- const before = url.searchParams.get('before') || null;
2020
-
2021
- // Convert 'before' cursor to offset if needed
2022
- let offset = 0;
2023
- if (before) {
2024
- // Simple cursor-based pagination: before is an offset value
2025
- offset = parseInt(before, 10);
2026
- }
2027
-
2028
- const result = queries.getThreadHistory(threadId, limit, offset);
2029
-
2030
- // Generate next_cursor if there are more results
2031
- const response = {
2032
- states: result.states,
2033
- next_cursor: result.hasMore ? String(offset + limit) : null
2034
- };
2035
-
2036
- sendJSON(req, res, 200, response);
2037
- } catch (err) {
2038
- if (err.message.includes('not found')) {
2039
- sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
2040
- } else {
2041
- sendJSON(req, res, 422, { error: err.message, type: 'validation_error' });
2042
- }
2043
- }
2044
- return;
2045
- }
2046
-
2047
- // POST /threads/{thread_id}/copy - Copy thread with all states/checkpoints
2048
- const threadCopyMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/copy$/);
2049
- if (threadCopyMatch && req.method === 'POST') {
2050
- const sourceThreadId = threadCopyMatch[1];
2051
- try {
2052
- const body = await parseBody(req);
2053
- const newThread = queries.copyThread(sourceThreadId);
2054
-
2055
- // Update metadata if provided
2056
- if (body.metadata) {
2057
- const updated = queries.patchThread(newThread.thread_id, { metadata: body.metadata });
2058
- sendJSON(req, res, 201, updated);
2059
- } else {
2060
- sendJSON(req, res, 201, newThread);
2061
- }
2062
- } catch (err) {
2063
- if (err.message.includes('not found')) {
2064
- sendJSON(req, res, 404, { error: err.message, type: 'not_found' });
2065
- } else {
2066
- sendJSON(req, res, 500, { error: err.message, type: 'internal_error' });
2067
- }
2068
- }
2069
- return;
2070
- }
2071
-
2072
- // POST /threads/{thread_id}/runs/stream - SSE removed, use WebSocket
2073
- const threadRunsStreamMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/stream$/);
2074
- if (threadRunsStreamMatch && req.method === 'POST') {
2075
- res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
2076
- return;
2077
- }
2078
-
2079
- // GET /threads/{thread_id}/runs/{run_id}/stream - SSE removed, use WebSocket
2080
- const threadRunStreamMatch = pathOnly.match(/^\/api\/threads\/([a-f0-9-]{36})\/runs\/([a-f0-9-]{36})\/stream$/);
2081
- if (threadRunStreamMatch && req.method === 'GET') {
2082
- res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
2083
- return;
2084
- }
1922
+ const threadHandler = _threadRoutes._match(req.method, pathOnly);
1923
+ if (threadHandler) { await threadHandler(req, res); return; }
2085
1924
 
2086
1925
  if (routePath.startsWith('/api/image/')) {
2087
1926
  const imagePath = routePath.slice('/api/image/'.length);
@@ -2245,7 +2084,8 @@ function serveFile(filePath, res, req) {
2245
2084
  fs.readFile(filePath, (err2, data) => {
2246
2085
  if (err2) { res.writeHead(500); res.end('Server error'); return; }
2247
2086
  let content = data.toString();
2248
- const baseTag = `<script>window.__BASE_URL='${BASE_URL}';window.__SERVER_VERSION='${PKG_VERSION}';</script>`;
2087
+ const wsToken = process.env.PASSWORD ? `window.__WS_TOKEN='${process.env.PASSWORD.replace(/'/g, "\\'")}';` : '';
2088
+ const baseTag = `<script>window.__BASE_URL='${BASE_URL}';window.__SERVER_VERSION='${PKG_VERSION}';${wsToken}</script>`;
2249
2089
  content = content.replace('<head>', `<head>\n <base href="${BASE_URL}/">\n ` + baseTag);
2250
2090
  content = content.replace(/(href|src)="vendor\//g, `$1="${BASE_URL}/vendor/`);
2251
2091
  content = content.replace(/(src)="\/gm\/js\//g, `$1="${BASE_URL}/js/`);
@@ -2973,12 +2813,18 @@ const subscriptionIndex = new Map();
2973
2813
  const pm2Subscribers = new Set();
2974
2814
 
2975
2815
  wss.on('connection', (ws, req) => {
2976
- // req.url in WebSocket is just the path (e.g., '/gm/sync'), not a full URL
2977
- const wsPath = req.url.startsWith(BASE_URL) ? req.url.slice(BASE_URL.length) : req.url;
2978
- if (wsPath === '/hot-reload') {
2816
+ const _pwd = process.env.PASSWORD;
2817
+ if (_pwd) {
2818
+ const url = new URL(req.url, 'http://localhost');
2819
+ const token = url.searchParams.get('token');
2820
+ if (token !== _pwd) { ws.close(4001, 'Unauthorized'); return; }
2821
+ }
2822
+ const wsPath = req.url.split('?')[0];
2823
+ const wsRoute = wsPath.startsWith(BASE_URL) ? wsPath.slice(BASE_URL.length) : wsPath;
2824
+ if (wsRoute === '/hot-reload') {
2979
2825
  hotReloadClients.push(ws);
2980
2826
  ws.on('close', () => { const i = hotReloadClients.indexOf(ws); if (i > -1) hotReloadClients.splice(i, 1); });
2981
- } else if (wsPath === '/sync') {
2827
+ } else if (wsRoute === '/sync') {
2982
2828
  syncClients.add(ws);
2983
2829
  ws.isAlive = true;
2984
2830
  ws.subscriptions = new Set();
@@ -3009,7 +2855,7 @@ wss.on('connection', (ws, req) => {
3009
2855
  if (ws.pm2Subscribed) {
3010
2856
  pm2Subscribers.delete(ws);
3011
2857
  }
3012
- console.log(`[WebSocket] Client ${ws.clientId} disconnected`);
2858
+ debugLog(`[WebSocket] Client ${ws.clientId} disconnected`);
3013
2859
  });
3014
2860
  }
3015
2861
  });
@@ -3072,6 +2918,7 @@ const _speechRoutes = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync,
3072
2918
  const _oauthRoutes = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
3073
2919
  const _utilRoutes = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
3074
2920
  const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
2921
+ const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
3075
2922
 
3076
2923
  registerConvHandlers(wsRouter, {
3077
2924
  queries, activeExecutions, rateLimitState,
@@ -3086,13 +2933,13 @@ registerMsgHandlers(wsRouter, {
3086
2933
 
3087
2934
  registerQueueHandlers(wsRouter, { queries, messageQueues, broadcastSync });
3088
2935
 
3089
- console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.length:', discoveredAgents.length);
2936
+ debugLog('[INIT] registerSessionHandlers, agents: ' + discoveredAgents.length);
3090
2937
  registerSessionHandlers(wsRouter, {
3091
2938
  db: queries, discoveredAgents, modelCache,
3092
2939
  getAgentDescriptor, activeScripts, broadcastSync,
3093
2940
  startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
3094
2941
  });
3095
- console.log('[INIT] registerSessionHandlers completed');
2942
+ debugLog('[INIT] registerSessionHandlers completed');
3096
2943
 
3097
2944
  registerRunHandlers(wsRouter, {
3098
2945
  queries, discoveredAgents, activeExecutions, activeProcessesByRunId,
@@ -3165,7 +3012,7 @@ wsRouter.onLegacy((data, ws) => {
3165
3012
  if (data.conversationId && checkpointManager.hasPendingCheckpoint(data.conversationId)) {
3166
3013
  const checkpoint = checkpointManager.getPendingCheckpoint(data.conversationId);
3167
3014
  if (checkpoint) {
3168
- console.log(`[checkpoint] Injecting ${checkpoint.events.length} events to client for ${data.conversationId}`);
3015
+ debugLog(`[checkpoint] Injecting ${checkpoint.events.length} events to client for ${data.conversationId}`);
3169
3016
 
3170
3017
  const latestSession = queries.getLatestSession(data.conversationId);
3171
3018
  if (latestSession) {
@@ -113,7 +113,9 @@ class WebSocketManager {
113
113
  getWebSocketURL() {
114
114
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
115
115
  const baseURL = window.__BASE_URL || '/gm';
116
- return `${protocol}//${window.location.host}${baseURL}/sync`;
116
+ let url = `${protocol}//${window.location.host}${baseURL}/sync`;
117
+ if (window.__WS_TOKEN) url += `?token=${encodeURIComponent(window.__WS_TOKEN)}`;
118
+ return url;
117
119
  }
118
120
 
119
121
  async connect() {