agentgui 1.0.917 → 1.0.918

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.
Files changed (52) hide show
  1. package/database-schema.js +0 -58
  2. package/lib/db-queries-cleanup.js +0 -12
  3. package/lib/db-queries-del.js +0 -1
  4. package/lib/db-queries.js +0 -4
  5. package/lib/http-handler.js +1 -10
  6. package/lib/plugins/database-plugin.js +1 -1
  7. package/lib/process-message.js +2 -2
  8. package/lib/provider-config.js +0 -16
  9. package/lib/recovery.js +2 -12
  10. package/lib/routes-agent-actions.js +2 -58
  11. package/lib/routes-debug.js +1 -7
  12. package/lib/routes-registry.js +5 -17
  13. package/lib/server-startup.js +1 -59
  14. package/lib/stream-event-handler.js +1 -3
  15. package/lib/ws-handlers-session2.js +2 -23
  16. package/lib/ws-handlers-util.js +106 -175
  17. package/lib/ws-legacy-handlers.js +1 -104
  18. package/lib/ws-setup.js +1 -3
  19. package/package.json +1 -15
  20. package/server.js +9 -26
  21. package/test.js +1 -21
  22. package/ecosystem.config.cjs +0 -22
  23. package/lib/checkpoint-manager.js +0 -182
  24. package/lib/db-queries-tools.js +0 -131
  25. package/lib/db-queries-voice.js +0 -85
  26. package/lib/oauth-codex.js +0 -164
  27. package/lib/oauth-common.js +0 -92
  28. package/lib/oauth-gemini.js +0 -199
  29. package/lib/plugins/auth-plugin.js +0 -132
  30. package/lib/plugins/speech-plugin.js +0 -72
  31. package/lib/plugins/tools-plugin.js +0 -120
  32. package/lib/pm2-manager.js +0 -170
  33. package/lib/routes-oauth.js +0 -105
  34. package/lib/routes-speech.js +0 -173
  35. package/lib/routes-tools.js +0 -184
  36. package/lib/speech-manager.js +0 -200
  37. package/lib/speech.js +0 -50
  38. package/lib/tool-install-machine.js +0 -157
  39. package/lib/tool-manager.js +0 -98
  40. package/lib/tool-provisioner.js +0 -98
  41. package/lib/tool-spawner.js +0 -163
  42. package/lib/tool-version-check.js +0 -196
  43. package/lib/tool-version-fetch.js +0 -68
  44. package/lib/ws-handlers-oauth.js +0 -76
  45. package/static/js/agent-auth-oauth.js +0 -159
  46. package/static/js/pm2-monitor.js +0 -151
  47. package/static/js/stt-handler.js +0 -147
  48. package/static/js/tool-install-machine.js +0 -155
  49. package/static/js/tools-manager-ui.js +0 -124
  50. package/static/js/tools-manager.js +0 -172
  51. package/static/js/voice-machine.js +0 -145
  52. package/static/js/voice.js +0 -134
@@ -1,170 +0,0 @@
1
- import pm2 from 'pm2';
2
-
3
- const ACTIVE_STATES = new Set(['online', 'launching', 'stopping', 'waiting restart']);
4
-
5
- class PM2Manager {
6
- constructor() {
7
- this.connected = false;
8
- this.monitoring = false;
9
- this.monitorInterval = null;
10
- this.broadcastFn = null;
11
- this.logStreams = new Map();
12
- }
13
-
14
- async connect() {
15
- if (this.connected) return true;
16
- return new Promise((resolve, reject) => {
17
- pm2.connect(false, (err) => {
18
- if (err) { this.connected = false; reject(err); }
19
- else { this.connected = true; resolve(true); }
20
- });
21
- });
22
- }
23
-
24
- async ensureConnected() {
25
- if (this.connected) return true;
26
- await this.connect();
27
- return true;
28
- }
29
-
30
- async listProcesses() {
31
- try {
32
- await this.ensureConnected();
33
- return new Promise((resolve) => {
34
- pm2.list((err, list) => {
35
- if (err) { this.connected = false; resolve([]); return; }
36
- if (!Array.isArray(list)) { resolve([]); return; }
37
- resolve(list.map(proc => ({
38
- name: proc.name,
39
- pm_id: proc.pm_id,
40
- status: proc.status,
41
- pid: proc.pid,
42
- cpu: proc.monit ? (proc.monit.cpu || 0) : 0,
43
- memory: proc.monit ? (typeof proc.monit.memory === 'number' ? proc.monit.memory : 0) : 0,
44
- uptime: proc.pm2_env ? proc.pm2_env.pm_uptime : null,
45
- restarts: proc.pm2_env ? (proc.pm2_env.restart_time || 0) : 0,
46
- watching: proc.pm2_env ? (proc.pm2_env.watch || false) : false,
47
- isActive: ACTIVE_STATES.has(proc.status)
48
- })));
49
- });
50
- });
51
- } catch (_) {
52
- this.connected = false;
53
- return [];
54
- }
55
- }
56
-
57
- async startMonitoring(broadcastFn) {
58
- if (this.monitoring) return;
59
- this.monitoring = true;
60
- this.broadcastFn = broadcastFn;
61
- const tick = async () => {
62
- if (!this.monitoring) return;
63
- try {
64
- const processes = await this.listProcesses();
65
- const hasActive = processes.some(p => p.isActive);
66
- broadcastFn({ type: 'pm2_monit_update', processes, hasActive, available: true, timestamp: Date.now() });
67
- } catch (_) {}
68
- };
69
- this.monitorInterval = setInterval(tick, 2000);
70
- await tick();
71
- }
72
-
73
- stopMonitoring() {
74
- this.monitoring = false;
75
- if (this.monitorInterval) { clearInterval(this.monitorInterval); this.monitorInterval = null; }
76
- this.broadcastFn = null;
77
- }
78
-
79
- async startProcess(name) {
80
- try {
81
- await this.ensureConnected();
82
- return new Promise((resolve) => {
83
- pm2.start(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
84
- });
85
- } catch (err) { return { success: false, error: err.message }; }
86
- }
87
-
88
- async stopProcess(name) {
89
- try {
90
- await this.ensureConnected();
91
- return new Promise((resolve) => {
92
- pm2.stop(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
93
- });
94
- } catch (err) { return { success: false, error: err.message }; }
95
- }
96
-
97
- async restartProcess(name) {
98
- try {
99
- await this.ensureConnected();
100
- return new Promise((resolve) => {
101
- pm2.restart(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
102
- });
103
- } catch (err) { return { success: false, error: err.message }; }
104
- }
105
-
106
- async deleteProcess(name) {
107
- try {
108
- await this.ensureConnected();
109
- return new Promise((resolve) => {
110
- pm2.delete(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
111
- });
112
- } catch (err) { return { success: false, error: err.message }; }
113
- }
114
-
115
- async getLogs(name, options = {}) {
116
- try {
117
- await this.ensureConnected();
118
- const existing = this.logStreams.get(name);
119
- if (existing) { try { existing.destroy(); } catch (_) {} this.logStreams.delete(name); }
120
- return new Promise((resolve) => {
121
- const lines = [];
122
- let settled = false;
123
- const finish = (result) => { if (!settled) { settled = true; resolve(result); } };
124
- const timer = setTimeout(() => finish({ success: true, logs: lines.join('\n') }), 4000);
125
- try {
126
- const stream = pm2.logs(name, { raw: true, lines: options.lines || 100, follow: false });
127
- this.logStreams.set(name, stream);
128
- stream.on('data', (c) => lines.push(c.toString()));
129
- stream.on('end', () => { clearTimeout(timer); this.logStreams.delete(name); finish({ success: true, logs: lines.join('\n') }); });
130
- stream.on('error', (e) => { clearTimeout(timer); this.logStreams.delete(name); finish({ success: false, error: e.message }); });
131
- } catch (err) { clearTimeout(timer); finish({ success: false, error: err.message }); }
132
- });
133
- } catch (err) { return { success: false, error: err.message }; }
134
- }
135
-
136
- async flushLogs(name) {
137
- try {
138
- await this.ensureConnected();
139
- return new Promise((resolve) => {
140
- pm2.flush(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
141
- });
142
- } catch (err) { return { success: false, error: err.message }; }
143
- }
144
-
145
- async ping() {
146
- try {
147
- await this.ensureConnected();
148
- return { success: true, status: 'connected' };
149
- } catch (err) { return { success: false, error: err.message }; }
150
- }
151
-
152
- async heal() {
153
- this.connected = false;
154
- try {
155
- await this.connect();
156
- return { success: true };
157
- } catch (err) { return { success: false, error: err.message }; }
158
- }
159
-
160
- disconnect() {
161
- this.stopMonitoring();
162
- for (const [, s] of this.logStreams) { try { s.destroy(); } catch (_) {} }
163
- this.logStreams.clear();
164
- this.connected = false;
165
- try { pm2.disconnect(); } catch (_) {}
166
- }
167
- }
168
-
169
- export const pm2Manager = new PM2Manager();
170
- export default pm2Manager;
@@ -1,105 +0,0 @@
1
- import { startGeminiOAuth, exchangeGeminiOAuthCode, getGeminiOAuthState } from './oauth-gemini.js';
2
- import { startCodexOAuth, exchangeCodexOAuthCode, getCodexOAuthState } from './oauth-codex.js';
3
-
4
- export function register(deps) {
5
- const { sendJSON, parseBody, PORT, BASE_URL, rootDir } = deps;
6
- const routes = {};
7
-
8
- routes['POST /api/gemini-oauth/start'] = async (req, res) => {
9
- try {
10
- const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
11
- sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
12
- } catch (e) {
13
- console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
14
- sendJSON(req, res, 500, { error: e.message });
15
- }
16
- };
17
-
18
- routes['GET /api/gemini-oauth/status'] = async (req, res) => {
19
- sendJSON(req, res, 200, getGeminiOAuthState());
20
- };
21
-
22
- routes['POST /api/gemini-oauth/relay'] = async (req, res) => {
23
- try {
24
- const body = await parseBody(req);
25
- const { code, state: stateParam } = body;
26
- if (!code || !stateParam) { sendJSON(req, res, 400, { error: 'Missing code or state' }); return; }
27
- const email = await exchangeGeminiOAuthCode(code, stateParam);
28
- sendJSON(req, res, 200, { success: true, email });
29
- } catch (e) {
30
- sendJSON(req, res, 400, { error: e.message });
31
- }
32
- };
33
-
34
- routes['POST /api/gemini-oauth/complete'] = async (req, res) => {
35
- try {
36
- const body = await parseBody(req);
37
- const pastedUrl = (body.url || '').trim();
38
- if (!pastedUrl) { sendJSON(req, res, 400, { error: 'No URL provided' }); return; }
39
- let parsed;
40
- try { parsed = new URL(pastedUrl); } catch (_) {
41
- sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' }); return;
42
- }
43
- const error = parsed.searchParams.get('error');
44
- if (error) { sendJSON(req, res, 200, { error: parsed.searchParams.get('error_description') || error }); return; }
45
- const code = parsed.searchParams.get('code');
46
- const state = parsed.searchParams.get('state');
47
- const email = await exchangeGeminiOAuthCode(code, state);
48
- sendJSON(req, res, 200, { success: true, email });
49
- } catch (e) {
50
- sendJSON(req, res, 400, { error: e.message });
51
- }
52
- };
53
-
54
- routes['POST /api/codex-oauth/start'] = async (req, res) => {
55
- try {
56
- const result = await startCodexOAuth(req, { PORT, BASE_URL });
57
- sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
58
- } catch (e) {
59
- console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
60
- sendJSON(req, res, 500, { error: e.message });
61
- }
62
- };
63
-
64
- routes['GET /api/codex-oauth/status'] = async (req, res) => {
65
- sendJSON(req, res, 200, getCodexOAuthState());
66
- };
67
-
68
- routes['POST /api/codex-oauth/relay'] = async (req, res) => {
69
- try {
70
- const body = await parseBody(req);
71
- const { code, state: stateParam } = body;
72
- if (!code || !stateParam) { sendJSON(req, res, 400, { error: 'Missing code or state' }); return; }
73
- const email = await exchangeCodexOAuthCode(code, stateParam);
74
- sendJSON(req, res, 200, { success: true, email });
75
- } catch (e) {
76
- sendJSON(req, res, 400, { error: e.message });
77
- }
78
- };
79
-
80
- routes['POST /api/codex-oauth/complete'] = async (req, res) => {
81
- try {
82
- const body = await parseBody(req);
83
- const pastedUrl = (body.url || '').trim();
84
- if (!pastedUrl) { sendJSON(req, res, 400, { error: 'No URL provided' }); return; }
85
- let parsed;
86
- try { parsed = new URL(pastedUrl); } catch (_) {
87
- sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' }); return;
88
- }
89
- const error = parsed.searchParams.get('error');
90
- if (error) { sendJSON(req, res, 200, { error: parsed.searchParams.get('error_description') || error }); return; }
91
- const code = parsed.searchParams.get('code');
92
- const state = parsed.searchParams.get('state');
93
- const email = await exchangeCodexOAuthCode(code, state);
94
- sendJSON(req, res, 200, { success: true, email });
95
- } catch (e) {
96
- sendJSON(req, res, 400, { error: e.message });
97
- }
98
- };
99
-
100
- routes['_match'] = (method, pathOnly) => {
101
- return routes[`${method} ${pathOnly}`] || null;
102
- };
103
-
104
- return routes;
105
- }
@@ -1,173 +0,0 @@
1
- import { getSpeech, ensurePocketTtsSetup, ensureModelsDownloaded, modelDownloadState } from './speech-manager.js';
2
-
3
- export function register(deps) {
4
- const { sendJSON, parseBody, broadcastSync, debugLog } = deps;
5
- const routes = {};
6
-
7
- routes['POST /api/stt'] = async (req, res) => {
8
- try {
9
- const chunks = [];
10
- for await (const chunk of req) chunks.push(chunk);
11
- const audioBuffer = Buffer.concat(chunks);
12
- if (audioBuffer.length === 0) { sendJSON(req, res, 400, { error: 'No audio data' }); return; }
13
- broadcastSync({ type: 'stt_progress', status: 'transcribing', percentComplete: 0 });
14
- const { transcribe } = await getSpeech();
15
- const text = await transcribe(audioBuffer);
16
- const finalText = (text || '').trim();
17
- broadcastSync({ type: 'stt_progress', status: 'completed', percentComplete: 100, transcript: finalText });
18
- sendJSON(req, res, 200, { text: finalText });
19
- } catch (err) {
20
- debugLog('[STT] Error: ' + err.message);
21
- let errorMsg = err.message || 'STT failed';
22
- if (errorMsg.includes('VERS_1.21') || errorMsg.includes('onnxruntime')) {
23
- errorMsg = 'STT model load failed: onnxruntime version mismatch. Try: npm install or npm ci';
24
- } else if (errorMsg.includes('not valid JSON') || errorMsg.includes('Unexpected token') || errorMsg.includes('corrupted files cleared')) {
25
- try {
26
- const speech = await getSpeech();
27
- const cleared = speech.clearCorruptedSTTCache();
28
- speech.resetSTTError();
29
- await ensureModelsDownloaded();
30
- errorMsg = 'STT cache was corrupted, re-downloaded models. Please try again.';
31
- } catch (e) {
32
- errorMsg = 'STT model load failed: corrupted cache. Recovery attempted, try again.';
33
- }
34
- }
35
- broadcastSync({ type: 'stt_progress', status: 'failed', percentComplete: 0, error: errorMsg });
36
- if (!res.headersSent) sendJSON(req, res, 500, { error: errorMsg });
37
- }
38
- };
39
-
40
- routes['GET /api/voices'] = async (req, res) => {
41
- try {
42
- const { getVoices } = await getSpeech();
43
- sendJSON(req, res, 200, { ok: true, voices: getVoices() });
44
- } catch (err) {
45
- sendJSON(req, res, 200, { ok: true, voices: [] });
46
- }
47
- };
48
-
49
- routes['POST /api/tts'] = async (req, res) => {
50
- try {
51
- const body = await parseBody(req);
52
- const text = body.text || '';
53
- const voiceId = body.voiceId || null;
54
- if (!text) { sendJSON(req, res, 400, { error: 'No text provided' }); return; }
55
- if (process.platform === 'win32') {
56
- const setupOk = await ensurePocketTtsSetup((msg) => {
57
- broadcastSync({ type: 'tts_setup_progress', ...msg });
58
- });
59
- if (!setupOk) { sendJSON(req, res, 503, { error: 'pocket-tts setup failed', retryable: false }); return; }
60
- const speech = await getSpeech();
61
- if (speech.preloadTTS) { speech.preloadTTS(); await new Promise(r => setTimeout(r, 2000)); }
62
- }
63
- const speech = await getSpeech();
64
- const wavBuffer = await speech.synthesize(text, voiceId);
65
- res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': wavBuffer.length });
66
- res.end(wavBuffer);
67
- } catch (err) {
68
- debugLog('[TTS] Error: ' + err.message);
69
- const isModelError = /model.*load|pipeline.*failed|failed to load/i.test(err.message);
70
- const statusCode = isModelError ? 503 : 500;
71
- if (!res.headersSent) sendJSON(req, res, statusCode, { error: err.message || 'TTS failed', retryable: !isModelError });
72
- }
73
- };
74
-
75
- routes['POST /api/tts-stream'] = async (req, res) => {
76
- try {
77
- const body = await parseBody(req);
78
- const text = body.text || '';
79
- const voiceId = body.voiceId || null;
80
- if (!text) { sendJSON(req, res, 400, { error: 'No text provided' }); return; }
81
- const speech = await getSpeech();
82
- const gen = speech.synthesizeStream(text, voiceId);
83
- const firstResult = await gen.next();
84
- if (firstResult.done) { sendJSON(req, res, 500, { error: 'TTS stream returned no audio', retryable: true }); return; }
85
- res.writeHead(200, {
86
- 'Content-Type': 'application/octet-stream',
87
- 'Transfer-Encoding': 'chunked',
88
- 'X-Content-Type': 'audio/wav-stream',
89
- 'Cache-Control': 'no-cache'
90
- });
91
- const writeChunk = (wavChunk) => {
92
- const lenBuf = Buffer.alloc(4);
93
- lenBuf.writeUInt32BE(wavChunk.length, 0);
94
- res.write(lenBuf);
95
- res.write(wavChunk);
96
- };
97
- writeChunk(firstResult.value);
98
- for await (const wavChunk of gen) writeChunk(wavChunk);
99
- res.end();
100
- } catch (err) {
101
- debugLog('[TTS-STREAM] Error: ' + err.message);
102
- const isModelError = /model.*load|pipeline.*failed|failed to load/i.test(err.message);
103
- if (!res.headersSent) sendJSON(req, res, isModelError ? 503 : 500, { error: err.message || 'TTS stream failed', retryable: !isModelError });
104
- else res.end();
105
- }
106
- };
107
-
108
- routes['GET /api/speech-status'] = async (req, res) => {
109
- try {
110
- const { getStatus } = await getSpeech();
111
- const baseStatus = getStatus();
112
- sendJSON(req, res, 200, {
113
- ...baseStatus,
114
- setupMessage: baseStatus.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request',
115
- modelsDownloading: modelDownloadState.downloading,
116
- modelsComplete: modelDownloadState.complete,
117
- modelsError: modelDownloadState.error,
118
- modelsProgress: modelDownloadState.progress,
119
- });
120
- } catch (err) {
121
- sendJSON(req, res, 200, {
122
- sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false,
123
- setupMessage: 'Will setup on first TTS request',
124
- modelsDownloading: modelDownloadState.downloading,
125
- modelsComplete: modelDownloadState.complete,
126
- modelsError: modelDownloadState.error,
127
- });
128
- }
129
- };
130
-
131
- routes['POST /api/speech-status'] = async (req, res) => {
132
- const body = await parseBody(req);
133
- if (body.forceDownload) {
134
- if (modelDownloadState.complete) { sendJSON(req, res, 200, { ok: true, modelsComplete: true, message: 'Models already ready' }); return; }
135
- if (!modelDownloadState.downloading) {
136
- modelDownloadState.error = null;
137
- ensureModelsDownloaded().then(ok => {
138
- broadcastSync({ type: 'model_download_progress', progress: { done: true, complete: ok, error: ok ? null : 'Download failed' } });
139
- }).catch(err => {
140
- broadcastSync({ type: 'model_download_progress', progress: { done: true, error: err.message } });
141
- });
142
- }
143
- sendJSON(req, res, 200, { ok: true, message: 'Starting model download' });
144
- return;
145
- }
146
- sendJSON(req, res, 400, { error: 'Unknown request' });
147
- };
148
-
149
- routes['_match'] = (method, pathOnly) => {
150
- const key = `${method} ${pathOnly}`;
151
- if (routes[key]) return routes[key];
152
- if (method === 'GET' && pathOnly.startsWith('/api/tts-cache/')) return routes['_tts-cache'];
153
- return null;
154
- };
155
-
156
- routes['_tts-cache'] = async (req, res, pathOnly) => {
157
- const cacheKey = decodeURIComponent(pathOnly.slice('/api/tts-cache/'.length));
158
- try {
159
- const speech = await getSpeech();
160
- const cached = speech.ttsCacheGet(cacheKey);
161
- if (cached) {
162
- res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': cached.length, 'Cache-Control': 'public, max-age=3600' });
163
- res.end(cached);
164
- } else {
165
- sendJSON(req, res, 404, { error: 'not cached' });
166
- }
167
- } catch (err) {
168
- sendJSON(req, res, 500, { error: err.message });
169
- }
170
- };
171
-
172
- return routes;
173
- }
@@ -1,184 +0,0 @@
1
- import * as toolInstallMachine from './tool-install-machine.js';
2
-
3
- export function register(deps) {
4
- const { sendJSON, parseBody, queries, broadcastSync, logError, toolManager } = deps;
5
-
6
- function mapTool(t) {
7
- return {
8
- id: t.id, name: t.name, pkg: t.pkg,
9
- category: t.category || 'plugin',
10
- installed: t.installed || false,
11
- status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
12
- isUpToDate: t.isUpToDate || false,
13
- upgradeNeeded: t.upgradeNeeded || false,
14
- hasUpdate: (t.upgradeNeeded && t.installed) || false,
15
- installedVersion: t.installedVersion || null,
16
- publishedVersion: t.publishedVersion || null,
17
- machineState: toolInstallMachine.getState(t.id),
18
- };
19
- }
20
-
21
- const routes = {};
22
-
23
- routes['GET /api/tools'] = async (req, res) => {
24
- try {
25
- const tools = await Promise.race([
26
- toolManager.getAllToolsAsync(true),
27
- new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
28
- ]);
29
- sendJSON(req, res, 200, { tools: tools.map(mapTool) });
30
- } catch (err) {
31
- sendJSON(req, res, 200, { tools: toolManager.getAllToolsSync().map(mapTool) });
32
- }
33
- };
34
-
35
- routes['POST /api/tools/update'] = async (req, res) => {
36
- const allToolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'cli-agent-browser', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo', 'gm-codex'];
37
- sendJSON(req, res, 200, { updating: true, toolCount: allToolIds.length });
38
- broadcastSync({ type: 'tools_update_started', tools: allToolIds });
39
- setImmediate(async () => {
40
- const results = {};
41
- for (const toolId of allToolIds) {
42
- try {
43
- const result = await toolManager.update(toolId, (msg) => broadcastSync({ type: 'tool_update_progress', toolId, data: msg }));
44
- results[toolId] = result;
45
- if (result.success) {
46
- const version = result.version || null;
47
- queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
48
- queries.addToolInstallHistory(toolId, 'update', 'success', null);
49
- const freshStatus = await toolManager.checkToolStatusAsync(toolId);
50
- broadcastSync({ type: 'tool_update_complete', toolId, data: { ...result, ...freshStatus } });
51
- } else {
52
- queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
53
- queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
54
- broadcastSync({ type: 'tool_update_failed', toolId, data: result });
55
- }
56
- } catch (err) {
57
- queries.updateToolStatus(toolId, { status: 'failed', error_message: err.message });
58
- queries.addToolInstallHistory(toolId, 'update', 'failed', err.message);
59
- broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error: err.message } });
60
- }
61
- }
62
- broadcastSync({ type: 'tools_update_complete', data: results });
63
- });
64
- };
65
-
66
- routes['POST /api/tools/refresh-all'] = async (req, res) => {
67
- sendJSON(req, res, 200, { refreshing: true, toolCount: 4 });
68
- broadcastSync({ type: 'tools_refresh_started' });
69
- setImmediate(async () => {
70
- const tools = await toolManager.refreshAllToolsAsync();
71
- for (const tool of tools) {
72
- queries.updateToolStatus(tool.id, { status: tool.installed ? 'installed' : 'not_installed', version: tool.installedVersion, last_check_at: Date.now() });
73
- if (tool.installed && tool.upgradeNeeded) {
74
- queries.updateToolStatus(tool.id, { update_available: 1, latest_version: tool.publishedVersion });
75
- }
76
- }
77
- broadcastSync({ type: 'tools_refresh_complete', data: tools });
78
- });
79
- };
80
-
81
- routes['_match'] = (method, pathOnly) => {
82
- const key = `${method} ${pathOnly}`;
83
- if (routes[key]) return routes[key];
84
- let m;
85
- if ((m = pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/))) return (req, res) => handleToolStatus(req, res, m[1]);
86
- if (method === 'POST' && (m = pathOnly.match(/^\/api\/tools\/([^/]+)\/install$/))) return (req, res) => handleToolInstall(req, res, m[1]);
87
- if (method === 'POST' && (m = pathOnly.match(/^\/api\/tools\/([^/]+)\/update$/))) return (req, res) => handleToolUpdate(req, res, m[1]);
88
- if (method === 'GET' && (m = pathOnly.match(/^\/api\/tools\/([^/]+)\/history$/))) return (req, res) => handleToolHistory(req, res, m[1]);
89
- return null;
90
- };
91
-
92
- async function handleToolStatus(req, res, toolId) {
93
- const dbStatus = queries.getToolStatus(toolId);
94
- const tmStatus = toolManager.checkToolStatus(toolId);
95
- if (!tmStatus && !dbStatus) { sendJSON(req, res, 404, { error: 'Tool not found' }); return; }
96
- const status = {
97
- toolId, installed: tmStatus?.installed || (dbStatus?.status === 'installed'),
98
- isUpToDate: tmStatus?.isUpToDate || false, upgradeNeeded: tmStatus?.upgradeNeeded || false,
99
- status: dbStatus?.status || (tmStatus?.installed ? 'installed' : 'not_installed'),
100
- installedVersion: dbStatus?.version || tmStatus?.installedVersion || null,
101
- timestamp: Date.now(), error_message: dbStatus?.error_message || null
102
- };
103
- if (status.installed) { const updates = await toolManager.checkForUpdates(toolId); status.hasUpdate = updates.needsUpdate || false; }
104
- sendJSON(req, res, 200, status);
105
- }
106
-
107
- async function handleToolInstall(req, res, toolId) {
108
- const tool = toolManager.getToolConfig(toolId);
109
- if (!tool) { sendJSON(req, res, 404, { error: 'Tool not found' }); return; }
110
- const existing = queries.getToolStatus(toolId);
111
- if (!existing) queries.insertToolInstallation(toolId, { status: 'not_installed' });
112
- queries.updateToolStatus(toolId, { status: 'installing' });
113
- sendJSON(req, res, 200, { success: true, installing: true, estimatedTime: 60000 });
114
- let done = false;
115
- const timeout = setTimeout(() => {
116
- if (!done) { done = true; queries.updateToolStatus(toolId, { status: 'failed', error_message: 'Install timeout after 6 minutes' }); broadcastSync({ type: 'tool_install_failed', toolId, data: { success: false, error: 'Install timeout after 6 minutes' } }); queries.addToolInstallHistory(toolId, 'install', 'failed', 'Install timeout after 6 minutes'); }
117
- }, 360000);
118
- toolManager.install(toolId, (msg) => broadcastSync({ type: 'tool_install_progress', toolId, data: msg })).then(async (result) => {
119
- clearTimeout(timeout); if (done) return; done = true;
120
- if (result.success) {
121
- const version = result.version || null;
122
- queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
123
- const freshStatus = await toolManager.checkToolStatusAsync(toolId);
124
- broadcastSync({ type: 'tool_install_complete', toolId, data: { success: true, version, ...freshStatus } });
125
- queries.addToolInstallHistory(toolId, 'install', 'success', null);
126
- } else {
127
- queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
128
- broadcastSync({ type: 'tool_install_failed', toolId, data: result });
129
- queries.addToolInstallHistory(toolId, 'install', 'failed', result.error);
130
- }
131
- }).catch((err) => {
132
- clearTimeout(timeout); if (done) return; done = true;
133
- const error = err?.message || 'Unknown error';
134
- logError('toolInstall', err, { toolId });
135
- queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
136
- broadcastSync({ type: 'tool_install_failed', toolId, data: { success: false, error } });
137
- queries.addToolInstallHistory(toolId, 'install', 'failed', error);
138
- });
139
- }
140
-
141
- async function handleToolUpdate(req, res, toolId) {
142
- const body = await parseBody(req);
143
- const tool = toolManager.getToolConfig(toolId);
144
- if (!tool) { sendJSON(req, res, 404, { error: 'Tool not found' }); return; }
145
- const current = await toolManager.checkToolStatusAsync(toolId);
146
- if (!current || !current.installed) { sendJSON(req, res, 400, { error: 'Tool not installed' }); return; }
147
- queries.updateToolStatus(toolId, { status: 'updating' });
148
- sendJSON(req, res, 200, { success: true, updating: true });
149
- let done = false;
150
- const timeout = setTimeout(() => {
151
- if (!done) { done = true; queries.updateToolStatus(toolId, { status: 'failed', error_message: 'Update timeout after 6 minutes' }); broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error: 'Update timeout after 6 minutes' } }); queries.addToolInstallHistory(toolId, 'update', 'failed', 'Update timeout after 6 minutes'); }
152
- }, 360000);
153
- toolManager.update(toolId, (msg) => broadcastSync({ type: 'tool_update_progress', toolId, data: msg })).then(async (result) => {
154
- clearTimeout(timeout); if (done) return; done = true;
155
- if (result.success) {
156
- const version = result.version || null;
157
- queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
158
- const freshStatus = await toolManager.checkToolStatusAsync(toolId);
159
- broadcastSync({ type: 'tool_update_complete', toolId, data: { success: true, version, ...freshStatus } });
160
- queries.addToolInstallHistory(toolId, 'update', 'success', null);
161
- } else {
162
- queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
163
- broadcastSync({ type: 'tool_update_failed', toolId, data: result });
164
- queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
165
- }
166
- }).catch((err) => {
167
- clearTimeout(timeout); if (done) return; done = true;
168
- const error = err?.message || 'Unknown error';
169
- logError('toolUpdate', err, { toolId });
170
- queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
171
- broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error } });
172
- queries.addToolInstallHistory(toolId, 'update', 'failed', error);
173
- });
174
- }
175
-
176
- async function handleToolHistory(req, res, toolId) {
177
- const url = new URL(req.url, 'http://localhost');
178
- const limit = Math.min(parseInt(url.searchParams.get('limit')) || 20, 100);
179
- const offset = parseInt(url.searchParams.get('offset')) || 0;
180
- sendJSON(req, res, 200, { history: queries.getToolInstallHistory(toolId, limit, offset) });
181
- }
182
-
183
- return routes;
184
- }