agentgui 1.0.715 → 1.0.716
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/CLAUDE.md +20 -7
- package/lib/acp-protocol.js +91 -0
- package/lib/acp-runner.js +136 -0
- package/lib/acp-sdk-manager.js +20 -64
- package/lib/agent-descriptors.js +47 -332
- package/lib/agent-registry-configs.js +125 -0
- package/lib/claude-runner.js +189 -1247
- package/lib/plugin-loader.js +3 -15
- package/lib/tool-manager.js +99 -621
- package/lib/tool-provisioner.js +93 -0
- package/lib/tool-spawner.js +121 -0
- package/lib/tool-version.js +196 -0
- package/lib/ws-handlers-conv.js +5 -198
- package/lib/ws-handlers-msg.js +119 -0
- package/lib/ws-handlers-oauth.js +76 -0
- package/lib/ws-handlers-queue.js +56 -0
- package/lib/ws-handlers-scripts.js +58 -0
- package/lib/ws-handlers-util.js +22 -206
- package/package.json +1 -1
- package/server.js +21 -3
|
@@ -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
|
+
}
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
if (id
|
|
317
|
-
|
|
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
|
|
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
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,
|
|
4524
|
-
broadcastSync, processMessageWithStreaming, cleanupExecution,
|
|
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) => {
|