agentgui 1.0.840 → 1.0.842
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 +2 -0
- package/CLAUDE.md +3 -0
- package/lib/http-utils.js +42 -0
- package/lib/provider-config.js +150 -0
- package/lib/server-utils.js +60 -0
- package/package.json +1 -1
- package/server.js +7 -248
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
|
@@ -60,6 +60,9 @@ lib/routes-runs.js Runs HTTP route handlers (POST /api/runs, runs search, ru
|
|
|
60
60
|
lib/routes-scripts.js Scripts/cancel/resume/inject HTTP route handlers (conversation scripts, run-script, stop-script, cancel, resume, inject)
|
|
61
61
|
lib/routes-agent-actions.js Agent auth and update HTTP route handlers (POST /api/agents/:id/auth, POST /api/agents/:id/update)
|
|
62
62
|
lib/routes-auth-config.js Auth config HTTP route handlers (GET /api/auth/configs, POST /api/auth/save-config)
|
|
63
|
+
lib/http-utils.js HTTP utility functions (parseBody, acceptsEncoding, compressAndSend, sendJSON)
|
|
64
|
+
lib/provider-config.js Provider config helpers (buildSystemPrompt, maskKey, getProviderConfigs, saveProviderConfig, PROVIDER_CONFIGS)
|
|
65
|
+
lib/server-utils.js Server utility functions (logError, errLogPath, makeCleanupExecution, makeGetModelsForAgent)
|
|
63
66
|
lib/routes-debug.js Debug/backup/restore/ws-stats HTTP route handlers
|
|
64
67
|
lib/routes-threads.js Thread CRUD HTTP route handlers (ACP v0.2.3 thread API)
|
|
65
68
|
lib/ws-protocol.js WebSocket RPC router (WsRouter class)
|
|
@@ -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
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 });
|