agentgui 1.0.397 → 1.0.399
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/ws-events.js +20 -0
- package/lib/ws-handlers-conv.js +183 -0
- package/lib/ws-handlers-run.js +157 -0
- package/lib/ws-handlers-session.js +165 -0
- package/lib/ws-handlers-util.js +186 -0
- package/lib/ws-optimizer.js +2 -2
- package/lib/ws-protocol.js +81 -0
- package/package.json +3 -2
- package/scripts/patch-fsbrowse.js +88 -0
- package/server.js +285 -93
- package/static/app.js +7 -17
- package/static/index.html +19 -18
- package/static/js/client.js +4 -13
- package/static/js/conversations.js +12 -51
- package/static/js/features.js +12 -11
- package/static/js/voice.js +69 -64
- package/static/js/ws-client.js +80 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } 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, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
|
|
10
|
+
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
11
|
+
startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
|
|
12
|
+
STARTUP_CWD } = deps;
|
|
13
|
+
|
|
14
|
+
router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
|
|
15
|
+
|
|
16
|
+
router.handle('folders', (p) => {
|
|
17
|
+
const folderPath = p.path || STARTUP_CWD;
|
|
18
|
+
try {
|
|
19
|
+
const expanded = folderPath.startsWith('~') ? folderPath.replace('~', os.homedir()) : folderPath;
|
|
20
|
+
const entries = fs.readdirSync(expanded, { withFileTypes: true });
|
|
21
|
+
const folders = entries
|
|
22
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
23
|
+
.map(e => ({ name: e.name }))
|
|
24
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
25
|
+
return { folders };
|
|
26
|
+
} catch (e) { err(400, e.message); }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
router.handle('clone', (p) => {
|
|
30
|
+
const repo = (p.repo || '').trim();
|
|
31
|
+
if (!repo || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo)) {
|
|
32
|
+
err(400, 'Invalid repo format. Use org/repo or user/repo');
|
|
33
|
+
}
|
|
34
|
+
const cloneDir = STARTUP_CWD || os.homedir();
|
|
35
|
+
const repoName = repo.split('/')[1];
|
|
36
|
+
const targetPath = path.join(cloneDir, repoName);
|
|
37
|
+
if (fs.existsSync(targetPath)) err(409, `Directory already exists: ${repoName}`);
|
|
38
|
+
try {
|
|
39
|
+
const isWindows = os.platform() === 'win32';
|
|
40
|
+
execSync('git clone https://github.com/' + repo + '.git', {
|
|
41
|
+
cwd: cloneDir, encoding: 'utf-8', timeout: 120000,
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
44
|
+
shell: isWindows
|
|
45
|
+
});
|
|
46
|
+
return { ok: true, repo, path: targetPath, name: repoName };
|
|
47
|
+
} catch (e) { err(500, (e.stderr || e.message || 'Clone failed').trim()); }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
router.handle('git.check', () => {
|
|
51
|
+
try {
|
|
52
|
+
const isWindows = os.platform() === 'win32';
|
|
53
|
+
const devnull = isWindows ? '' : ' 2>/dev/null';
|
|
54
|
+
const remoteUrl = execSync('git remote get-url origin' + devnull, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows }).trim();
|
|
55
|
+
const statusResult = execSync('git status --porcelain' + devnull, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
56
|
+
const hasChanges = statusResult.trim().length > 0;
|
|
57
|
+
const unpushedResult = execSync('git rev-list --count --not --remotes' + devnull, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
58
|
+
const hasUnpushed = parseInt(unpushedResult.trim() || '0', 10) > 0;
|
|
59
|
+
const ownsRemote = !remoteUrl.includes('github.com/') || remoteUrl.includes(process.env.GITHUB_USER || '');
|
|
60
|
+
return { ownsRemote, hasChanges, hasUnpushed, remoteUrl };
|
|
61
|
+
} catch {
|
|
62
|
+
return { ownsRemote: false, hasChanges: false, hasUnpushed: false, remoteUrl: '' };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
router.handle('git.push', () => {
|
|
67
|
+
try {
|
|
68
|
+
const isWindows = os.platform() === 'win32';
|
|
69
|
+
const cmd = isWindows
|
|
70
|
+
? 'git add -A & git commit -m "Auto-commit" & git push'
|
|
71
|
+
: 'git add -A && git commit -m "Auto-commit" && git push';
|
|
72
|
+
execSync(cmd, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
|
|
73
|
+
return { success: true };
|
|
74
|
+
} catch (e) { err(500, e.message); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
router.handle('speech.status', async () => {
|
|
78
|
+
try {
|
|
79
|
+
const { getStatus } = await getSpeech();
|
|
80
|
+
const base = getStatus();
|
|
81
|
+
let pythonDetected = false, pythonVersion = null;
|
|
82
|
+
try {
|
|
83
|
+
const { createRequire } = await import('module');
|
|
84
|
+
const r = createRequire(import.meta.url);
|
|
85
|
+
const serverTTS = r('webtalk/server-tts');
|
|
86
|
+
if (typeof serverTTS.detectPython === 'function') {
|
|
87
|
+
const py = serverTTS.detectPython();
|
|
88
|
+
pythonDetected = py.found;
|
|
89
|
+
pythonVersion = py.version || null;
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
return {
|
|
93
|
+
...base, pythonDetected, pythonVersion,
|
|
94
|
+
setupMessage: base.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request',
|
|
95
|
+
modelsDownloading: modelDownloadState.downloading,
|
|
96
|
+
modelsComplete: modelDownloadState.complete,
|
|
97
|
+
modelsError: modelDownloadState.error,
|
|
98
|
+
modelsProgress: modelDownloadState.progress,
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false,
|
|
103
|
+
setupMessage: 'Will setup on first TTS request',
|
|
104
|
+
modelsDownloading: modelDownloadState.downloading,
|
|
105
|
+
modelsComplete: modelDownloadState.complete,
|
|
106
|
+
modelsError: modelDownloadState.error,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
router.handle('speech.download', () => {
|
|
112
|
+
if (modelDownloadState.complete) return { ok: true, modelsComplete: true, message: 'Models already ready' };
|
|
113
|
+
if (!modelDownloadState.downloading) {
|
|
114
|
+
modelDownloadState.error = null;
|
|
115
|
+
ensureModelsDownloaded().then(ok => {
|
|
116
|
+
broadcastSync({ type: 'model_download_progress', progress: { done: true, complete: ok, error: ok ? null : 'Download failed' } });
|
|
117
|
+
}).catch(e => {
|
|
118
|
+
broadcastSync({ type: 'model_download_progress', progress: { done: true, error: e.message } });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, message: 'Starting model download' };
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
router.handle('voices', async () => {
|
|
125
|
+
try {
|
|
126
|
+
const { getVoices } = await getSpeech();
|
|
127
|
+
return { ok: true, voices: getVoices() };
|
|
128
|
+
} catch { return { ok: true, voices: [] }; }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
router.handle('auth.configs', () => getProviderConfigs());
|
|
132
|
+
|
|
133
|
+
router.handle('auth.save', (p) => {
|
|
134
|
+
const { providerId, apiKey, defaultModel } = p;
|
|
135
|
+
if (typeof providerId !== 'string' || !providerId.length || providerId.length > 100) err(400, 'Invalid providerId');
|
|
136
|
+
if (typeof apiKey !== 'string' || !apiKey.length || apiKey.length > 10000) err(400, 'Invalid apiKey');
|
|
137
|
+
if (defaultModel !== undefined && (typeof defaultModel !== 'string' || defaultModel.length > 200)) err(400, 'Invalid defaultModel');
|
|
138
|
+
const configPath = saveProviderConfig(providerId, apiKey, defaultModel || '');
|
|
139
|
+
return { success: true, path: configPath };
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
router.handle('import.claude', () => ({ imported: queries.importClaudeCodeConversations() }));
|
|
143
|
+
|
|
144
|
+
router.handle('discover.claude', () => ({ discovered: queries.discoverClaudeCodeConversations() }));
|
|
145
|
+
|
|
146
|
+
router.handle('gemini.start', async () => {
|
|
147
|
+
try {
|
|
148
|
+
const result = await startGeminiOAuth();
|
|
149
|
+
return { authUrl: result.authUrl, mode: result.mode };
|
|
150
|
+
} catch (e) { err(500, e.message); }
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
router.handle('gemini.status', () => {
|
|
154
|
+
const st = typeof geminiOAuthState === 'function' ? geminiOAuthState() : geminiOAuthState;
|
|
155
|
+
return st;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
router.handle('gemini.relay', async (p) => {
|
|
159
|
+
const { code, state } = p;
|
|
160
|
+
if (!code || !state) err(400, 'Missing code or state');
|
|
161
|
+
try {
|
|
162
|
+
const email = await exchangeGeminiOAuthCode(code, state);
|
|
163
|
+
return { success: true, email };
|
|
164
|
+
} catch (e) { err(400, e.message); }
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
router.handle('gemini.complete', async (p) => {
|
|
168
|
+
const pastedUrl = (p.url || '').trim();
|
|
169
|
+
if (!pastedUrl) err(400, 'No URL provided');
|
|
170
|
+
let parsed;
|
|
171
|
+
try { parsed = new URL(pastedUrl); } catch { err(400, 'Invalid URL. Paste the full URL from the browser address bar.'); }
|
|
172
|
+
const urlError = parsed.searchParams.get('error');
|
|
173
|
+
if (urlError) {
|
|
174
|
+
const desc = parsed.searchParams.get('error_description') || urlError;
|
|
175
|
+
return { error: desc };
|
|
176
|
+
}
|
|
177
|
+
const code = parsed.searchParams.get('code');
|
|
178
|
+
const state = parsed.searchParams.get('state');
|
|
179
|
+
try {
|
|
180
|
+
const email = await exchangeGeminiOAuthCode(code, state);
|
|
181
|
+
return { success: true, email };
|
|
182
|
+
} catch (e) { err(400, e.message); }
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
router.handle('ws.stats', () => wsOptimizer.getStats());
|
|
186
|
+
}
|
package/lib/ws-optimizer.js
CHANGED
|
@@ -116,7 +116,7 @@ class WSOptimizer {
|
|
|
116
116
|
this.clientQueues = new Map();
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
sendToClient(ws, event) {
|
|
119
|
+
sendToClient(ws, event, originalType) {
|
|
120
120
|
if (ws.readyState !== 1) return;
|
|
121
121
|
let queue = this.clientQueues.get(ws);
|
|
122
122
|
if (!queue) {
|
|
@@ -124,7 +124,7 @@ class WSOptimizer {
|
|
|
124
124
|
this.clientQueues.set(ws, queue);
|
|
125
125
|
}
|
|
126
126
|
const data = typeof event === 'string' ? event : JSON.stringify(event);
|
|
127
|
-
const priority = typeof event === 'object' ? getPriority(event.type) : 2;
|
|
127
|
+
const priority = typeof event === 'object' ? getPriority(originalType || event.type) : 2;
|
|
128
128
|
queue.add(data, priority);
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
class WsRouter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.handlers = new Map();
|
|
4
|
+
this.legacyHandler = null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
handle(method, fn) {
|
|
8
|
+
this.handlers.set(method, fn);
|
|
9
|
+
return this;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
onLegacy(fn) {
|
|
13
|
+
this.legacyHandler = fn;
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
reply(ws, requestId, data) {
|
|
18
|
+
if (ws.readyState === 1) {
|
|
19
|
+
ws.send(JSON.stringify({ r: requestId, d: data || {} }));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
replyError(ws, requestId, code, message) {
|
|
24
|
+
if (ws.readyState === 1) {
|
|
25
|
+
ws.send(JSON.stringify({ r: requestId, e: { c: code, m: message } }));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
send(ws, type, data) {
|
|
30
|
+
if (ws.readyState === 1) {
|
|
31
|
+
ws.send(JSON.stringify({ t: type, d: data || {} }));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
broadcast(clients, type, data) {
|
|
36
|
+
const msg = JSON.stringify({ t: type, d: data || {} });
|
|
37
|
+
for (const ws of clients) {
|
|
38
|
+
if (ws.readyState === 1) ws.send(msg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async onMessage(ws, rawData) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(rawData);
|
|
46
|
+
} catch {
|
|
47
|
+
if (ws.readyState === 1) {
|
|
48
|
+
ws.send(JSON.stringify({ r: null, e: { c: 400, m: 'Invalid JSON' } }));
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (parsed.m && parsed.r !== undefined) {
|
|
54
|
+
const handler = this.handlers.get(parsed.m);
|
|
55
|
+
if (!handler) {
|
|
56
|
+
this.replyError(ws, parsed.r, 404, `Unknown method: ${parsed.m}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const result = await handler(parsed.p || {}, ws);
|
|
61
|
+
this.reply(ws, parsed.r, result);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const code = err.code || 500;
|
|
64
|
+
const message = err.message || 'Internal error';
|
|
65
|
+
this.replyError(ws, parsed.r, code, message);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.legacyHandler) {
|
|
71
|
+
this.legacyHandler(parsed, ws);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parsed.r !== undefined) {
|
|
76
|
+
this.replyError(ws, parsed.r, 400, 'Missing method');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { WsRouter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.399",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"homepage": "https://github.com/AnEntrypoint/agentgui#readme",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node server.js",
|
|
20
|
-
"dev": "node server.js --watch"
|
|
20
|
+
"dev": "node server.js --watch",
|
|
21
|
+
"postinstall": "node scripts/patch-fsbrowse.js"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@anthropic-ai/claude-code": "^2.1.37",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Patch script to fix Windows path duplication issue in fsbrowse
|
|
4
|
+
* Fixes: Error ENOENT: no such file or directory, scandir 'C:\C:\dev'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
const fsbrowsePath = path.join(__dirname, '..', 'node_modules', 'fsbrowse', 'index.js');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(fsbrowsePath)) {
|
|
16
|
+
console.warn('[PATCH] fsbrowse not found, skipping patch');
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
let content = fs.readFileSync(fsbrowsePath, 'utf8');
|
|
22
|
+
|
|
23
|
+
// Check if patch is already applied
|
|
24
|
+
if (content.includes('sanitizedIsAbsoluteOnDrive')) {
|
|
25
|
+
console.log('[PATCH] fsbrowse Windows path fix already applied');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Replace the makeResolver function with the fixed version
|
|
30
|
+
const oldMakeResolver = `function makeResolver(baseDir) {
|
|
31
|
+
return function resolveWithBaseDir(relPath) {
|
|
32
|
+
const sanitized = sanitizePath(relPath);
|
|
33
|
+
const fullPath = path.resolve(baseDir, sanitized);
|
|
34
|
+
if (!fullPath.startsWith(baseDir)) {
|
|
35
|
+
return { ok: false, error: 'EPATHINJECTION' };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true, path: fullPath };
|
|
38
|
+
};
|
|
39
|
+
}`;
|
|
40
|
+
|
|
41
|
+
const newMakeResolver = `function makeResolver(baseDir) {
|
|
42
|
+
const normalizedBase = path.normalize(baseDir);
|
|
43
|
+
const baseDriveLetter = normalizedBase.match(/^[A-Z]:/i)?.[0];
|
|
44
|
+
|
|
45
|
+
return function resolveWithBaseDir(relPath) {
|
|
46
|
+
const sanitized = sanitizePath(relPath);
|
|
47
|
+
let fullPath;
|
|
48
|
+
|
|
49
|
+
// Extract drive letter from both paths to check for same-drive duplication on Windows
|
|
50
|
+
const sanitizedDriveLetter = sanitized.match(/^[A-Z]:/i)?.[0];
|
|
51
|
+
const sanitizedIsAbsoluteOnDrive = /^[A-Z]:/i.test(sanitized);
|
|
52
|
+
|
|
53
|
+
// If both paths are on the same Windows drive, strip the drive letter from relPath
|
|
54
|
+
// to avoid duplication like C:\\C:\\dev
|
|
55
|
+
if (baseDriveLetter && sanitizedIsAbsoluteOnDrive && sanitizedDriveLetter === baseDriveLetter) {
|
|
56
|
+
// Remove drive letter and leading slashes to make it relative
|
|
57
|
+
const relativePath = sanitized.replace(/^[A-Z]:[\/\\]?/i, '');
|
|
58
|
+
fullPath = path.resolve(normalizedBase, relativePath);
|
|
59
|
+
} else {
|
|
60
|
+
fullPath = path.resolve(normalizedBase, sanitized);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Normalize for consistent comparison
|
|
64
|
+
const normalizedFullPath = path.normalize(fullPath);
|
|
65
|
+
const normalizedComparisonBase = path.normalize(normalizedBase);
|
|
66
|
+
|
|
67
|
+
// Check path injection - convert backslashes to forward slashes for comparison
|
|
68
|
+
const normalizedCheck = normalizedFullPath.replace(/\\\\/g, '/');
|
|
69
|
+
const normalizedBaseCheck = normalizedComparisonBase.replace(/\\\\/g, '/');
|
|
70
|
+
|
|
71
|
+
if (!normalizedCheck.startsWith(normalizedBaseCheck)) {
|
|
72
|
+
return { ok: false, error: 'EPATHINJECTION' };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, path: normalizedFullPath };
|
|
75
|
+
};
|
|
76
|
+
}`;
|
|
77
|
+
|
|
78
|
+
if (content.includes(oldMakeResolver)) {
|
|
79
|
+
content = content.replace(oldMakeResolver, newMakeResolver);
|
|
80
|
+
fs.writeFileSync(fsbrowsePath, content, 'utf8');
|
|
81
|
+
console.log('[PATCH] fsbrowse Windows path fix applied successfully');
|
|
82
|
+
} else {
|
|
83
|
+
console.warn('[PATCH] Could not find makeResolver function to patch');
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('[PATCH] Error applying fsbrowse patch:', err.message);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|