agentgui 1.0.715 → 1.0.717

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.
@@ -0,0 +1,119 @@
1
+ import { z } from 'zod';
2
+ import * as execMachine from './execution-machine.js';
3
+
4
+ function fail(code, message) { const e = new Error(message); e.code = code; throw e; }
5
+ function notFound(msg = 'Not found') { fail(404, msg); }
6
+ function validate(schema, params) {
7
+ const result = schema.safeParse(params);
8
+ if (!result.success) fail(400, result.error.issues.map(i => i.message).join('; '));
9
+ return result.data;
10
+ }
11
+
12
+ const MsgStreamSchema = z.object({
13
+ id: z.string().min(1, 'conversation id required'),
14
+ content: z.union([z.string(), z.any()]).optional(),
15
+ message: z.union([z.string(), z.any()]).optional(),
16
+ agentId: z.string().optional(),
17
+ model: z.string().optional(),
18
+ subAgent: z.string().optional(),
19
+ }).passthrough();
20
+
21
+ export function register(router, deps) {
22
+ const { queries, activeExecutions, messageQueues,
23
+ broadcastSync, processMessageWithStreaming, logError = () => {} } = deps;
24
+
25
+ const queueSeqByConv = new Map();
26
+ function getNextQueueSeq(conversationId) {
27
+ const current = queueSeqByConv.get(conversationId) || 0;
28
+ const next = current + 1;
29
+ queueSeqByConv.set(conversationId, next);
30
+ return next;
31
+ }
32
+
33
+ function startExecution(convId, message, agentId, model, content, subAgent) {
34
+ const session = queries.createSession(convId);
35
+ queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, convId, session.id);
36
+ execMachine.send(convId, { type: 'START', sessionId: session.id });
37
+ activeExecutions.set(convId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
38
+ queries.setIsStreaming(convId, true);
39
+ broadcastSync({ type: 'streaming_start', sessionId: session.id, conversationId: convId, messageId: message.id, agentId, timestamp: Date.now() });
40
+ processMessageWithStreaming(convId, message.id, session.id, content, agentId, model, subAgent).catch(e => logError('startExecution', e, { convId }));
41
+ return session;
42
+ }
43
+
44
+ function enqueue(convId, content, agentId, model, messageId, subAgent) {
45
+ const item = { content, agentId, model, messageId, subAgent };
46
+ execMachine.send(convId, { type: 'ENQUEUE', item });
47
+ if (!messageQueues.has(convId)) messageQueues.set(convId, []);
48
+ messageQueues.get(convId).push(item);
49
+ const queueLength = execMachine.getQueue(convId).length;
50
+ broadcastSync({ type: 'queue_status', conversationId: convId, queueLength, messageId, timestamp: Date.now() });
51
+ return queueLength;
52
+ }
53
+
54
+ function normalizeContent(rawContent, method) {
55
+ if (rawContent !== undefined && typeof rawContent !== 'string') {
56
+ console.warn(`[ws-validation] ${method} content is ${typeof rawContent}: ${JSON.stringify(rawContent).slice(0, 200)}`);
57
+ logError('ws-content-type', new TypeError(`non-string content in ${method}`), { method, type: typeof rawContent });
58
+ }
59
+ return typeof rawContent === 'string' ? rawContent : (rawContent ? JSON.stringify(rawContent) : '');
60
+ }
61
+
62
+ router.handle('msg.ls', (p) => {
63
+ return queries.getPaginatedMessages(p.id, Math.min(p.limit || 50, 100), Math.max(p.offset || 0, 0));
64
+ });
65
+
66
+ router.handle('msg.ls.earlier', (p) => {
67
+ if (!p.id) fail(400, 'Missing conversation id');
68
+ if (!p.before) fail(400, 'Missing before messageId parameter');
69
+ const limit = Math.min(p.limit || 50, 100);
70
+ const result = queries.getMessagesBefore(p.id, p.before, limit);
71
+ return { ok: true, messages: result.messages, total: result.total, hasMore: result.hasMore, limit: result.limit };
72
+ });
73
+
74
+ router.handle('msg.get', (p) => {
75
+ const msg = queries.getMessage(p.messageId);
76
+ if (!msg || msg.conversationId !== p.id) notFound();
77
+ return { message: msg };
78
+ });
79
+
80
+ router.handle('msg.send', (p) => {
81
+ const conv = queries.getConversation(p.id);
82
+ if (!conv) notFound('Conversation not found');
83
+ const agentId = p.agentId || conv.agentType || conv.agentId || 'claude-code';
84
+ const model = p.model || conv.model || null;
85
+ const subAgent = p.subAgent || conv.subAgent || null;
86
+ const idempotencyKey = p.idempotencyKey || null;
87
+ p.content = normalizeContent(p.content, 'msg.send');
88
+ const message = queries.createMessage(p.id, 'user', p.content, idempotencyKey);
89
+ queries.createEvent('message.created', { role: 'user', messageId: message.id }, p.id);
90
+ if (!execMachine.isActive(p.id)) {
91
+ broadcastSync({ type: 'message_created', conversationId: p.id, message, timestamp: Date.now() });
92
+ const session = startExecution(p.id, message, agentId, model, p.content, subAgent);
93
+ return { message, session, idempotencyKey };
94
+ }
95
+ const qp = enqueue(p.id, p.content, agentId, model, message.id, subAgent);
96
+ return { message, queued: true, queuePosition: qp, idempotencyKey };
97
+ });
98
+
99
+ router.handle('msg.stream', (p) => {
100
+ p = validate(MsgStreamSchema, p);
101
+ const conv = queries.getConversation(p.id);
102
+ if (!conv) notFound('Conversation not found');
103
+ const prompt = normalizeContent(p.content || p.message, 'msg.stream');
104
+ const agentId = p.agentId || conv.agentType || conv.agentId || 'claude-code';
105
+ const model = p.model || conv.model || null;
106
+ const subAgent = p.subAgent || conv.subAgent || null;
107
+ const userMessage = queries.createMessage(p.id, 'user', prompt);
108
+ queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, p.id);
109
+ if (!execMachine.isActive(p.id)) {
110
+ broadcastSync({ type: 'message_created', conversationId: p.id, message: userMessage, timestamp: Date.now() });
111
+ const session = startExecution(p.id, userMessage, agentId, model, prompt, subAgent);
112
+ return { message: userMessage, session, streamId: session.id };
113
+ }
114
+ const qp = enqueue(p.id, prompt, agentId, model, userMessage.id, subAgent);
115
+ const seq = getNextQueueSeq(p.id);
116
+ broadcastSync({ type: 'queue_status', conversationId: p.id, queueLength: execMachine.getQueue(p.id).length, seq, timestamp: Date.now() });
117
+ return { message: userMessage, queued: true, queuePosition: qp };
118
+ });
119
+ }
@@ -0,0 +1,76 @@
1
+ function err(code, message) { const e = new Error(message); e.code = code; throw e; }
2
+
3
+ export function register(router, deps) {
4
+ const { startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
5
+ startCodexOAuth, exchangeCodexOAuthCode, codexOAuthState } = deps;
6
+
7
+ router.handle('gemini.start', async () => {
8
+ try {
9
+ const result = await startGeminiOAuth();
10
+ return { authUrl: result.authUrl, mode: result.mode };
11
+ } catch (e) { err(500, e.message); }
12
+ });
13
+
14
+ router.handle('gemini.status', () => {
15
+ return typeof geminiOAuthState === 'function' ? geminiOAuthState() : geminiOAuthState;
16
+ });
17
+
18
+ router.handle('gemini.relay', async (p) => {
19
+ const { code, state } = p;
20
+ if (!code || !state) err(400, 'Missing code or state');
21
+ try {
22
+ const email = await exchangeGeminiOAuthCode(code, state);
23
+ return { success: true, email };
24
+ } catch (e) { err(400, e.message); }
25
+ });
26
+
27
+ router.handle('gemini.complete', async (p) => {
28
+ const pastedUrl = (p.url || '').trim();
29
+ if (!pastedUrl) err(400, 'No URL provided');
30
+ let parsed;
31
+ try { parsed = new URL(pastedUrl); } catch { err(400, 'Invalid URL. Paste the full URL from the browser address bar.'); }
32
+ const urlError = parsed.searchParams.get('error');
33
+ if (urlError) return { error: parsed.searchParams.get('error_description') || urlError };
34
+ const code = parsed.searchParams.get('code');
35
+ const state = parsed.searchParams.get('state');
36
+ try {
37
+ const email = await exchangeGeminiOAuthCode(code, state);
38
+ return { success: true, email };
39
+ } catch (e) { err(400, e.message); }
40
+ });
41
+
42
+ router.handle('codex.start', async () => {
43
+ try {
44
+ const result = await startCodexOAuth();
45
+ return { authUrl: result.authUrl, mode: result.mode };
46
+ } catch (e) { err(500, e.message); }
47
+ });
48
+
49
+ router.handle('codex.status', () => {
50
+ return typeof codexOAuthState === 'function' ? codexOAuthState() : codexOAuthState;
51
+ });
52
+
53
+ router.handle('codex.relay', async (p) => {
54
+ const { code, state } = p;
55
+ if (!code || !state) err(400, 'Missing code or state');
56
+ try {
57
+ const email = await exchangeCodexOAuthCode(code, state);
58
+ return { success: true, email };
59
+ } catch (e) { err(400, e.message); }
60
+ });
61
+
62
+ router.handle('codex.complete', async (p) => {
63
+ const pastedUrl = (p.url || '').trim();
64
+ if (!pastedUrl) err(400, 'No URL provided');
65
+ let parsed;
66
+ try { parsed = new URL(pastedUrl); } catch { err(400, 'Invalid URL. Paste the full URL from the browser address bar.'); }
67
+ const urlError = parsed.searchParams.get('error');
68
+ if (urlError) return { error: parsed.searchParams.get('error_description') || urlError };
69
+ const code = parsed.searchParams.get('code');
70
+ const state = parsed.searchParams.get('state');
71
+ try {
72
+ const email = await exchangeCodexOAuthCode(code, state);
73
+ return { success: true, email };
74
+ } catch (e) { err(400, e.message); }
75
+ });
76
+ }
@@ -0,0 +1,56 @@
1
+ import * as execMachine from './execution-machine.js';
2
+
3
+ function fail(code, message) { const e = new Error(message); e.code = code; throw e; }
4
+ function notFound(msg = 'Not found') { fail(404, msg); }
5
+
6
+ export function register(router, deps) {
7
+ const { queries, messageQueues, broadcastSync } = deps;
8
+
9
+ const queueSeqByConv = new Map();
10
+ function getNextQueueSeq(conversationId) {
11
+ const current = queueSeqByConv.get(conversationId) || 0;
12
+ const next = current + 1;
13
+ queueSeqByConv.set(conversationId, next);
14
+ return next;
15
+ }
16
+
17
+ router.handle('q.ls', (p) => {
18
+ if (!queries.getConversation(p.id)) notFound('Conversation not found');
19
+ const machineQueue = execMachine.getQueue(p.id);
20
+ return { queue: machineQueue.length > 0 ? machineQueue : (messageQueues.get(p.id) || []) };
21
+ });
22
+
23
+ router.handle('q.del', (p) => {
24
+ const machineQueue = execMachine.getQueue(p.id);
25
+ const mapQueue = messageQueues.get(p.id);
26
+ if (!machineQueue.length && !mapQueue) notFound('Queue not found');
27
+ const idx = machineQueue.findIndex(q => q.messageId === p.messageId);
28
+ if (idx === -1 && (!mapQueue || mapQueue.findIndex(q => q.messageId === p.messageId) === -1)) notFound('Queued message not found');
29
+ if (idx !== -1) {
30
+ const newQueue = [...machineQueue];
31
+ newQueue.splice(idx, 1);
32
+ execMachine.send(p.id, { type: 'SET_QUEUE', queue: newQueue });
33
+ }
34
+ if (mapQueue) {
35
+ const mi = mapQueue.findIndex(q => q.messageId === p.messageId);
36
+ if (mi !== -1) mapQueue.splice(mi, 1);
37
+ if (mapQueue.length === 0) messageQueues.delete(p.id);
38
+ }
39
+ const seq = getNextQueueSeq(p.id);
40
+ broadcastSync({ type: 'queue_status', conversationId: p.id, queueLength: execMachine.getQueue(p.id).length, seq, timestamp: Date.now() });
41
+ return { deleted: true };
42
+ });
43
+
44
+ router.handle('q.upd', (p) => {
45
+ const machineQueue = execMachine.getQueue(p.id);
46
+ const mapQueue = messageQueues.get(p.id);
47
+ if (!machineQueue.length && !mapQueue) notFound('Queue not found');
48
+ const item = machineQueue.find(q => q.messageId === p.messageId) || mapQueue?.find(q => q.messageId === p.messageId);
49
+ if (!item) notFound('Queued message not found');
50
+ if (p.content !== undefined) item.content = p.content;
51
+ if (p.agentId !== undefined) item.agentId = p.agentId;
52
+ const seq = getNextQueueSeq(p.id);
53
+ broadcastSync({ type: 'queue_updated', conversationId: p.id, messageId: p.messageId, content: item.content, agentId: item.agentId, seq, timestamp: Date.now() });
54
+ return { updated: true, item };
55
+ });
56
+ }
@@ -0,0 +1,58 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { spawn } from 'child_process';
5
+
6
+ function err(code, message) { const e = new Error(message); e.code = code; throw e; }
7
+
8
+ export function register(router, deps) {
9
+ const { queries, broadcastSync, STARTUP_CWD, activeScripts } = deps;
10
+
11
+ router.handle('conv.scripts', (p) => {
12
+ const conv = queries.getConversation(p.id);
13
+ if (!conv) err(404, 'Not found');
14
+ const wd = conv.workingDirectory || STARTUP_CWD;
15
+ let hasStart = false, hasDev = false;
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
18
+ const scripts = pkg.scripts || {};
19
+ hasStart = !!scripts.start;
20
+ hasDev = !!scripts.dev;
21
+ } catch {}
22
+ const running = activeScripts.has(p.id);
23
+ const runningScript = running ? activeScripts.get(p.id).script : null;
24
+ return { hasStart, hasDev, running, runningScript };
25
+ });
26
+
27
+ router.handle('conv.run-script', (p) => {
28
+ const conv = queries.getConversation(p.id);
29
+ if (!conv) err(404, 'Not found');
30
+ if (activeScripts.has(p.id)) err(409, 'Script already running');
31
+ const script = p.script;
32
+ if (script !== 'start' && script !== 'dev') err(400, 'Invalid script');
33
+ const wd = conv.workingDirectory || STARTUP_CWD;
34
+ try {
35
+ const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
36
+ if (!pkg.scripts || !pkg.scripts[script]) err(400, `Script "${script}" not found`);
37
+ } catch (e) { if (e.code) throw e; err(400, 'No package.json'); }
38
+ const childEnv = { ...process.env, FORCE_COLOR: '1' };
39
+ delete childEnv.PORT; delete childEnv.BASE_URL; delete childEnv.HOT_RELOAD;
40
+ const isWindows = os.platform() === 'win32';
41
+ const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
42
+ activeScripts.set(p.id, { process: child, script, startTime: Date.now() });
43
+ broadcastSync({ type: 'script_started', conversationId: p.id, script, timestamp: Date.now() });
44
+ const onData = (stream) => (chunk) => broadcastSync({ type: 'script_output', conversationId: p.id, data: chunk.toString(), stream, timestamp: Date.now() });
45
+ child.stdout.on('data', onData('stdout'));
46
+ child.stderr.on('data', onData('stderr'));
47
+ child.on('error', (e) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: 1, error: e.message, timestamp: Date.now() }); });
48
+ child.on('close', (code) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: code || 0, timestamp: Date.now() }); });
49
+ return { ok: true, script, pid: child.pid };
50
+ });
51
+
52
+ router.handle('conv.stop-script', (p) => {
53
+ const entry = activeScripts.get(p.id);
54
+ if (!entry) err(404, 'No running script');
55
+ try { process.kill(-entry.process.pid, 'SIGTERM'); } catch { try { entry.process.kill('SIGTERM'); } catch {} }
56
+ return { ok: true };
57
+ });
58
+ }
@@ -1,33 +1,23 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { execSync, spawn, spawnSync } from 'child_process';
4
+ import { execSync, spawnSync } from 'child_process';
5
5
 
6
6
  function err(code, message) { const e = new Error(message); e.code = code; throw e; }
7
7
 
8
8
  export function register(router, deps) {
9
9
  const { queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
10
10
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
11
- startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
12
- startCodexOAuth, exchangeCodexOAuthCode, codexOAuthState,
13
- STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents } = deps;
11
+ STARTUP_CWD, voiceCacheManager, toolManager, discoveredAgents } = deps;
14
12
 
15
13
  router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
16
14
 
17
- // NOTE: agent.ls is handled by ws-handlers-session.js to ensure proper agent mapping
18
- // Do not re-register here as it will override the session handler
19
-
20
15
  router.handle('folders', (p) => {
21
16
  const folderPath = p.path || STARTUP_CWD;
22
17
  try {
23
18
  const raw = folderPath.startsWith('~') ? folderPath.replace('~', os.homedir()) : folderPath;
24
- const expanded = path.resolve(raw);
25
- const entries = fs.readdirSync(expanded, { withFileTypes: true });
26
- const folders = entries
27
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
28
- .map(e => ({ name: e.name }))
29
- .sort((a, b) => a.name.localeCompare(b.name));
30
- return { folders };
19
+ const entries = fs.readdirSync(path.resolve(raw), { withFileTypes: true });
20
+ return { folders: entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => ({ name: e.name })).sort((a, b) => a.name.localeCompare(b.name)) };
31
21
  } catch (e) { err(400, e.message); }
32
22
  });
33
23
 
@@ -80,25 +70,13 @@ export function register(router, deps) {
80
70
  });
81
71
 
82
72
  router.handle('speech.status', async () => {
73
+ const modelState = { modelsDownloading: modelDownloadState.downloading, modelsComplete: modelDownloadState.complete, modelsError: modelDownloadState.error };
83
74
  try {
84
75
  const { getStatus } = await getSpeech();
85
76
  const base = getStatus();
86
- return {
87
- ...base,
88
- setupMessage: base.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request',
89
- modelsDownloading: modelDownloadState.downloading,
90
- modelsComplete: modelDownloadState.complete,
91
- modelsError: modelDownloadState.error,
92
- modelsProgress: modelDownloadState.progress,
93
- };
77
+ return { ...base, setupMessage: base.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request', ...modelState, modelsProgress: modelDownloadState.progress };
94
78
  } catch {
95
- return {
96
- sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false,
97
- setupMessage: 'Will setup on first TTS request',
98
- modelsDownloading: modelDownloadState.downloading,
99
- modelsComplete: modelDownloadState.complete,
100
- modelsError: modelDownloadState.error,
101
- };
79
+ return { sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false, setupMessage: 'Will setup on first TTS request', ...modelState };
102
80
  }
103
81
  });
104
82
 
@@ -137,134 +115,8 @@ export function register(router, deps) {
137
115
 
138
116
  router.handle('discover.claude', () => ({ discovered: queries.discoverClaudeCodeConversations() }));
139
117
 
140
- router.handle('gemini.start', async () => {
141
- try {
142
- const result = await startGeminiOAuth();
143
- return { authUrl: result.authUrl, mode: result.mode };
144
- } catch (e) { err(500, e.message); }
145
- });
146
-
147
- router.handle('gemini.status', () => {
148
- const st = typeof geminiOAuthState === 'function' ? geminiOAuthState() : geminiOAuthState;
149
- return st;
150
- });
151
-
152
- router.handle('gemini.relay', async (p) => {
153
- const { code, state } = p;
154
- if (!code || !state) err(400, 'Missing code or state');
155
- try {
156
- const email = await exchangeGeminiOAuthCode(code, state);
157
- return { success: true, email };
158
- } catch (e) { err(400, e.message); }
159
- });
160
-
161
- router.handle('gemini.complete', async (p) => {
162
- const pastedUrl = (p.url || '').trim();
163
- if (!pastedUrl) err(400, 'No URL provided');
164
- let parsed;
165
- try { parsed = new URL(pastedUrl); } catch { err(400, 'Invalid URL. Paste the full URL from the browser address bar.'); }
166
- const urlError = parsed.searchParams.get('error');
167
- if (urlError) {
168
- const desc = parsed.searchParams.get('error_description') || urlError;
169
- return { error: desc };
170
- }
171
- const code = parsed.searchParams.get('code');
172
- const state = parsed.searchParams.get('state');
173
- try {
174
- const email = await exchangeGeminiOAuthCode(code, state);
175
- return { success: true, email };
176
- } catch (e) { err(400, e.message); }
177
- });
178
-
179
- router.handle('codex.start', async () => {
180
- try {
181
- const result = await startCodexOAuth();
182
- return { authUrl: result.authUrl, mode: result.mode };
183
- } catch (e) { err(500, e.message); }
184
- });
185
-
186
- router.handle('codex.status', () => {
187
- const st = typeof codexOAuthState === 'function' ? codexOAuthState() : codexOAuthState;
188
- return st;
189
- });
190
-
191
- router.handle('codex.relay', async (p) => {
192
- const { code, state } = p;
193
- if (!code || !state) err(400, 'Missing code or state');
194
- try {
195
- const email = await exchangeCodexOAuthCode(code, state);
196
- return { success: true, email };
197
- } catch (e) { err(400, e.message); }
198
- });
199
-
200
- router.handle('codex.complete', async (p) => {
201
- const pastedUrl = (p.url || '').trim();
202
- if (!pastedUrl) err(400, 'No URL provided');
203
- let parsed;
204
- try { parsed = new URL(pastedUrl); } catch { err(400, 'Invalid URL. Paste the full URL from the browser address bar.'); }
205
- const urlError = parsed.searchParams.get('error');
206
- if (urlError) {
207
- const desc = parsed.searchParams.get('error_description') || urlError;
208
- return { error: desc };
209
- }
210
- const code = parsed.searchParams.get('code');
211
- const state = parsed.searchParams.get('state');
212
- try {
213
- const email = await exchangeCodexOAuthCode(code, state);
214
- return { success: true, email };
215
- } catch (e) { err(400, e.message); }
216
- });
217
-
218
118
  router.handle('ws.stats', () => wsOptimizer.getStats());
219
119
 
220
- router.handle('conv.scripts', (p) => {
221
- const conv = queries.getConversation(p.id);
222
- if (!conv) err(404, 'Not found');
223
- const wd = conv.workingDirectory || STARTUP_CWD;
224
- let hasStart = false, hasDev = false;
225
- try {
226
- const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
227
- const scripts = pkg.scripts || {};
228
- hasStart = !!scripts.start;
229
- hasDev = !!scripts.dev;
230
- } catch {}
231
- const running = activeScripts.has(p.id);
232
- const runningScript = running ? activeScripts.get(p.id).script : null;
233
- return { hasStart, hasDev, running, runningScript };
234
- });
235
-
236
- router.handle('conv.run-script', (p) => {
237
- const conv = queries.getConversation(p.id);
238
- if (!conv) err(404, 'Not found');
239
- if (activeScripts.has(p.id)) err(409, 'Script already running');
240
- const script = p.script;
241
- if (script !== 'start' && script !== 'dev') err(400, 'Invalid script');
242
- const wd = conv.workingDirectory || STARTUP_CWD;
243
- try {
244
- const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
245
- if (!pkg.scripts || !pkg.scripts[script]) err(400, `Script "${script}" not found`);
246
- } catch (e) { if (e.code) throw e; err(400, 'No package.json'); }
247
- const childEnv = { ...process.env, FORCE_COLOR: '1' };
248
- delete childEnv.PORT; delete childEnv.BASE_URL; delete childEnv.HOT_RELOAD;
249
- const isWindows = os.platform() === 'win32';
250
- const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
251
- activeScripts.set(p.id, { process: child, script, startTime: Date.now() });
252
- broadcastSync({ type: 'script_started', conversationId: p.id, script, timestamp: Date.now() });
253
- const onData = (stream) => (chunk) => broadcastSync({ type: 'script_output', conversationId: p.id, data: chunk.toString(), stream, timestamp: Date.now() });
254
- child.stdout.on('data', onData('stdout'));
255
- child.stderr.on('data', onData('stderr'));
256
- child.on('error', (e) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: 1, error: e.message, timestamp: Date.now() }); });
257
- child.on('close', (code) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: code || 0, timestamp: Date.now() }); });
258
- return { ok: true, script, pid: child.pid };
259
- });
260
-
261
- router.handle('conv.stop-script', (p) => {
262
- const entry = activeScripts.get(p.id);
263
- if (!entry) err(404, 'No running script');
264
- try { process.kill(-entry.process.pid, 'SIGTERM'); } catch { try { entry.process.kill('SIGTERM'); } catch {} }
265
- return { ok: true };
266
- });
267
-
268
120
  router.handle('voice.cache', async (p) => {
269
121
  const { conversationId, text } = p;
270
122
  if (!conversationId || !text) err(400, 'Missing conversationId or text');
@@ -289,64 +141,28 @@ export function register(router, deps) {
289
141
  router.handle('tools.list', async () => {
290
142
  try {
291
143
  const tools = await toolManager.getAllToolsAsync();
292
- const result = tools.map((t) => ({
293
- id: t.id,
294
- name: t.name,
295
- pkg: t.pkg,
296
- category: t.category || 'plugin',
297
- installed: t.installed,
298
- status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
299
- isUpToDate: t.isUpToDate,
300
- upgradeNeeded: t.upgradeNeeded,
301
- hasUpdate: t.upgradeNeeded && t.installed,
302
- installedVersion: t.installedVersion,
303
- publishedVersion: t.publishedVersion
304
- }));
305
- return { tools: result };
306
- } catch (e) {
307
- err(500, e.message);
308
- }
144
+ return { tools: tools.map((t) => ({ id: t.id, name: t.name, pkg: t.pkg, category: t.category || 'plugin', installed: t.installed, status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed', isUpToDate: t.isUpToDate, upgradeNeeded: t.upgradeNeeded, hasUpdate: t.upgradeNeeded && t.installed, installedVersion: t.installedVersion, publishedVersion: t.publishedVersion })) };
145
+ } catch (e) { err(500, e.message); }
309
146
  });
310
147
 
311
- router.handle('agent.subagents', async (p) => {
312
- const { id } = p;
313
- if (!id) err(400, 'Missing agent id');
148
+ const SUB_AGENT_MAP = {
149
+ 'opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }], 'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
150
+ 'gemini': [{ id: 'gm-gc', name: 'GM Gemini' }], 'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
151
+ 'kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }], 'cli-kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
152
+ 'codex': [], 'cli-codex': []
153
+ };
314
154
 
315
- // Claude Code: run 'claude agents list' and parse output
316
- if (id === 'claude-code' || id === 'cli-claude') {
317
- const spawnEnv = { ...process.env };
318
- delete spawnEnv.CLAUDECODE;
319
- const result = spawnSync('claude', ['agents', 'list'], {
320
- encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
321
- env: spawnEnv
322
- });
155
+ router.handle('agent.subagents', async (p) => {
156
+ if (!p.id) err(400, 'Missing agent id');
157
+ if (p.id === 'claude-code' || p.id === 'cli-claude') {
158
+ const spawnEnv = { ...process.env }; delete spawnEnv.CLAUDECODE;
159
+ const result = spawnSync('claude', ['agents', 'list'], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], env: spawnEnv });
323
160
  if (result.status !== 0 || !result.stdout) return { subAgents: [] };
324
- const output = result.stdout.trim();
325
- // Output format: ' agentId · model' lines under section headers
326
- const agents = [];
327
- for (const line of output.split('\n').filter(l => l.trim())) {
328
- const match = line.match(/^ (\S+)\s+·/);
329
- if (match) {
330
- const id = match[1];
331
- agents.push({ id, name: id });
332
- }
333
- }
161
+ const agents = result.stdout.trim().split('\n').filter(l => l.trim()).map(l => l.match(/^ (\S+)\s+·/)).filter(Boolean).map(m => ({ id: m[1], name: m[1] }));
334
162
  console.log('[agent.subagents] claude agents list found:', agents.map(a => a.id).join(', '));
335
163
  return { subAgents: agents };
336
164
  }
337
-
338
- // ACP agents: hardcoded map filtered by installed tools
339
- const subAgentMap = {
340
- 'opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
341
- 'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
342
- 'gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
343
- 'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
344
- 'kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
345
- 'cli-kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
346
- 'codex': [],
347
- 'cli-codex': []
348
- };
349
- const subAgents = subAgentMap[id] || [];
165
+ const subAgents = SUB_AGENT_MAP[p.id] || [];
350
166
  const tools = await toolManager.getAllToolsAsync();
351
167
  const installed = new Set(tools.filter(t => t.category === 'plugin' && t.installed).map(t => t.id));
352
168
  return { subAgents: subAgents.filter(sa => installed.has(sa.id)) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.715",
3
+ "version": "1.0.717",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -25,6 +25,10 @@ import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
25
25
  import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
26
26
  import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
27
27
  import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
28
+ import { register as registerOAuthHandlers } from './lib/ws-handlers-oauth.js';
29
+ import { register as registerScriptHandlers } from './lib/ws-handlers-scripts.js';
30
+ import { register as registerQueueHandlers } from './lib/ws-handlers-queue.js';
31
+ import { register as registerMsgHandlers } from './lib/ws-handlers-msg.js';
28
32
  import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-sdk-manager.js';
29
33
  import * as execMachine from './lib/execution-machine.js';
30
34
  import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
@@ -4520,11 +4524,18 @@ function broadcastSync(event) {
4520
4524
  const wsRouter = new WsRouter();
4521
4525
 
4522
4526
  registerConvHandlers(wsRouter, {
4523
- queries, activeExecutions, messageQueues, rateLimitState,
4524
- broadcastSync, processMessageWithStreaming, cleanupExecution, logError,
4527
+ queries, activeExecutions, rateLimitState,
4528
+ broadcastSync, processMessageWithStreaming, cleanupExecution,
4525
4529
  getJsonlWatcher: () => jsonlWatcher
4526
4530
  });
4527
4531
 
4532
+ registerMsgHandlers(wsRouter, {
4533
+ queries, activeExecutions, messageQueues,
4534
+ broadcastSync, processMessageWithStreaming, logError
4535
+ });
4536
+
4537
+ registerQueueHandlers(wsRouter, { queries, messageQueues, broadcastSync });
4538
+
4528
4539
  console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.length:', discoveredAgents.length);
4529
4540
  registerSessionHandlers(wsRouter, {
4530
4541
  db: queries, discoveredAgents, modelCache,
@@ -4541,11 +4552,18 @@ registerRunHandlers(wsRouter, {
4541
4552
  registerUtilHandlers(wsRouter, {
4542
4553
  queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
4543
4554
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
4555
+ STARTUP_CWD, voiceCacheManager, toolManager, discoveredAgents
4556
+ });
4557
+
4558
+ registerScriptHandlers(wsRouter, {
4559
+ queries, broadcastSync, STARTUP_CWD, activeScripts
4560
+ });
4561
+
4562
+ registerOAuthHandlers(wsRouter, {
4544
4563
  startGeminiOAuth, exchangeGeminiOAuthCode,
4545
4564
  geminiOAuthState: () => geminiOAuthState,
4546
4565
  startCodexOAuth, exchangeCodexOAuthCode,
4547
4566
  codexOAuthState: () => codexOAuthState,
4548
- STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents
4549
4567
  });
4550
4568
 
4551
4569
  wsRouter.onLegacy((data, ws) => {