agentgui 1.0.839 → 1.0.841

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 CHANGED
@@ -1,6 +1,8 @@
1
1
  ## [Unreleased]
2
2
 
3
3
  ### Refactor
4
+ - Extract maskKey, getProviderConfigs, saveProviderConfig, buildSystemPrompt, PROVIDER_CONFIGS from server.js to lib/provider-config.js (151L); extract logError, makeCleanupExecution, makeGetModelsForAgent, errLogPath from server.js to lib/server-utils.js (61L); server.js imports all via named imports; cleanupExecution wired after broadcastSync; _debugRoutes receives errLogPath
5
+ - Extract parseBody, acceptsEncoding, compressAndSend, sendJSON from server.js to lib/http-utils.js (43L); server.js imports from new module; zlib import removed from server.js
4
6
  - Extract message/stream/queue routes (messagesMatch, streamMatch, queueMatch handlers) to lib/routes-messages.js (140L) and session/chunk/full/execution routes to lib/routes-sessions.js (145L); server.js reduced from 2406L to 2127L; both files ≤200L; wired via _messagesRoutes._match and _sessionsRoutes._match in request handler
5
7
  - Extract runs/scripts/agent-auth/auth-config HTTP routes from server.js to lib/routes-runs.js (157L), lib/routes-scripts.js (136L), lib/routes-agent-actions.js (118L), lib/routes-auth-config.js (30L); routes-auth-config uses getProviderConfigs/saveProviderConfig from server.js deps (no duplication); server.js reduced from 2406L to 1399L total (-1007L)
6
8
  - Extract processMessageWithStreaming (539L), scheduleRetry, drainMessageQueue, and parseRateLimitResetTime from server.js into lib/process-message.js (127L, createProcessMessage factory), lib/stream-event-handler.js (116L, createEventHandler), lib/message-queue.js (63L, createMessageQueue), lib/process-message-rate-limit.js (19L); all files ≤200L; server.js reduced by ~660L and imports/wires all factories after broadcastSync is created
package/CLAUDE.md CHANGED
@@ -54,6 +54,12 @@ lib/routes-tools.js Tool management HTTP route handlers (list, install, updat
54
54
  lib/routes-util.js Utility HTTP route handlers (clone, folders, git, home, version, import)
55
55
  lib/routes-agents.js Agent list/search/auth-status/descriptor/models HTTP route handlers
56
56
  lib/routes-conversations.js Conversation CRUD HTTP route handlers (list, create, get, update, delete, archive, restore)
57
+ lib/routes-messages.js Message/stream/queue HTTP route handlers (GET+POST messages, stream, queue CRUD)
58
+ lib/routes-sessions.js Session/chunk/full/execution HTTP route handlers (session get, chunks, full load, execution events)
59
+ lib/routes-runs.js Runs HTTP route handlers (POST /api/runs, runs search, run by id, wait, cancel, thread run cancel/wait)
60
+ lib/routes-scripts.js Scripts/cancel/resume/inject HTTP route handlers (conversation scripts, run-script, stop-script, cancel, resume, inject)
61
+ lib/routes-agent-actions.js Agent auth and update HTTP route handlers (POST /api/agents/:id/auth, POST /api/agents/:id/update)
62
+ lib/routes-auth-config.js Auth config HTTP route handlers (GET /api/auth/configs, POST /api/auth/save-config)
57
63
  lib/routes-debug.js Debug/backup/restore/ws-stats HTTP route handlers
58
64
  lib/routes-threads.js Thread CRUD HTTP route handlers (ACP v0.2.3 thread API)
59
65
  lib/ws-protocol.js WebSocket RPC router (WsRouter class)
@@ -74,27 +80,27 @@ static/js/app-shortcuts.js Keyboard shortcuts overlay
74
80
  static/theme.js Theme switching
75
81
  static/css/main.css All application styles (extracted from index.html)
76
82
  static/css/tools-popup.css Tool popup styles
77
- static/js/client.js AgentGUIClient class (constructor + _dbg + init); instantiation at bottom
78
- static/js/client-ws.js WebSocket listeners, _convIsStreaming, _setConvStreaming, setupRendererListeners, restoreStateFromUrl, isValidId (prototype extension)
79
- static/js/client-url.js URL/scroll helpers: updateUrlForConversation, saveScrollPosition, restoreScrollPosition, setupScrollTracking (prototype extension)
80
- static/js/client-ui.js setupUI (modified, calls _setupUIButtonEvents/_setupUIWindowEvents) + setupChatMicButton (prototype extension)
81
- static/js/client-ui-controls.js _setupUIButtonEvents + _setupUIWindowEvents extracted helpers (prototype extension)
82
- static/js/client-ws-msg.js connectWebSocket, handleWebSocketMessage, queueEvent (prototype extension)
83
- static/js/client-streaming.js handleStreamingStart (prototype extension)
84
- static/js/client-streaming2.js handleStreamingResumed, handleStreamingProgress, _handleStreamingProgressInner (prototype extension)
85
- static/js/client-streaming3.js renderBlockContent, scrollToBottom, _showNewContentPill, _removeNewContentPill, handleStreamingError (prototype extension)
86
- static/js/client-streaming4.js handleStreamingComplete, _promptPushIfWeOwnRemote, handleConversationCreated, handleMessageCreated, queue handlers (prototype extension)
87
- static/js/client-events.js fetchAndRenderQueue, handleRateLimitHit/Clear, handleAllConversationsDeleted, isHtmlContent, sanitizeHtml, parseMarkdownCodeBlocks (prototype extension)
88
- static/js/client-render.js renderCodeBlock, renderMessageContent (prototype extension)
89
- static/js/client-exec.js startExecution, optimistic message helpers, _subscribeToConversationUpdates, _flushBgCache (prototype extension)
90
- static/js/client-helpers.js _recoverMissedChunks, cache/placeholder/height/countdown/debug helpers, showLoadingSpinner/hideLoadingSpinner (prototype extension)
91
- static/js/client-ui2.js _showWelcomeScreen, _showSkeletonLoading, streamToConversation, _hydrateSessionBlocks (prototype extension)
92
- static/js/client-conv.js _getLazyObserver, _renderConversationContent, renderChunk, _renderChunkInner, loadAgents, loadSubAgentsForCli (prototype extension)
93
- static/js/client-agents.js checkSpeechStatus, loadModelsForAgent, _populateModelSelector, lock/unlockAgentAndModel, applyAgentAndModelSelection, loadConversations, updateConnectionStatus (prototype extension)
94
- static/js/client-status.js _updateConnectionIndicator, _handleModelDownloadProgress, _handleTTSSetupProgress, _toggleConnectionTooltip, updateMetrics, controls, toggleTheme, createNewConversation (prototype extension)
95
- static/js/client-cache.js cacheCurrentConversation, invalidateCache, loadConversationMessages (prototype extension)
96
- static/js/client-load.js _makeLoadRequest, _verifyRequestId, _completeLoadRequest, _loadConvRender (prototype extension)
97
- static/js/client-scroll.js syncPromptState, updateBusyPromptArea, removeScrollUpDetection, setupScrollUpDetection (prototype extension)
83
+ static/js/client.js AgentGUIClient class (constructor + _dbg + init); instantiation at bottom
84
+ static/js/client-ws.js WebSocket listeners, _convIsStreaming, _setConvStreaming, setupRendererListeners, restoreStateFromUrl, isValidId (prototype extension)
85
+ static/js/client-url.js URL/scroll helpers: updateUrlForConversation, saveScrollPosition, restoreScrollPosition, setupScrollTracking (prototype extension)
86
+ static/js/client-ui.js setupUI (modified, calls _setupUIButtonEvents/_setupUIWindowEvents) + setupChatMicButton (prototype extension)
87
+ static/js/client-ui-controls.js _setupUIButtonEvents + _setupUIWindowEvents extracted helpers (prototype extension)
88
+ static/js/client-ws-msg.js connectWebSocket, handleWebSocketMessage, queueEvent (prototype extension)
89
+ static/js/client-streaming.js handleStreamingStart (prototype extension)
90
+ static/js/client-streaming2.js handleStreamingResumed, handleStreamingProgress, _handleStreamingProgressInner (prototype extension)
91
+ static/js/client-streaming3.js renderBlockContent, scrollToBottom, _showNewContentPill, _removeNewContentPill, handleStreamingError (prototype extension)
92
+ static/js/client-streaming4.js handleStreamingComplete, _promptPushIfWeOwnRemote, handleConversationCreated, handleMessageCreated, queue handlers (prototype extension)
93
+ static/js/client-events.js fetchAndRenderQueue, handleRateLimitHit/Clear, handleAllConversationsDeleted, isHtmlContent, sanitizeHtml, parseMarkdownCodeBlocks (prototype extension)
94
+ static/js/client-render.js renderCodeBlock, renderMessageContent (prototype extension)
95
+ static/js/client-exec.js startExecution, optimistic message helpers, _subscribeToConversationUpdates, _flushBgCache (prototype extension)
96
+ static/js/client-helpers.js _recoverMissedChunks, cache/placeholder/height/countdown/debug helpers, showLoadingSpinner/hideLoadingSpinner (prototype extension)
97
+ static/js/client-ui2.js _showWelcomeScreen, _showSkeletonLoading, streamToConversation, _hydrateSessionBlocks (prototype extension)
98
+ static/js/client-conv.js _getLazyObserver, _renderConversationContent, renderChunk, _renderChunkInner, loadAgents, loadSubAgentsForCli (prototype extension)
99
+ static/js/client-agents.js checkSpeechStatus, loadModelsForAgent, _populateModelSelector, lock/unlockAgentAndModel, applyAgentAndModelSelection, loadConversations, updateConnectionStatus (prototype extension)
100
+ static/js/client-status.js _updateConnectionIndicator, _handleModelDownloadProgress, _handleTTSSetupProgress, _toggleConnectionTooltip, updateMetrics, controls, toggleTheme, createNewConversation (prototype extension)
101
+ static/js/client-cache.js cacheCurrentConversation, invalidateCache, loadConversationMessages (prototype extension)
102
+ static/js/client-load.js _makeLoadRequest, _verifyRequestId, _completeLoadRequest, _loadConvRender (prototype extension)
103
+ static/js/client-scroll.js syncPromptState, updateBusyPromptArea, removeScrollUpDetection, setupScrollUpDetection (prototype extension)
98
104
  static/js/client-utils.js renderMessagesFragment, renderMessages, escapeHtml, showError, on, emit, agent/model getters, draft/prompt helpers, destroy (prototype extension)
99
105
  static/js/conversations.js Conversation management (class definition)
100
106
  static/js/conv-list-renderer.js Conversation list render, CRUD, WS listener (prototype extension)
@@ -0,0 +1,42 @@
1
+ import zlib from 'zlib';
2
+
3
+ export function parseBody(req) {
4
+ return new Promise((resolve, reject) => {
5
+ let body = '';
6
+ req.on('data', chunk => body += chunk);
7
+ req.on('error', reject);
8
+ req.on('end', () => {
9
+ try { resolve(body ? JSON.parse(body) : {}); }
10
+ catch (e) { reject(new Error('Invalid JSON')); }
11
+ });
12
+ });
13
+ }
14
+
15
+ export function acceptsEncoding(req, encoding) {
16
+ const accept = req.headers['accept-encoding'] || '';
17
+ return accept.includes(encoding);
18
+ }
19
+
20
+ export function compressAndSend(req, res, statusCode, contentType, body) {
21
+ const raw = typeof body === 'string' ? Buffer.from(body) : body;
22
+ const isHtml = contentType && contentType.includes('text/html');
23
+ const baseHeaders = { 'Content-Type': contentType };
24
+ if (isHtml) baseHeaders['Cache-Control'] = 'no-store';
25
+ if (raw.length < 860) {
26
+ res.writeHead(statusCode, { ...baseHeaders, 'Content-Length': raw.length });
27
+ res.end(raw);
28
+ return;
29
+ }
30
+ if (acceptsEncoding(req, 'gzip')) {
31
+ const compressed = zlib.gzipSync(raw, { level: 6 });
32
+ res.writeHead(statusCode, { ...baseHeaders, 'Content-Encoding': 'gzip', 'Content-Length': compressed.length });
33
+ res.end(compressed);
34
+ } else {
35
+ res.writeHead(statusCode, { ...baseHeaders, 'Content-Length': raw.length });
36
+ res.end(raw);
37
+ }
38
+ }
39
+
40
+ export function sendJSON(req, res, statusCode, data) {
41
+ compressAndSend(req, res, statusCode, 'application/json', JSON.stringify(data));
42
+ }
@@ -0,0 +1,150 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { getGeminiOAuthStatus } from './oauth-gemini.js';
5
+ import { getCodexOAuthStatus } from './oauth-codex.js';
6
+
7
+ export function buildSystemPrompt(agentId, model, subAgent) {
8
+ const parts = [];
9
+ if (agentId && agentId !== 'claude-code') {
10
+ const displayAgentId = agentId.split('-·-')[0];
11
+ parts.push(`Use ${displayAgentId} subagent for all tasks.`);
12
+ }
13
+ if (model) parts.push(`Model: ${model}.`);
14
+ if (subAgent) parts.push(`Subagent: ${subAgent}.`);
15
+ return parts.join(' ');
16
+ }
17
+
18
+ const PROVIDER_CONFIGS = {
19
+ 'anthropic': {
20
+ name: 'Anthropic', configPaths: [
21
+ path.join(os.homedir(), '.claude.json'),
22
+ path.join(os.homedir(), '.config', 'claude', 'settings.json'),
23
+ path.join(os.homedir(), '.anthropic.json')
24
+ ],
25
+ configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
26
+ },
27
+ 'openai': {
28
+ name: 'OpenAI', configPaths: [
29
+ path.join(os.homedir(), '.openai.json'),
30
+ path.join(os.homedir(), '.config', 'openai', 'api-key')
31
+ ],
32
+ configFormat: (apiKey, model) => ({ apiKey, defaultModel: model })
33
+ },
34
+ 'google': {
35
+ name: 'Google Gemini', configPaths: [
36
+ path.join(os.homedir(), '.gemini.json'),
37
+ path.join(os.homedir(), '.config', 'gemini', 'credentials.json')
38
+ ],
39
+ configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
40
+ },
41
+ 'openrouter': {
42
+ name: 'OpenRouter', configPaths: [
43
+ path.join(os.homedir(), '.openrouter.json'),
44
+ path.join(os.homedir(), '.config', 'openrouter', 'config.json')
45
+ ],
46
+ configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
47
+ },
48
+ 'github': {
49
+ name: 'GitHub Models', configPaths: [
50
+ path.join(os.homedir(), '.github.json'),
51
+ path.join(os.homedir(), '.config', 'github-copilot.json')
52
+ ],
53
+ configFormat: (apiKey, model) => ({ github_token: apiKey, default_model: model })
54
+ },
55
+ 'azure': {
56
+ name: 'Azure OpenAI', configPaths: [
57
+ path.join(os.homedir(), '.azure.json'),
58
+ path.join(os.homedir(), '.config', 'azure-openai', 'config.json')
59
+ ],
60
+ configFormat: (apiKey, model) => ({ api_key: apiKey, endpoint: '', default_model: model })
61
+ },
62
+ 'anthropic-claude-code': {
63
+ name: 'Claude Code Max', configPaths: [
64
+ path.join(os.homedir(), '.claude', 'max.json'),
65
+ path.join(os.homedir(), '.config', 'claude-code', 'max.json')
66
+ ],
67
+ configFormat: (apiKey, model) => ({ api_key: apiKey, plan: 'max', default_model: model })
68
+ },
69
+ 'opencode': {
70
+ name: 'OpenCode', configPaths: [
71
+ path.join(os.homedir(), '.opencode', 'config.json'),
72
+ path.join(os.homedir(), '.config', 'opencode', 'config.json')
73
+ ],
74
+ configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model, providers: ['anthropic', 'openai', 'google'] })
75
+ },
76
+ 'proxypilot': {
77
+ name: 'ProxyPilot', configPaths: [
78
+ path.join(os.homedir(), '.proxypilot', 'config.json'),
79
+ path.join(os.homedir(), '.config', 'proxypilot', 'config.json')
80
+ ],
81
+ configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
82
+ },
83
+ 'codex': {
84
+ name: 'Codex CLI', configPaths: [
85
+ path.join(os.homedir(), '.codex', 'auth.json')
86
+ ],
87
+ configFormat: (apiKey) => ({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey })
88
+ }
89
+ };
90
+
91
+ export function maskKey(key) {
92
+ if (!key || key.length < 8) return '****';
93
+ return '****' + key.slice(-4);
94
+ }
95
+
96
+ export function getProviderConfigs() {
97
+ const configs = {};
98
+ for (const [providerId, config] of Object.entries(PROVIDER_CONFIGS)) {
99
+ if (providerId === 'google') {
100
+ const oauthStatus = getGeminiOAuthStatus();
101
+ if (oauthStatus) {
102
+ configs[providerId] = { name: config.name, ...oauthStatus };
103
+ continue;
104
+ }
105
+ }
106
+ if (providerId === 'codex') {
107
+ const oauthStatus = getCodexOAuthStatus();
108
+ if (oauthStatus) {
109
+ configs[providerId] = { name: config.name, ...oauthStatus };
110
+ continue;
111
+ }
112
+ }
113
+ for (const configPath of config.configPaths) {
114
+ try {
115
+ if (fs.existsSync(configPath)) {
116
+ const content = fs.readFileSync(configPath, 'utf8');
117
+ const parsed = JSON.parse(content);
118
+ const rawKey = parsed.api_key || parsed.apiKey || parsed.github_token || parsed.OPENAI_API_KEY || '';
119
+ configs[providerId] = {
120
+ name: config.name,
121
+ apiKey: maskKey(rawKey),
122
+ hasKey: !!rawKey,
123
+ defaultModel: parsed.default_model || parsed.defaultModel || '',
124
+ path: configPath
125
+ };
126
+ break;
127
+ }
128
+ } catch (_) {}
129
+ }
130
+ if (!configs[providerId]) {
131
+ configs[providerId] = { name: config.name, apiKey: '', hasKey: false, defaultModel: '', path: '' };
132
+ }
133
+ }
134
+ return configs;
135
+ }
136
+
137
+ export function saveProviderConfig(providerId, apiKey, defaultModel) {
138
+ const config = PROVIDER_CONFIGS[providerId];
139
+ if (!config) throw new Error('Unknown provider: ' + providerId);
140
+ const configPath = config.configPaths[0];
141
+ const configDir = path.dirname(configPath);
142
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
143
+ let existing = {};
144
+ try {
145
+ if (fs.existsSync(configPath)) existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
146
+ } catch (_) {}
147
+ const merged = { ...existing, ...config.configFormat(apiKey, defaultModel) };
148
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
149
+ return configPath;
150
+ }
@@ -0,0 +1,60 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export const errLogPath = path.join(os.homedir(), 'logs', 'agentgui-errors.log');
6
+
7
+ export function logError(op, err, ctx = {}) {
8
+ try {
9
+ const line = JSON.stringify({ ts: new Date().toISOString(), op, msg: err?.message, stack: err?.stack, ...ctx }) + '\n';
10
+ fs.appendFile(errLogPath, line, () => {});
11
+ } catch (_) {}
12
+ }
13
+
14
+ export function makeCleanupExecution(deps) {
15
+ const { execMachine, activeExecutions, queries, broadcastSync, debugLog } = deps;
16
+ return function cleanupExecution(conversationId, broadcastCompletion = false) {
17
+ debugLog(`[cleanup] Starting cleanup for ${conversationId}`);
18
+ const machineSnap = execMachine.snapshot(conversationId);
19
+ if (machineSnap && machineSnap.value !== 'idle') {
20
+ execMachine.send(conversationId, { type: 'CANCEL' });
21
+ }
22
+ activeExecutions.delete(conversationId);
23
+ queries.setIsStreaming(conversationId, false);
24
+ if (broadcastCompletion) {
25
+ broadcastSync({
26
+ type: 'execution_cleaned_up',
27
+ conversationId,
28
+ timestamp: Date.now()
29
+ });
30
+ }
31
+ debugLog(`[cleanup] Cleanup complete for ${conversationId}`);
32
+ };
33
+ }
34
+
35
+ export function makeGetModelsForAgent(deps) {
36
+ const { modelCache, discoveredAgents, ensureRunning, queryACPModels } = deps;
37
+ return async function getModelsForAgent(agentId) {
38
+ const cached = modelCache.get(agentId);
39
+ if (cached && Date.now() - cached.timestamp < 300000) return cached.models;
40
+ let models = [];
41
+ if (agentId === 'claude-code') {
42
+ models = [
43
+ { id: 'haiku', label: 'Haiku' },
44
+ { id: 'sonnet', label: 'Sonnet' },
45
+ { id: 'opus', label: 'Opus' }
46
+ ];
47
+ } else {
48
+ const agent = discoveredAgents.find(a => a.id === agentId);
49
+ if (agent?.protocol === 'acp') {
50
+ await ensureRunning(agentId);
51
+ try { models = await queryACPModels(agentId); } catch (_) {}
52
+ } else if (agent?.protocol === 'cli-wrapper' && agent.acpId) {
53
+ await ensureRunning(agent.acpId);
54
+ try { models = await queryACPModels(agent.acpId); } catch (_) {}
55
+ }
56
+ }
57
+ modelCache.set(agentId, { models, timestamp: Date.now() });
58
+ return models;
59
+ };
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.839",
3
+ "version": "1.0.841",
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
@@ -2,7 +2,6 @@ import http from 'http';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
- import zlib from 'zlib';
6
5
  import { fileURLToPath } from 'url';
7
6
  import { WebSocketServer } from 'ws';
8
7
  import { execSync, spawn } from 'child_process';
@@ -37,6 +36,7 @@ import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getC
37
36
  import { WSOptimizer } from './lib/ws-optimizer.js';
38
37
  import { WsRouter } from './lib/ws-protocol.js';
39
38
  import { encode as wsEncode } from './lib/codec.js';
39
+ import { parseBody, acceptsEncoding, compressAndSend, sendJSON } from './lib/http-utils.js';
40
40
  const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); };
41
41
  import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
42
42
  import { register as registerConvHandlers2 } from './lib/ws-handlers-conv2.js';
@@ -63,6 +63,8 @@ import { parseRateLimitResetTime } from './lib/process-message-rate-limit.js';
63
63
  import { createEventHandler } from './lib/stream-event-handler.js';
64
64
  import { createMessageQueue } from './lib/message-queue.js';
65
65
  import { createProcessMessage } from './lib/process-message.js';
66
+ import { buildSystemPrompt, getProviderConfigs, saveProviderConfig } from './lib/provider-config.js';
67
+ import { logError, errLogPath, makeCleanupExecution, makeGetModelsForAgent } from './lib/server-utils.js';
66
68
 
67
69
 
68
70
  process.on('uncaughtException', (err, origin) => {
@@ -80,18 +82,6 @@ process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - un
80
82
  process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
81
83
  process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
82
84
 
83
-
84
- function buildSystemPrompt(agentId, model, subAgent) {
85
- const parts = [];
86
- if (agentId && agentId !== 'claude-code') {
87
- const displayAgentId = agentId.split('-·-')[0];
88
- parts.push(`Use ${displayAgentId} subagent for all tasks.`);
89
- }
90
- if (model) parts.push(`Model: ${model}.`);
91
- if (subAgent) parts.push(`Subagent: ${subAgent}.`);
92
- return parts.join(' ');
93
- }
94
-
95
85
  const activeExecutions = new Map();
96
86
  const activeScripts = new Map();
97
87
  const messageQueues = new Map();
@@ -108,41 +98,6 @@ const debugLog = (msg) => {
108
98
  console.error(`[${timestamp}] ${msg}`);
109
99
  };
110
100
 
111
- const _errLogPath = path.join(os.homedir(), 'logs', 'agentgui-errors.log');
112
- function logError(op, err, ctx = {}) {
113
- try {
114
- const line = JSON.stringify({ ts: new Date().toISOString(), op, msg: err?.message, stack: err?.stack, ...ctx }) + '\n';
115
- fs.appendFile(_errLogPath, line, () => {});
116
- } catch (_) {}
117
- }
118
-
119
- // Atomic cleanup function - machine is authoritative, Map stays in sync
120
- function cleanupExecution(conversationId, broadcastCompletion = false) {
121
- debugLog(`[cleanup] Starting cleanup for ${conversationId}`);
122
-
123
- // Machine drives cleanup: send CANCEL (clears queue, transitions to idle)
124
- const machineSnap = execMachine.snapshot(conversationId);
125
- if (machineSnap && machineSnap.value !== 'idle') {
126
- execMachine.send(conversationId, { type: 'CANCEL' });
127
- }
128
-
129
- // Keep Map in sync with machine
130
- activeExecutions.delete(conversationId);
131
-
132
- // Clean database state
133
- queries.setIsStreaming(conversationId, false);
134
-
135
- if (broadcastCompletion) {
136
- broadcastSync({
137
- type: 'execution_cleaned_up',
138
- conversationId,
139
- timestamp: Date.now()
140
- });
141
- }
142
-
143
- debugLog(`[cleanup] Cleanup complete for ${conversationId}`);
144
- }
145
-
146
101
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
147
102
  const rootDir = process.env.PORTABLE_EXE_DIR || __dirname;
148
103
  const PORT = process.env.PORT || 3000;
@@ -246,205 +201,7 @@ initializeAgentDiscovery(discoveredAgents, rootDir, logError).then(() => {
246
201
  }).catch(() => {});
247
202
 
248
203
  const modelCache = new Map();
249
-
250
- async function getModelsForAgent(agentId) {
251
- const cached = modelCache.get(agentId);
252
- if (cached && Date.now() - cached.timestamp < 300000) return cached.models;
253
- let models = [];
254
- if (agentId === 'claude-code') {
255
- models = [
256
- { id: 'haiku', label: 'Haiku' },
257
- { id: 'sonnet', label: 'Sonnet' },
258
- { id: 'opus', label: 'Opus' }
259
- ];
260
- } else {
261
- const agent = discoveredAgents.find(a => a.id === agentId);
262
- if (agent?.protocol === 'acp') {
263
- await ensureRunning(agentId);
264
- try { models = await queryACPModels(agentId); } catch (_) {}
265
- } else if (agent?.protocol === 'cli-wrapper' && agent.acpId) {
266
- await ensureRunning(agent.acpId);
267
- try { models = await queryACPModels(agent.acpId); } catch (_) {}
268
- }
269
- }
270
- modelCache.set(agentId, { models, timestamp: Date.now() });
271
- return models;
272
- }
273
-
274
- const PROVIDER_CONFIGS = {
275
- 'anthropic': {
276
- name: 'Anthropic', configPaths: [
277
- path.join(os.homedir(), '.claude.json'),
278
- path.join(os.homedir(), '.config', 'claude', 'settings.json'),
279
- path.join(os.homedir(), '.anthropic.json')
280
- ],
281
- configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
282
- },
283
- 'openai': {
284
- name: 'OpenAI', configPaths: [
285
- path.join(os.homedir(), '.openai.json'),
286
- path.join(os.homedir(), '.config', 'openai', 'api-key')
287
- ],
288
- configFormat: (apiKey, model) => ({ apiKey, defaultModel: model })
289
- },
290
- 'google': {
291
- name: 'Google Gemini', configPaths: [
292
- path.join(os.homedir(), '.gemini.json'),
293
- path.join(os.homedir(), '.config', 'gemini', 'credentials.json')
294
- ],
295
- configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
296
- },
297
- 'openrouter': {
298
- name: 'OpenRouter', configPaths: [
299
- path.join(os.homedir(), '.openrouter.json'),
300
- path.join(os.homedir(), '.config', 'openrouter', 'config.json')
301
- ],
302
- configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
303
- },
304
- 'github': {
305
- name: 'GitHub Models', configPaths: [
306
- path.join(os.homedir(), '.github.json'),
307
- path.join(os.homedir(), '.config', 'github-copilot.json')
308
- ],
309
- configFormat: (apiKey, model) => ({ github_token: apiKey, default_model: model })
310
- },
311
- 'azure': {
312
- name: 'Azure OpenAI', configPaths: [
313
- path.join(os.homedir(), '.azure.json'),
314
- path.join(os.homedir(), '.config', 'azure-openai', 'config.json')
315
- ],
316
- configFormat: (apiKey, model) => ({ api_key: apiKey, endpoint: '', default_model: model })
317
- },
318
- 'anthropic-claude-code': {
319
- name: 'Claude Code Max', configPaths: [
320
- path.join(os.homedir(), '.claude', 'max.json'),
321
- path.join(os.homedir(), '.config', 'claude-code', 'max.json')
322
- ],
323
- configFormat: (apiKey, model) => ({ api_key: apiKey, plan: 'max', default_model: model })
324
- },
325
- 'opencode': {
326
- name: 'OpenCode', configPaths: [
327
- path.join(os.homedir(), '.opencode', 'config.json'),
328
- path.join(os.homedir(), '.config', 'opencode', 'config.json')
329
- ],
330
- configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model, providers: ['anthropic', 'openai', 'google'] })
331
- },
332
- 'proxypilot': {
333
- name: 'ProxyPilot', configPaths: [
334
- path.join(os.homedir(), '.proxypilot', 'config.json'),
335
- path.join(os.homedir(), '.config', 'proxypilot', 'config.json')
336
- ],
337
- configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
338
- },
339
- 'codex': {
340
- name: 'Codex CLI', configPaths: [
341
- path.join(os.homedir(), '.codex', 'auth.json')
342
- ],
343
- configFormat: (apiKey) => ({ auth_mode: 'apikey', OPENAI_API_KEY: apiKey })
344
- }
345
- };
346
-
347
- function maskKey(key) {
348
- if (!key || key.length < 8) return '****';
349
- return '****' + key.slice(-4);
350
- }
351
-
352
- function getProviderConfigs() {
353
- const configs = {};
354
- for (const [providerId, config] of Object.entries(PROVIDER_CONFIGS)) {
355
- if (providerId === 'google') {
356
- const oauthStatus = getGeminiOAuthStatus();
357
- if (oauthStatus) {
358
- configs[providerId] = { name: config.name, ...oauthStatus };
359
- continue;
360
- }
361
- }
362
- if (providerId === 'codex') {
363
- const oauthStatus = getCodexOAuthStatus();
364
- if (oauthStatus) {
365
- configs[providerId] = { name: config.name, ...oauthStatus };
366
- continue;
367
- }
368
- }
369
- for (const configPath of config.configPaths) {
370
- try {
371
- if (fs.existsSync(configPath)) {
372
- const content = fs.readFileSync(configPath, 'utf8');
373
- const parsed = JSON.parse(content);
374
- const rawKey = parsed.api_key || parsed.apiKey || parsed.github_token || parsed.OPENAI_API_KEY || '';
375
- configs[providerId] = {
376
- name: config.name,
377
- apiKey: maskKey(rawKey),
378
- hasKey: !!rawKey,
379
- defaultModel: parsed.default_model || parsed.defaultModel || '',
380
- path: configPath
381
- };
382
- break;
383
- }
384
- } catch (_) {}
385
- }
386
- if (!configs[providerId]) {
387
- configs[providerId] = { name: config.name, apiKey: '', hasKey: false, defaultModel: '', path: '' };
388
- }
389
- }
390
- return configs;
391
- }
392
-
393
- function saveProviderConfig(providerId, apiKey, defaultModel) {
394
- const config = PROVIDER_CONFIGS[providerId];
395
- if (!config) throw new Error('Unknown provider: ' + providerId);
396
- const configPath = config.configPaths[0];
397
- const configDir = path.dirname(configPath);
398
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
399
- let existing = {};
400
- try {
401
- if (fs.existsSync(configPath)) existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
402
- } catch (_) {}
403
- const merged = { ...existing, ...config.configFormat(apiKey, defaultModel) };
404
- fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
405
- return configPath;
406
- }
407
-
408
- function parseBody(req) {
409
- return new Promise((resolve, reject) => {
410
- let body = '';
411
- req.on('data', chunk => body += chunk);
412
- req.on('error', reject);
413
- req.on('end', () => {
414
- try { resolve(body ? JSON.parse(body) : {}); }
415
- catch (e) { reject(new Error('Invalid JSON')); }
416
- });
417
- });
418
- }
419
-
420
- function acceptsEncoding(req, encoding) {
421
- const accept = req.headers['accept-encoding'] || '';
422
- return accept.includes(encoding);
423
- }
424
-
425
- function compressAndSend(req, res, statusCode, contentType, body) {
426
- const raw = typeof body === 'string' ? Buffer.from(body) : body;
427
- const isHtml = contentType && contentType.includes('text/html');
428
- const baseHeaders = { 'Content-Type': contentType };
429
- if (isHtml) baseHeaders['Cache-Control'] = 'no-store';
430
- if (raw.length < 860) {
431
- res.writeHead(statusCode, { ...baseHeaders, 'Content-Length': raw.length });
432
- res.end(raw);
433
- return;
434
- }
435
- if (acceptsEncoding(req, 'gzip')) {
436
- const compressed = zlib.gzipSync(raw, { level: 6 });
437
- res.writeHead(statusCode, { ...baseHeaders, 'Content-Encoding': 'gzip', 'Content-Length': compressed.length });
438
- res.end(compressed);
439
- } else {
440
- res.writeHead(statusCode, { ...baseHeaders, 'Content-Length': raw.length });
441
- res.end(raw);
442
- }
443
- }
444
-
445
- function sendJSON(req, res, statusCode, data) {
446
- compressAndSend(req, res, statusCode, 'application/json', JSON.stringify(data));
447
- }
204
+ const getModelsForAgent = makeGetModelsForAgent({ modelCache, discoveredAgents, ensureRunning, queryACPModels });
448
205
 
449
206
  const _rateLimitMap = new LRUCache({ max: 1000, ttl: 60000 });
450
207
  const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '300', 10);
@@ -741,6 +498,8 @@ const broadcastSync = createBroadcast({
741
498
  getSeq: () => ++broadcastSeq
742
499
  });
743
500
 
501
+ const cleanupExecution = makeCleanupExecution({ execMachine, activeExecutions, queries, broadcastSync, debugLog });
502
+
744
503
  // Wire up process-message factories now that broadcastSync and all deps are available
745
504
  const _mqDeps = {
746
505
  queries, messageQueues, activeExecutions, rateLimitState, execMachine,
@@ -767,7 +526,7 @@ const _oauthRoutes = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL,
767
526
  const _utilRoutes = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
768
527
  const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
769
528
  const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
770
- const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath });
529
+ const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath: errLogPath });
771
530
  const _convRoutes = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
772
531
  const _agentRoutes = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
773
532
  const _messagesRoutes = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });