agentgui 1.0.751 → 1.0.752
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/lib/agent-discovery.js +161 -0
- package/package.json +1 -1
- package/server.js +42 -175
- package/static/app.js +78 -0
- package/static/css/main.css +104 -0
- package/static/index.html +4 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
const BINARIES = [
|
|
8
|
+
{ cmd: 'claude', id: 'claude-code', name: 'Claude Code', icon: 'C', protocol: 'cli' },
|
|
9
|
+
{ cmd: 'opencode', id: 'opencode', name: 'OpenCode', icon: 'O', protocol: 'acp', npxPackage: 'opencode-ai' },
|
|
10
|
+
{ cmd: 'gemini', id: 'gemini', name: 'Gemini CLI', icon: 'G', protocol: 'acp', npxPackage: '@google/gemini-cli' },
|
|
11
|
+
{ cmd: 'kilo', id: 'kilo', name: 'Kilo Code', icon: 'K', protocol: 'acp', npxPackage: '@kilocode/cli' },
|
|
12
|
+
{ cmd: 'goose', id: 'goose', name: 'Goose', icon: 'g', protocol: 'acp' },
|
|
13
|
+
{ cmd: 'openhands', id: 'openhands', name: 'OpenHands', icon: 'H', protocol: 'acp' },
|
|
14
|
+
{ cmd: 'augment', id: 'augment', name: 'Augment Code', icon: 'A', protocol: 'acp' },
|
|
15
|
+
{ cmd: 'cline', id: 'cline', name: 'Cline', icon: 'c', protocol: 'acp' },
|
|
16
|
+
{ cmd: 'kimi', id: 'kimi', name: 'Kimi CLI', icon: 'K', protocol: 'acp' },
|
|
17
|
+
{ cmd: 'qwen-code', id: 'qwen', name: 'Qwen Code', icon: 'Q', protocol: 'acp' },
|
|
18
|
+
{ cmd: 'codex', id: 'codex', name: 'Codex CLI', icon: 'X', protocol: 'acp', npxPackage: '@openai/codex' },
|
|
19
|
+
{ cmd: 'mistral-vibe', id: 'mistral', name: 'Mistral Vibe', icon: 'M', protocol: 'acp' },
|
|
20
|
+
{ cmd: 'kiro', id: 'kiro', name: 'Kiro CLI', icon: 'k', protocol: 'acp' },
|
|
21
|
+
{ cmd: 'fast-agent', id: 'fast-agent', name: 'fast-agent', icon: 'F', protocol: 'acp' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const CLI_WRAPPERS = [
|
|
25
|
+
{ id: 'cli-opencode', name: 'OpenCode', icon: 'O', protocol: 'cli-wrapper', acpId: 'opencode' },
|
|
26
|
+
{ id: 'cli-gemini', name: 'Gemini', icon: 'G', protocol: 'cli-wrapper', acpId: 'gemini' },
|
|
27
|
+
{ id: 'cli-kilo', name: 'Kilo', icon: 'K', protocol: 'cli-wrapper', acpId: 'kilo' },
|
|
28
|
+
{ id: 'cli-codex', name: 'Codex', icon: 'X', protocol: 'cli-wrapper', acpId: 'codex' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function findCommand(cmd, rootDir) {
|
|
32
|
+
const isWindows = os.platform() === 'win32';
|
|
33
|
+
const localBin = path.join(rootDir, 'node_modules', '.bin', isWindows ? cmd + '.cmd' : cmd);
|
|
34
|
+
if (fs.existsSync(localBin)) {
|
|
35
|
+
console.log(`[agent-discovery] Found ${cmd} in local node_modules`);
|
|
36
|
+
return localBin;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const timeoutMs = 10000;
|
|
40
|
+
if (isWindows) {
|
|
41
|
+
const result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
|
|
42
|
+
if (result) {
|
|
43
|
+
console.log(`[agent-discovery] Found ${cmd} in PATH`);
|
|
44
|
+
return result.split('\n')[0].trim();
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
|
|
48
|
+
if (result) {
|
|
49
|
+
console.log(`[agent-discovery] Found ${cmd} in PATH`);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.log(`[agent-discovery] ${cmd} not found or timed out`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function queryACPServerAgents(baseUrl) {
|
|
61
|
+
const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(endpoint, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
66
|
+
body: JSON.stringify({}),
|
|
67
|
+
signal: AbortSignal.timeout(5000)
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
if (!data?.agents || !Array.isArray(data.agents)) {
|
|
75
|
+
console.error(`Invalid agents response from ${baseUrl}`);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
return data.agents.map(agent => ({
|
|
79
|
+
id: agent.agent_id || agent.id,
|
|
80
|
+
name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
|
|
81
|
+
metadata: {
|
|
82
|
+
ref: {
|
|
83
|
+
name: agent.metadata?.ref?.name,
|
|
84
|
+
version: agent.metadata?.ref?.version,
|
|
85
|
+
url: agent.metadata?.ref?.url,
|
|
86
|
+
tags: agent.metadata?.ref?.tags
|
|
87
|
+
},
|
|
88
|
+
description: agent.metadata?.description,
|
|
89
|
+
author: agent.metadata?.author,
|
|
90
|
+
license: agent.metadata?.license
|
|
91
|
+
},
|
|
92
|
+
specs: agent.specs ? {
|
|
93
|
+
capabilities: agent.specs.capabilities,
|
|
94
|
+
input_schema: agent.specs.input_schema || agent.specs.input,
|
|
95
|
+
output_schema: agent.specs.output_schema || agent.specs.output,
|
|
96
|
+
thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
|
|
97
|
+
config_schema: agent.specs.config_schema || agent.specs.config,
|
|
98
|
+
custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
|
|
99
|
+
} : null,
|
|
100
|
+
custom_data: agent.custom_data,
|
|
101
|
+
icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
|
|
102
|
+
protocol: 'acp',
|
|
103
|
+
path: baseUrl
|
|
104
|
+
}));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(`ACP agents query failed for ${baseUrl}: ${error.message}`);
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function discoverAgents(rootDir) {
|
|
112
|
+
const agents = [];
|
|
113
|
+
for (const bin of BINARIES) {
|
|
114
|
+
const result = findCommand(bin.cmd, rootDir);
|
|
115
|
+
if (result) {
|
|
116
|
+
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: result, protocol: bin.protocol });
|
|
117
|
+
} else if (bin.npxPackage) {
|
|
118
|
+
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: bin.npxPackage, npxLaunchable: true });
|
|
119
|
+
} else if (bin.id === 'claude-code') {
|
|
120
|
+
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: '@anthropic-ai/claude-code', npxLaunchable: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('[discoverAgents] Found agents:', agents.map(a => a.id).join(', '));
|
|
125
|
+
for (const wrapper of CLI_WRAPPERS) {
|
|
126
|
+
if (agents.some(a => a.id === wrapper.acpId)) {
|
|
127
|
+
console.log(`[discoverAgents] Adding CLI wrapper for ${wrapper.id}`);
|
|
128
|
+
agents.push(wrapper);
|
|
129
|
+
} else {
|
|
130
|
+
console.log(`[discoverAgents] Skipping CLI wrapper ${wrapper.id} (ACP agent ${wrapper.acpId} not found)`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const wrappedAcpIds = new Set(CLI_WRAPPERS.filter(w => agents.some(a => a.id === w.acpId)).map(w => w.acpId));
|
|
134
|
+
const filtered = agents.filter(a => !wrappedAcpIds.has(a.id));
|
|
135
|
+
console.log('[discoverAgents] Final agent count:', filtered.length, 'Agent IDs:', filtered.map(a => a.id).join(', '));
|
|
136
|
+
return filtered;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function discoverExternalACPServers(discoveredAgents) {
|
|
140
|
+
const externalAgents = [];
|
|
141
|
+
for (const agent of discoveredAgents.filter(a => a.protocol === 'acp' && a.acpPort)) {
|
|
142
|
+
try {
|
|
143
|
+
const agents = await queryACPServerAgents(`http://localhost:${agent.acpPort}`);
|
|
144
|
+
externalAgents.push(...agents);
|
|
145
|
+
} catch (_) {}
|
|
146
|
+
}
|
|
147
|
+
return externalAgents;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function initializeAgentDiscovery(discoveredAgents, rootDir, logError) {
|
|
151
|
+
try {
|
|
152
|
+
const agents = discoverAgents(rootDir);
|
|
153
|
+
discoveredAgents.length = 0;
|
|
154
|
+
discoveredAgents.push(...agents);
|
|
155
|
+
console.log('[AGENTS] Discovered:', discoveredAgents.map(a => ({ id: a.id, found: !!a.path, protocol: a.protocol })));
|
|
156
|
+
console.log('[AGENTS] Total count:', discoveredAgents.length);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('[AGENTS] Discovery error:', err.message);
|
|
159
|
+
if (logError) logError('initializeAgentDiscovery', err);
|
|
160
|
+
}
|
|
161
|
+
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -17,6 +17,7 @@ import fsbrowse from 'fsbrowse';
|
|
|
17
17
|
import { queries } from './database.js';
|
|
18
18
|
import { runClaudeWithStreaming } from './lib/claude-runner.js';
|
|
19
19
|
import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
|
|
20
|
+
import { findCommand, queryACPServerAgents, discoverAgents, discoverExternalACPServers, initializeAgentDiscovery } from './lib/agent-discovery.js';
|
|
20
21
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
21
22
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
22
23
|
import { encode as wsEncode } from './lib/codec.js';
|
|
@@ -49,14 +50,20 @@ process.on('unhandledRejection', (reason, promise) => {
|
|
|
49
50
|
if (reason instanceof Error) console.error(reason.stack);
|
|
50
51
|
});
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
console.log(
|
|
53
|
+
function gracefulShutdown(signal) {
|
|
54
|
+
console.log(`[SIGNAL] ${signal} received - graceful shutdown`);
|
|
54
55
|
try { pm2Manager.disconnect(); } catch (_) {}
|
|
55
56
|
if (jsonlWatcher) try { jsonlWatcher.stop(); } catch (_) {}
|
|
57
|
+
for (const [convId, entry] of activeExecutions) {
|
|
58
|
+
try { if (entry.pid) process.kill(entry.pid, 'SIGTERM'); } catch (_) {}
|
|
59
|
+
}
|
|
56
60
|
stopACPTools().catch(() => {}).finally(() => {
|
|
57
61
|
try { wss.close(() => server.close(() => process.exit(0))); } catch (_) { process.exit(0); }
|
|
62
|
+
setTimeout(() => process.exit(1), 5000);
|
|
58
63
|
});
|
|
59
|
-
}
|
|
64
|
+
}
|
|
65
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
66
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
60
67
|
process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - uncrashable)'); });
|
|
61
68
|
process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
|
|
62
69
|
process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
|
|
@@ -444,180 +451,12 @@ expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
|
|
|
444
451
|
router(req, res, next);
|
|
445
452
|
});
|
|
446
453
|
|
|
447
|
-
function findCommand(cmd) {
|
|
448
|
-
const isWindows = os.platform() === 'win32';
|
|
449
|
-
const localBin = path.join(path.dirname(fileURLToPath(import.meta.url)), 'node_modules', '.bin', isWindows ? cmd + '.cmd' : cmd);
|
|
450
|
-
if (fs.existsSync(localBin)) {
|
|
451
|
-
console.log(`[agent-discovery] Found ${cmd} in local node_modules`);
|
|
452
|
-
return localBin;
|
|
453
|
-
}
|
|
454
|
-
try {
|
|
455
|
-
// Increase timeout to 10 seconds to handle slower systems and running agents
|
|
456
|
-
const timeoutMs = 10000;
|
|
457
|
-
if (isWindows) {
|
|
458
|
-
const result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
|
|
459
|
-
if (result) {
|
|
460
|
-
console.log(`[agent-discovery] Found ${cmd} in PATH`);
|
|
461
|
-
return result.split('\n')[0].trim();
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
const result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
|
|
465
|
-
if (result) {
|
|
466
|
-
console.log(`[agent-discovery] Found ${cmd} in PATH`);
|
|
467
|
-
return result;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
} catch (err) {
|
|
471
|
-
console.log(`[agent-discovery] ${cmd} not found or timed out`);
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async function queryACPServerAgents(baseUrl) {
|
|
478
|
-
const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
|
|
479
|
-
try {
|
|
480
|
-
const response = await fetch(endpoint, {
|
|
481
|
-
method: 'POST',
|
|
482
|
-
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
483
|
-
body: JSON.stringify({}),
|
|
484
|
-
signal: AbortSignal.timeout(5000)
|
|
485
|
-
});
|
|
486
|
-
if (!response.ok) {
|
|
487
|
-
console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
|
|
488
|
-
return [];
|
|
489
|
-
}
|
|
490
|
-
const data = await response.json();
|
|
491
|
-
if (!data?.agents || !Array.isArray(data.agents)) {
|
|
492
|
-
console.error(`Invalid agents response from ${baseUrl}`);
|
|
493
|
-
return [];
|
|
494
|
-
}
|
|
495
|
-
return data.agents.map(agent => ({
|
|
496
|
-
id: agent.agent_id || agent.id,
|
|
497
|
-
name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
|
|
498
|
-
metadata: {
|
|
499
|
-
ref: {
|
|
500
|
-
name: agent.metadata?.ref?.name,
|
|
501
|
-
version: agent.metadata?.ref?.version,
|
|
502
|
-
url: agent.metadata?.ref?.url,
|
|
503
|
-
tags: agent.metadata?.ref?.tags
|
|
504
|
-
},
|
|
505
|
-
description: agent.metadata?.description,
|
|
506
|
-
author: agent.metadata?.author,
|
|
507
|
-
license: agent.metadata?.license
|
|
508
|
-
},
|
|
509
|
-
specs: agent.specs ? {
|
|
510
|
-
capabilities: agent.specs.capabilities,
|
|
511
|
-
input_schema: agent.specs.input_schema || agent.specs.input,
|
|
512
|
-
output_schema: agent.specs.output_schema || agent.specs.output,
|
|
513
|
-
thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
|
|
514
|
-
config_schema: agent.specs.config_schema || agent.specs.config,
|
|
515
|
-
custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
|
|
516
|
-
} : null,
|
|
517
|
-
custom_data: agent.custom_data,
|
|
518
|
-
icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
|
|
519
|
-
protocol: 'acp',
|
|
520
|
-
path: baseUrl
|
|
521
|
-
}));
|
|
522
|
-
} catch (error) {
|
|
523
|
-
console.error(`ACP agents query failed for ${baseUrl}: ${error.message}`);
|
|
524
|
-
return [];
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function discoverAgents() {
|
|
529
|
-
const agents = [];
|
|
530
|
-
const binaries = [
|
|
531
|
-
{ cmd: 'claude', id: 'claude-code', name: 'Claude Code', icon: 'C', protocol: 'cli' },
|
|
532
|
-
{ cmd: 'opencode', id: 'opencode', name: 'OpenCode', icon: 'O', protocol: 'acp', npxPackage: 'opencode-ai' },
|
|
533
|
-
{ cmd: 'gemini', id: 'gemini', name: 'Gemini CLI', icon: 'G', protocol: 'acp', npxPackage: '@google/gemini-cli' },
|
|
534
|
-
{ cmd: 'kilo', id: 'kilo', name: 'Kilo Code', icon: 'K', protocol: 'acp', npxPackage: '@kilocode/cli' },
|
|
535
|
-
{ cmd: 'goose', id: 'goose', name: 'Goose', icon: 'g', protocol: 'acp' },
|
|
536
|
-
{ cmd: 'openhands', id: 'openhands', name: 'OpenHands', icon: 'H', protocol: 'acp' },
|
|
537
|
-
{ cmd: 'augment', id: 'augment', name: 'Augment Code', icon: 'A', protocol: 'acp' },
|
|
538
|
-
{ cmd: 'cline', id: 'cline', name: 'Cline', icon: 'c', protocol: 'acp' },
|
|
539
|
-
{ cmd: 'kimi', id: 'kimi', name: 'Kimi CLI', icon: 'K', protocol: 'acp' },
|
|
540
|
-
{ cmd: 'qwen-code', id: 'qwen', name: 'Qwen Code', icon: 'Q', protocol: 'acp' },
|
|
541
|
-
{ cmd: 'codex', id: 'codex', name: 'Codex CLI', icon: 'X', protocol: 'acp', npxPackage: '@openai/codex' },
|
|
542
|
-
{ cmd: 'mistral-vibe', id: 'mistral', name: 'Mistral Vibe', icon: 'M', protocol: 'acp' },
|
|
543
|
-
{ cmd: 'kiro', id: 'kiro', name: 'Kiro CLI', icon: 'k', protocol: 'acp' },
|
|
544
|
-
{ cmd: 'fast-agent', id: 'fast-agent', name: 'fast-agent', icon: 'F', protocol: 'acp' },
|
|
545
|
-
];
|
|
546
|
-
for (const bin of binaries) {
|
|
547
|
-
const result = findCommand(bin.cmd);
|
|
548
|
-
if (result) {
|
|
549
|
-
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: result, protocol: bin.protocol });
|
|
550
|
-
} else if (bin.npxPackage) {
|
|
551
|
-
// For npx-launchable packages (including claude-code as fallback)
|
|
552
|
-
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: bin.npxPackage, npxLaunchable: true });
|
|
553
|
-
} else if (bin.id === 'claude-code') {
|
|
554
|
-
// Ensure Claude Code is always available as an npx-launchable agent
|
|
555
|
-
agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: '@anthropic-ai/claude-code', npxLaunchable: true });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Add CLI tool wrappers for ACP agents (these map CLI commands to plugin sub-agents)
|
|
560
|
-
// These allow users to select "OpenCode", "Gemini", etc. and then pick a model/variant
|
|
561
|
-
const cliWrappers = [
|
|
562
|
-
{ id: 'cli-opencode', name: 'OpenCode', icon: 'O', protocol: 'cli-wrapper', acpId: 'opencode' },
|
|
563
|
-
{ id: 'cli-gemini', name: 'Gemini', icon: 'G', protocol: 'cli-wrapper', acpId: 'gemini' },
|
|
564
|
-
{ id: 'cli-kilo', name: 'Kilo', icon: 'K', protocol: 'cli-wrapper', acpId: 'kilo' },
|
|
565
|
-
{ id: 'cli-codex', name: 'Codex', icon: 'X', protocol: 'cli-wrapper', acpId: 'codex' },
|
|
566
|
-
];
|
|
567
|
-
|
|
568
|
-
// Only add CLI wrappers for agents that are already discovered
|
|
569
|
-
console.log('[discoverAgents] Found agents:', agents.map(a => a.id).join(', '));
|
|
570
|
-
for (const wrapper of cliWrappers) {
|
|
571
|
-
if (agents.some(a => a.id === wrapper.acpId)) {
|
|
572
|
-
console.log(`[discoverAgents] Adding CLI wrapper for ${wrapper.id}`);
|
|
573
|
-
agents.push(wrapper);
|
|
574
|
-
console.log(`[discoverAgents] After push, agents.length = ${agents.length}, includes ${wrapper.id}? ${agents.some(a => a.id === wrapper.id)}`);
|
|
575
|
-
} else {
|
|
576
|
-
console.log(`[discoverAgents] Skipping CLI wrapper ${wrapper.id} (ACP agent ${wrapper.acpId} not found)`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
// Remove raw ACP agents that have a cli-wrapper (avoid duplicates in UI)
|
|
580
|
-
const wrappedAcpIds = new Set(cliWrappers.filter(w => agents.some(a => a.id === w.acpId)).map(w => w.acpId));
|
|
581
|
-
const filtered = agents.filter(a => !wrappedAcpIds.has(a.id));
|
|
582
|
-
console.log('[discoverAgents] Final agent count:', filtered.length, 'Agent IDs:', filtered.map(a => a.id).join(', '));
|
|
583
|
-
|
|
584
|
-
return filtered;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Function to discover agents from external ACP servers
|
|
588
|
-
async function discoverExternalACPServers() {
|
|
589
|
-
const externalAgents = [];
|
|
590
|
-
for (const agent of discoveredAgents.filter(a => a.protocol === 'acp' && a.acpPort)) {
|
|
591
|
-
try {
|
|
592
|
-
const agents = await queryACPServerAgents(`http://localhost:${agent.acpPort}`);
|
|
593
|
-
externalAgents.push(...agents);
|
|
594
|
-
} catch (_) {}
|
|
595
|
-
}
|
|
596
|
-
return externalAgents;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
454
|
let discoveredAgents = [];
|
|
600
455
|
initializeDescriptors(discoveredAgents);
|
|
601
456
|
|
|
602
|
-
// Agent discovery happens asynchronously in background to not block startup
|
|
603
|
-
async function initializeAgentDiscovery() {
|
|
604
|
-
try {
|
|
605
|
-
const agents = discoverAgents();
|
|
606
|
-
// Mutate the existing array instead of reassigning to preserve closure references in handlers
|
|
607
|
-
discoveredAgents.length = 0;
|
|
608
|
-
discoveredAgents.push(...agents);
|
|
609
|
-
initializeDescriptors(discoveredAgents);
|
|
610
|
-
console.log('[AGENTS] Discovered:', discoveredAgents.map(a => ({ id: a.id, found: !!a.path, protocol: a.protocol })));
|
|
611
|
-
console.log('[AGENTS] Total count:', discoveredAgents.length);
|
|
612
|
-
} catch (err) {
|
|
613
|
-
console.error('[AGENTS] Discovery error:', err.message);
|
|
614
|
-
logError('initializeAgentDiscovery', err);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Start immediately but don't wait for it
|
|
619
457
|
const startTime = Date.now();
|
|
620
|
-
initializeAgentDiscovery().then(() => {
|
|
458
|
+
initializeAgentDiscovery(discoveredAgents, rootDir, logError).then(() => {
|
|
459
|
+
initializeDescriptors(discoveredAgents);
|
|
621
460
|
console.log('[INIT] initializeAgentDiscovery completed in', Date.now() - startTime, 'ms');
|
|
622
461
|
}).catch(() => {});
|
|
623
462
|
|
|
@@ -666,7 +505,7 @@ function getGeminiOAuthCreds() {
|
|
|
666
505
|
}
|
|
667
506
|
const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
|
|
668
507
|
try {
|
|
669
|
-
const geminiPath = findCommand('gemini');
|
|
508
|
+
const geminiPath = findCommand('gemini', rootDir);
|
|
670
509
|
if (geminiPath) {
|
|
671
510
|
const realPath = fs.realpathSync(geminiPath);
|
|
672
511
|
const pkgRoot = path.resolve(path.dirname(realPath), '..');
|
|
@@ -1364,6 +1203,9 @@ function sendJSON(req, res, statusCode, data) {
|
|
|
1364
1203
|
compressAndSend(req, res, statusCode, 'application/json', JSON.stringify(data));
|
|
1365
1204
|
}
|
|
1366
1205
|
|
|
1206
|
+
const _rateLimitMap = new LRUCache({ max: 1000, ttl: 60000 });
|
|
1207
|
+
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '300', 10);
|
|
1208
|
+
|
|
1367
1209
|
const server = http.createServer(async (req, res) => {
|
|
1368
1210
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1369
1211
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
@@ -1371,6 +1213,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
1371
1213
|
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
|
1372
1214
|
if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return;
|
|
1373
1215
|
|
|
1216
|
+
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;
|
|
1217
|
+
const hits = (_rateLimitMap.get(clientIp) || 0) + 1;
|
|
1218
|
+
_rateLimitMap.set(clientIp, hits);
|
|
1219
|
+
res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
|
|
1220
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX - hits));
|
|
1221
|
+
if (hits > RATE_LIMIT_MAX) {
|
|
1222
|
+
res.writeHead(429, { 'Retry-After': '60' });
|
|
1223
|
+
res.end('Too Many Requests');
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1374
1227
|
const _pwd = process.env.PASSWORD;
|
|
1375
1228
|
if (_pwd) {
|
|
1376
1229
|
const _auth = req.headers['authorization'] || '';
|
|
@@ -1437,6 +1290,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
1437
1290
|
return;
|
|
1438
1291
|
}
|
|
1439
1292
|
|
|
1293
|
+
if (pathOnly === '/api/health' && req.method === 'GET') {
|
|
1294
|
+
sendJSON(req, res, 200, {
|
|
1295
|
+
status: 'ok',
|
|
1296
|
+
version: PKG_VERSION,
|
|
1297
|
+
uptime: process.uptime(),
|
|
1298
|
+
agents: discoveredAgents.length,
|
|
1299
|
+
activeExecutions: activeExecutions.size,
|
|
1300
|
+
wsClients: wss.clients.size,
|
|
1301
|
+
memory: process.memoryUsage(),
|
|
1302
|
+
acp: getACPStatus()
|
|
1303
|
+
});
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1440
1307
|
if (pathOnly === '/api/conversations' && req.method === 'GET') {
|
|
1441
1308
|
const conversations = queries.getConversationsList();
|
|
1442
1309
|
// Filter out stale streaming state using a single bulk query instead of N+1 per-conversation queries
|
|
@@ -2426,7 +2293,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2426
2293
|
const localResult = queries.searchAgents(discoveredAgents, body);
|
|
2427
2294
|
|
|
2428
2295
|
// Get external agents from ACP servers
|
|
2429
|
-
const externalAgents = await discoverExternalACPServers();
|
|
2296
|
+
const externalAgents = await discoverExternalACPServers(discoveredAgents);
|
|
2430
2297
|
const externalResult = queries.searchAgents(externalAgents, body);
|
|
2431
2298
|
|
|
2432
2299
|
// Combine results
|
package/static/app.js
CHANGED
|
@@ -749,4 +749,82 @@ function downloadScreenshot() {
|
|
|
749
749
|
console.log('Download screenshot not yet implemented');
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
+
function showShortcutsOverlay() {
|
|
753
|
+
if (document.querySelector('.shortcuts-overlay')) return;
|
|
754
|
+
const overlay = document.createElement('div');
|
|
755
|
+
overlay.className = 'shortcuts-overlay';
|
|
756
|
+
overlay.innerHTML = `<div class="shortcuts-panel">
|
|
757
|
+
<h3>Keyboard Shortcuts</h3>
|
|
758
|
+
<table>
|
|
759
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>N</kbd></td><td>New conversation</td></tr>
|
|
760
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>B</kbd></td><td>Toggle sidebar</td></tr>
|
|
761
|
+
<tr><td><kbd>Ctrl</kbd>+<kbd>Enter</kbd></td><td>Send message</td></tr>
|
|
762
|
+
<tr><td><kbd>Escape</kbd></td><td>Cancel stream / blur input</td></tr>
|
|
763
|
+
<tr><td><kbd>?</kbd></td><td>Show this help</td></tr>
|
|
764
|
+
</table>
|
|
765
|
+
<p style="margin:1rem 0 0;font-size:0.75rem;color:#9ca3af;">Press Escape to close</p>
|
|
766
|
+
</div>`;
|
|
767
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
768
|
+
document.body.appendChild(overlay);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
document.addEventListener('keydown', (e) => {
|
|
772
|
+
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
|
|
773
|
+
const tag = document.activeElement?.tagName;
|
|
774
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
showShortcutsOverlay();
|
|
777
|
+
}
|
|
778
|
+
if (e.key === 'Escape' && document.querySelector('.shortcuts-overlay')) {
|
|
779
|
+
document.querySelector('.shortcuts-overlay').remove();
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
(function initSidebarSearch() {
|
|
784
|
+
const input = document.getElementById('sidebarSearchInput');
|
|
785
|
+
if (!input) return;
|
|
786
|
+
let debounceTimer;
|
|
787
|
+
input.addEventListener('input', () => {
|
|
788
|
+
clearTimeout(debounceTimer);
|
|
789
|
+
debounceTimer = setTimeout(() => {
|
|
790
|
+
const query = input.value.trim();
|
|
791
|
+
const list = document.querySelector('[data-conversation-list]');
|
|
792
|
+
if (!list) return;
|
|
793
|
+
const items = list.querySelectorAll('li[data-conv-id]');
|
|
794
|
+
if (!query) {
|
|
795
|
+
items.forEach(li => li.style.display = '');
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const lower = query.toLowerCase();
|
|
799
|
+
items.forEach(li => {
|
|
800
|
+
const text = li.textContent.toLowerCase();
|
|
801
|
+
li.style.display = text.includes(lower) ? '' : 'none';
|
|
802
|
+
});
|
|
803
|
+
}, 150);
|
|
804
|
+
});
|
|
805
|
+
})();
|
|
806
|
+
|
|
807
|
+
(function initErrorBoundary() {
|
|
808
|
+
let toastEl = null;
|
|
809
|
+
let hideTimer = null;
|
|
810
|
+
function showErrorToast(title, detail) {
|
|
811
|
+
if (toastEl) toastEl.remove();
|
|
812
|
+
toastEl = document.createElement('div');
|
|
813
|
+
toastEl.className = 'error-toast';
|
|
814
|
+
toastEl.innerHTML = `<div class="error-toast-title">${title}</div><div class="error-toast-detail">${detail}</div>`;
|
|
815
|
+
toastEl.addEventListener('click', () => { toastEl.remove(); toastEl = null; });
|
|
816
|
+
document.body.appendChild(toastEl);
|
|
817
|
+
clearTimeout(hideTimer);
|
|
818
|
+
hideTimer = setTimeout(() => { if (toastEl) { toastEl.remove(); toastEl = null; } }, 8000);
|
|
819
|
+
}
|
|
820
|
+
window.addEventListener('error', (e) => {
|
|
821
|
+
if (e.filename) showErrorToast('JavaScript Error', `${e.message} at ${e.filename.split('/').pop()}:${e.lineno}`);
|
|
822
|
+
});
|
|
823
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
824
|
+
const msg = e.reason?.message || String(e.reason || 'Unknown');
|
|
825
|
+
showErrorToast('Unhandled Promise Rejection', msg);
|
|
826
|
+
});
|
|
827
|
+
window.showErrorToast = showErrorToast;
|
|
828
|
+
})();
|
|
829
|
+
|
|
752
830
|
window.addEventListener('load', initializeApp);
|
package/static/css/main.css
CHANGED
|
@@ -3054,4 +3054,108 @@
|
|
|
3054
3054
|
html.dark .html-rendered-container {
|
|
3055
3055
|
border-color: #262626;
|
|
3056
3056
|
}
|
|
3057
|
+
|
|
3058
|
+
.sidebar-search-bar {
|
|
3059
|
+
padding: 0.25rem 0.75rem 0.5rem;
|
|
3060
|
+
}
|
|
3061
|
+
.sidebar-search-input {
|
|
3062
|
+
width: 100%;
|
|
3063
|
+
padding: 0.375rem 0.5rem;
|
|
3064
|
+
border: 1px solid #d1d5db;
|
|
3065
|
+
border-radius: 0.375rem;
|
|
3066
|
+
background: #f9fafb;
|
|
3067
|
+
color: #111827;
|
|
3068
|
+
font-size: 0.8125rem;
|
|
3069
|
+
outline: none;
|
|
3070
|
+
box-sizing: border-box;
|
|
3071
|
+
}
|
|
3072
|
+
.sidebar-search-input:focus {
|
|
3073
|
+
border-color: #3b82f6;
|
|
3074
|
+
box-shadow: 0 0 0 2px rgba(59,130,246,0.15);
|
|
3075
|
+
}
|
|
3076
|
+
html.dark .sidebar-search-input {
|
|
3077
|
+
background: #1f2937;
|
|
3078
|
+
border-color: #374151;
|
|
3079
|
+
color: #e5e7eb;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
.shortcuts-overlay {
|
|
3083
|
+
position: fixed;
|
|
3084
|
+
inset: 0;
|
|
3085
|
+
background: rgba(0,0,0,0.5);
|
|
3086
|
+
display: flex;
|
|
3087
|
+
align-items: center;
|
|
3088
|
+
justify-content: center;
|
|
3089
|
+
z-index: 9999;
|
|
3090
|
+
}
|
|
3091
|
+
.shortcuts-panel {
|
|
3092
|
+
background: white;
|
|
3093
|
+
border-radius: 0.75rem;
|
|
3094
|
+
padding: 1.5rem;
|
|
3095
|
+
max-width: 400px;
|
|
3096
|
+
width: 90%;
|
|
3097
|
+
box-shadow: 0 25px 50px rgba(0,0,0,0.25);
|
|
3098
|
+
}
|
|
3099
|
+
html.dark .shortcuts-panel {
|
|
3100
|
+
background: #1f2937;
|
|
3101
|
+
color: #e5e7eb;
|
|
3102
|
+
}
|
|
3103
|
+
.shortcuts-panel h3 {
|
|
3104
|
+
margin: 0 0 1rem;
|
|
3105
|
+
font-size: 1.125rem;
|
|
3106
|
+
}
|
|
3107
|
+
.shortcuts-panel table {
|
|
3108
|
+
width: 100%;
|
|
3109
|
+
border-collapse: collapse;
|
|
3110
|
+
}
|
|
3111
|
+
.shortcuts-panel td {
|
|
3112
|
+
padding: 0.375rem 0;
|
|
3113
|
+
font-size: 0.875rem;
|
|
3114
|
+
}
|
|
3115
|
+
.shortcuts-panel td:first-child {
|
|
3116
|
+
font-weight: 600;
|
|
3117
|
+
white-space: nowrap;
|
|
3118
|
+
padding-right: 1rem;
|
|
3119
|
+
}
|
|
3120
|
+
.shortcuts-panel kbd {
|
|
3121
|
+
background: #f3f4f6;
|
|
3122
|
+
border: 1px solid #d1d5db;
|
|
3123
|
+
border-radius: 0.25rem;
|
|
3124
|
+
padding: 0.125rem 0.375rem;
|
|
3125
|
+
font-size: 0.75rem;
|
|
3126
|
+
font-family: inherit;
|
|
3127
|
+
}
|
|
3128
|
+
html.dark .shortcuts-panel kbd {
|
|
3129
|
+
background: #374151;
|
|
3130
|
+
border-color: #4b5563;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
.error-toast {
|
|
3134
|
+
position: fixed;
|
|
3135
|
+
bottom: 1rem;
|
|
3136
|
+
right: 1rem;
|
|
3137
|
+
background: #dc2626;
|
|
3138
|
+
color: white;
|
|
3139
|
+
padding: 0.75rem 1rem;
|
|
3140
|
+
border-radius: 0.5rem;
|
|
3141
|
+
font-size: 0.875rem;
|
|
3142
|
+
max-width: 400px;
|
|
3143
|
+
z-index: 9998;
|
|
3144
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
|
3145
|
+
cursor: pointer;
|
|
3146
|
+
animation: slideInRight 0.2s ease-out;
|
|
3147
|
+
}
|
|
3148
|
+
.error-toast-title {
|
|
3149
|
+
font-weight: 600;
|
|
3150
|
+
margin-bottom: 0.25rem;
|
|
3151
|
+
}
|
|
3152
|
+
.error-toast-detail {
|
|
3153
|
+
opacity: 0.85;
|
|
3154
|
+
font-size: 0.8125rem;
|
|
3155
|
+
word-break: break-word;
|
|
3156
|
+
}
|
|
3157
|
+
@keyframes slideInRight {
|
|
3158
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
3159
|
+
to { transform: translateX(0); opacity: 1; }
|
|
3160
|
+
}
|
|
3057
3161
|
|
package/static/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
})();
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
<link rel="stylesheet" href="/gm/css/main.css">
|
|
24
|
+
<link rel="stylesheet" href="/gm/css/main.css">
|
|
25
25
|
<link rel="stylesheet" href="/gm/css/tools-popup.css">
|
|
26
26
|
</head>
|
|
27
27
|
<body>
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
<button id="newConversationBtn" class="sidebar-new-btn" data-new-conversation title="Start new conversation">+ New</button>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
|
+
<div class="sidebar-search-bar" id="sidebarSearchBar">
|
|
43
|
+
<input type="text" class="sidebar-search-input" id="sidebarSearchInput" placeholder="Search conversations..." autocomplete="off" spellcheck="false">
|
|
44
|
+
</div>
|
|
42
45
|
<div class="clone-input-bar" id="cloneInputBar" style="display:none;">
|
|
43
46
|
<input type="text" class="clone-input" id="cloneRepoInput" placeholder="org/repo" autocomplete="off" spellcheck="false">
|
|
44
47
|
<button class="clone-go-btn" id="cloneGoBtn" title="Clone">Go</button>
|