agentgui 1.0.840 → 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
@@ -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.840",
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 });