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,92 +0,0 @@
1
- import crypto from 'crypto';
2
-
3
- export function buildBaseUrl(req, PORT) {
4
- const override = process.env.AGENTGUI_BASE_URL;
5
- if (override) return override.replace(/\/+$/, '');
6
- const fwdProto = req.headers['x-forwarded-proto'];
7
- const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
8
- if (fwdHost) {
9
- const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
10
- const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
11
- return `${proto}://${cleanHost}`;
12
- }
13
- return `http://127.0.0.1:${PORT}`;
14
- }
15
-
16
- export function isRemoteRequest(req) {
17
- return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
18
- }
19
-
20
- export function encodeOAuthState(csrfToken, relayUrl) {
21
- const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
22
- return Buffer.from(payload).toString('base64url');
23
- }
24
-
25
- export function decodeOAuthState(stateStr) {
26
- try {
27
- const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
28
- return { csrfToken: payload.t, relayUrl: payload.r };
29
- } catch (_) {
30
- return { csrfToken: stateStr, relayUrl: null };
31
- }
32
- }
33
-
34
- export function oauthResultPage(title, message, success) {
35
- const color = success ? '#10b981' : '#ef4444';
36
- const icon = success ? '✓' : '✗';
37
- return `<!DOCTYPE html><html><head><title>${title}</title></head>
38
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
39
- <div style="text-align:center;max-width:400px;padding:2rem;">
40
- <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
41
- <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
42
- <p style="color:#9ca3af;">${message}</p>
43
- <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
44
- </div></body></html>`;
45
- }
46
-
47
- export function oauthRelayPage(code, state, error) {
48
- const stateData = decodeOAuthState(state || '');
49
- const relayUrl = stateData.relayUrl || '';
50
- const escapedCode = (code || '').replace(/['"\\]/g, '');
51
- const escapedState = (state || '').replace(/['"\\]/g, '');
52
- const escapedError = (error || '').replace(/['"\\]/g, '');
53
- const escapedRelay = relayUrl.replace(/['"\\]/g, '');
54
- return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
55
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
56
- <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
57
- <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
58
- <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
59
- <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
60
- </div>
61
- <script>
62
- (function() {
63
- var code = '${escapedCode}';
64
- var state = '${escapedState}';
65
- var error = '${escapedError}';
66
- var relayUrl = '${escapedRelay}';
67
- function show(icon, title, msg, color) {
68
- document.getElementById('spinner').textContent = icon;
69
- document.getElementById('spinner').style.color = color;
70
- document.getElementById('title').textContent = title;
71
- document.getElementById('msg').textContent = msg;
72
- }
73
- if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
74
- if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
75
- if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
76
- fetch(relayUrl, {
77
- method: 'POST',
78
- headers: { 'Content-Type': 'application/json' },
79
- body: JSON.stringify({ code: code, state: state })
80
- }).then(function(r) { return r.json(); }).then(function(data) {
81
- if (data.success) {
82
- show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
83
- } else {
84
- show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
85
- }
86
- }).catch(function(e) {
87
- show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
88
- });
89
- })();
90
- </script>
91
- </body></html>`;
92
- }
@@ -1,199 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import crypto from 'crypto';
5
- import { execSync } from 'child_process';
6
- import { OAuth2Client } from 'google-auth-library';
7
- import { findCommand } from './agent-discovery.js';
8
- import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState, oauthResultPage, oauthRelayPage } from './oauth-common.js';
9
-
10
- const GEMINI_SCOPES = [
11
- 'https://www.googleapis.com/auth/cloud-platform',
12
- 'https://www.googleapis.com/auth/userinfo.email',
13
- 'https://www.googleapis.com/auth/userinfo.profile',
14
- ];
15
- const GEMINI_DIR = path.join(os.homedir(), '.gemini');
16
- const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
17
- const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
18
- let geminiOAuthState = { status: 'idle', error: null, email: null };
19
- let geminiOAuthPending = null;
20
- function extractOAuthFromFile(oauth2Path) {
21
- try {
22
- const src = fs.readFileSync(oauth2Path, 'utf8');
23
- const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
24
- const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
25
- if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
26
- } catch {}
27
- return null;
28
- }
29
-
30
- export function getGeminiOAuthCreds(rootDir) {
31
- if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
32
- return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
33
- }
34
- const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
35
- try {
36
- const geminiPath = findCommand('gemini', rootDir);
37
- if (geminiPath) {
38
- const realPath = fs.realpathSync(geminiPath);
39
- const pkgRoot = path.resolve(path.dirname(realPath), '..');
40
- const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
41
- if (result) return result;
42
- }
43
- } catch (e) {
44
- console.error('[gemini-oauth] gemini lookup failed:', e.message);
45
- }
46
- try {
47
- const npmCacheDirs = new Set();
48
- const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
49
- addDir(path.join(os.homedir(), '.npm'));
50
- addDir(path.join(os.homedir(), '.cache', '.npm'));
51
- if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
52
- if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
53
- try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
54
- for (const cacheDir of npmCacheDirs) {
55
- if (!fs.existsSync(cacheDir)) continue;
56
- for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
57
- const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
58
- if (result) return result;
59
- }
60
- }
61
- } catch (e) {
62
- console.error('[gemini-oauth] npm cache scan failed:', e.message);
63
- }
64
- console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
65
- return null;
66
- }
67
-
68
- function saveGeminiCredentials(tokens, email) {
69
- if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
70
- fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
71
- try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
72
- let accounts = { active: null, old: [] };
73
- try {
74
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
75
- accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
76
- }
77
- } catch (_) {}
78
- if (email) {
79
- if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
80
- accounts.old.push(accounts.active);
81
- }
82
- accounts.active = email;
83
- }
84
- fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
85
- }
86
-
87
- export async function startGeminiOAuth(req, { PORT, BASE_URL, rootDir }) {
88
- const creds = getGeminiOAuthCreds(rootDir);
89
- if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
90
- const useCustomClient = !!creds.custom;
91
- const remote = isRemoteRequest(req);
92
- let redirectUri;
93
- if (useCustomClient && req) {
94
- redirectUri = `${buildBaseUrl(req, PORT)}${BASE_URL}/oauth2callback`;
95
- } else {
96
- redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
97
- }
98
- const csrfToken = crypto.randomBytes(32).toString('hex');
99
- const relayUrl = req ? `${buildBaseUrl(req, PORT)}${BASE_URL}/api/gemini-oauth/relay` : null;
100
- const state = encodeOAuthState(csrfToken, relayUrl);
101
- const client = new OAuth2Client({ clientId: creds.clientId, clientSecret: creds.clientSecret });
102
- const authUrl = client.generateAuthUrl({ redirect_uri: redirectUri, access_type: 'offline', scope: GEMINI_SCOPES, state });
103
- const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
104
- geminiOAuthPending = { client, redirectUri, state: csrfToken };
105
- geminiOAuthState = { status: 'pending', error: null, email: null };
106
- if (geminiOAuthPending._timeout) clearTimeout(geminiOAuthPending._timeout);
107
- geminiOAuthPending._timeout = setTimeout(() => {
108
- if (geminiOAuthState.status === 'pending') {
109
- geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
110
- geminiOAuthPending = null;
111
- }
112
- }, 5 * 60 * 1000);
113
- return { authUrl, mode };
114
- }
115
-
116
- export async function exchangeGeminiOAuthCode(code, stateParam) {
117
- if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
118
- if (geminiOAuthPending._timeout) { clearTimeout(geminiOAuthPending._timeout); geminiOAuthPending._timeout = null; }
119
- const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
120
- const { csrfToken } = decodeOAuthState(stateParam);
121
- if (csrfToken !== expectedCsrf) {
122
- geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
123
- geminiOAuthPending = null;
124
- throw new Error('State mismatch - possible CSRF attack.');
125
- }
126
- if (!code) {
127
- geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
128
- geminiOAuthPending = null;
129
- throw new Error('No authorization code received.');
130
- }
131
- const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
132
- client.setCredentials(tokens);
133
- let email = '';
134
- try {
135
- const { token } = await client.getAccessToken();
136
- if (token) {
137
- const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${token}` } });
138
- if (resp.ok) { const info = await resp.json(); email = info.email || ''; }
139
- }
140
- } catch (_) {}
141
- saveGeminiCredentials(tokens, email);
142
- geminiOAuthState = { status: 'success', error: null, email };
143
- geminiOAuthPending = null;
144
- return email;
145
- }
146
-
147
- export async function handleGeminiOAuthCallback(req, res, PORT) {
148
- const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
149
- const code = reqUrl.searchParams.get('code');
150
- const state = reqUrl.searchParams.get('state');
151
- const error = reqUrl.searchParams.get('error');
152
- const errorDesc = reqUrl.searchParams.get('error_description');
153
- if (error) {
154
- const desc = errorDesc || error;
155
- geminiOAuthState = { status: 'error', error: desc, email: null };
156
- geminiOAuthPending = null;
157
- }
158
- const stateData = decodeOAuthState(state || '');
159
- if (stateData.relayUrl) {
160
- res.writeHead(200, { 'Content-Type': 'text/html' });
161
- res.end(oauthRelayPage(code, state, errorDesc || error));
162
- return;
163
- }
164
- if (!geminiOAuthPending) {
165
- res.writeHead(200, { 'Content-Type': 'text/html' });
166
- res.end(oauthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
167
- return;
168
- }
169
- try {
170
- if (error) throw new Error(errorDesc || error);
171
- const email = await exchangeGeminiOAuthCode(code, state);
172
- res.writeHead(200, { 'Content-Type': 'text/html' });
173
- res.end(oauthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
174
- } catch (e) {
175
- res.writeHead(200, { 'Content-Type': 'text/html' });
176
- res.end(oauthResultPage('Authentication Failed', e.message, false));
177
- }
178
- }
179
-
180
- export function getGeminiOAuthStatus() {
181
- try {
182
- if (fs.existsSync(GEMINI_OAUTH_FILE)) {
183
- const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
184
- if (creds.refresh_token || creds.access_token) {
185
- let email = '';
186
- try {
187
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
188
- const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
189
- email = accts.active || '';
190
- }
191
- } catch (_) {}
192
- return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
193
- }
194
- }
195
- } catch (_) {}
196
- return null;
197
- }
198
- export function getGeminiOAuthState() { return geminiOAuthState; }
199
- export function getGeminiOAuthPending() { return geminiOAuthPending; }
@@ -1,132 +0,0 @@
1
- // Auth plugin - OAuth2, provider config, agentauth integration
2
-
3
- import http from 'http';
4
-
5
- export default {
6
- name: 'auth',
7
- version: '1.0.0',
8
- dependencies: ['database'],
9
-
10
- async init(config, plugins) {
11
- const db = plugins.get('database');
12
- const providerConfigs = new Map();
13
- const oauthClients = new Map();
14
- const sessions = new Map();
15
-
16
- // Detect provider configs on startup
17
- const detectProviders = () => {
18
- const providers = [
19
- { id: 'anthropic', name: 'Anthropic', configured: false },
20
- { id: 'google', name: 'Google', configured: false },
21
- { id: 'github', name: 'GitHub', configured: false },
22
- ];
23
- providers.forEach(p => providerConfigs.set(p.id, p));
24
- return providers;
25
- };
26
-
27
- detectProviders();
28
-
29
- // Agentauth integration
30
- const agentauthStart = async (provider, scopes) => {
31
- return new Promise((resolve, reject) => {
32
- const options = {
33
- hostname: 'localhost',
34
- port: 8765,
35
- path: '/auth/start',
36
- method: 'POST',
37
- headers: { 'Content-Type': 'application/json' },
38
- };
39
-
40
- const req = http.request(options, (res) => {
41
- let data = '';
42
- res.on('data', chunk => data += chunk);
43
- res.on('end', () => {
44
- try {
45
- resolve(JSON.parse(data));
46
- } catch (e) {
47
- reject(new Error('Agentauth unavailable'));
48
- }
49
- });
50
- });
51
-
52
- req.on('error', () => reject(new Error('Agentauth service not running')));
53
- req.write(JSON.stringify({ provider, scopes }));
54
- req.end();
55
- });
56
- };
57
-
58
- return {
59
- routes: [
60
- {
61
- method: 'GET',
62
- path: '/api/auth/status',
63
- handler: (req, res) => {
64
- res.json({ authenticated: sessions.size > 0, sessions: Array.from(sessions.keys()) });
65
- },
66
- },
67
- {
68
- method: 'GET',
69
- path: '/api/auth/configs',
70
- handler: (req, res) => {
71
- const masked = Array.from(providerConfigs.values()).map(p => ({
72
- id: p.id,
73
- name: p.name,
74
- configured: p.configured,
75
- }));
76
- res.json({ providers: masked });
77
- },
78
- },
79
- {
80
- method: 'POST',
81
- path: '/api/auth/callback',
82
- handler: (req, res) => {
83
- const { code } = req.body;
84
- res.json({ success: true, code });
85
- },
86
- },
87
- {
88
- method: 'POST',
89
- path: '/api/auth/logout',
90
- handler: (req, res) => {
91
- sessions.clear();
92
- res.json({ success: true });
93
- },
94
- },
95
- {
96
- method: 'POST',
97
- path: '/api/auth/agentauth-start',
98
- handler: async (req, res) => {
99
- const { provider, scopes } = req.body;
100
- try {
101
- const result = await agentauthStart(provider, scopes);
102
- res.json(result);
103
- } catch (e) {
104
- res.status(503).json({ error: e.message });
105
- }
106
- },
107
- },
108
- {
109
- method: 'GET',
110
- path: '/api/auth/agentauth-status',
111
- handler: async (req, res) => {
112
- const { code } = req.query;
113
- res.json({ status: 'polling-not-implemented' });
114
- },
115
- },
116
- ],
117
- wsHandlers: {},
118
- api: {
119
- getProviders: () => Array.from(providerConfigs.values()),
120
- },
121
- stop: async () => {
122
- sessions.clear();
123
- },
124
- };
125
- },
126
-
127
- async reload(state) {
128
- return state;
129
- },
130
-
131
- async stop() {},
132
- };
@@ -1,72 +0,0 @@
1
- // Speech plugin - STT/TTS model management, download on startup
2
-
3
- import path from 'path';
4
-
5
- export default {
6
- name: 'speech',
7
- version: '1.0.0',
8
- dependencies: ['database'],
9
-
10
- async init(config, plugins) {
11
- const modelsDir = path.join(process.env.HOME || '/tmp', '.gmgui', 'models');
12
- let modelsReady = false;
13
- const downloadProgress = new Map();
14
- let voiceList = [];
15
- const modelCache = new Map();
16
-
17
- // Models would be downloaded here on startup
18
- modelsReady = true;
19
- voiceList = ['en-US', 'en-GB', 'es-ES', 'fr-FR', 'de-DE'];
20
-
21
- return {
22
- routes: [
23
- {
24
- method: 'POST',
25
- path: '/api/stt',
26
- handler: async (req, res) => {
27
- if (!modelsReady) return res.status(503).json({ error: 'Models loading' });
28
- res.json({ text: 'transcription-not-implemented' });
29
- },
30
- },
31
- {
32
- method: 'POST',
33
- path: '/api/tts',
34
- handler: async (req, res) => {
35
- if (!modelsReady) return res.status(503).json({ error: 'Models loading' });
36
- const { text } = req.body;
37
- res.json({ audio: 'base64-audio-not-implemented' });
38
- },
39
- },
40
- {
41
- method: 'GET',
42
- path: '/api/speech-status',
43
- handler: (req, res) => {
44
- res.json({ ready: modelsReady, progress: Object.fromEntries(downloadProgress) });
45
- },
46
- },
47
- {
48
- method: 'GET',
49
- path: '/api/voices',
50
- handler: (req, res) => {
51
- res.json({ voices: voiceList });
52
- },
53
- },
54
- ],
55
- wsHandlers: {
56
- model_download_progress: (data, clients) => {},
57
- voice_list: (data, clients) => {},
58
- },
59
- api: {
60
- getVoices: () => voiceList,
61
- isReady: () => modelsReady,
62
- },
63
- stop: async () => {},
64
- };
65
- },
66
-
67
- async reload(state) {
68
- return state;
69
- },
70
-
71
- async stop() {},
72
- };
@@ -1,120 +0,0 @@
1
- // Tools plugin - tool detection, versioning, install/update handlers
2
-
3
- import { spawnSync } from 'child_process';
4
- import os from 'os';
5
-
6
- const isWindows = os.platform() === 'win32';
7
- const bunCmd = isWindows ? 'bun.cmd' : 'bun';
8
-
9
- export default {
10
- name: 'tools',
11
- version: '1.0.0',
12
- dependencies: ['database'],
13
-
14
- async init(config, plugins) {
15
- const db = plugins.get('database');
16
- const toolCache = new Map();
17
- const installInProgress = new Set();
18
- const operationQueue = [];
19
-
20
- const detectTools = async () => {
21
- const tools = [
22
- { id: 'gm-cc', pkg: 'gm-cc', name: 'GM Claude' },
23
- { id: 'gm-oc', pkg: 'gm-oc', name: 'GM OpenCode' },
24
- { id: 'gm-gc', pkg: 'gm-gc', name: 'GM Gemini' },
25
- { id: 'gm-kilo', pkg: 'gm-kilo', name: 'GM Kilo' },
26
- { id: 'gm-codex', pkg: 'gm-codex', name: 'GM Codex' },
27
- { id: 'cli-claude', pkg: '@anthropic-ai/claude-code', name: 'Claude Code' },
28
- { id: 'cli-opencode', pkg: 'opencode-ai', name: 'OpenCode' },
29
- { id: 'cli-gemini', pkg: '@google/gemini-cli', name: 'Gemini CLI' },
30
- { id: 'cli-kilo', pkg: '@kilocode/cli', name: 'Kilo Code' },
31
- { id: 'cli-codex', pkg: '@openai/codex', name: 'Codex CLI' },
32
- ];
33
-
34
- for (const tool of tools) {
35
- const result = spawnSync(bunCmd, ['x', tool.pkg, '--version'], { stdio: 'ignore', timeout: 10000 });
36
- tool.installed = result.status === 0;
37
- toolCache.set(tool.id, tool);
38
- }
39
- return tools;
40
- };
41
-
42
- await detectTools();
43
-
44
- return {
45
- routes: [
46
- {
47
- method: 'GET',
48
- path: '/api/tools',
49
- handler: async (req, res) => {
50
- res.json({ tools: Array.from(toolCache.values()) });
51
- },
52
- },
53
- {
54
- method: 'GET',
55
- path: '/api/tools/:id/status',
56
- handler: async (req, res) => {
57
- const tool = toolCache.get(req.params.id);
58
- res.json(tool || { error: 'Tool not found' });
59
- },
60
- },
61
- {
62
- method: 'POST',
63
- path: '/api/tools/:id/install',
64
- handler: async (req, res) => {
65
- res.json({ success: true });
66
- // Async install in background
67
- },
68
- },
69
- {
70
- method: 'POST',
71
- path: '/api/tools/:id/update',
72
- handler: async (req, res) => {
73
- res.json({ success: true });
74
- // Async update in background
75
- },
76
- },
77
- {
78
- method: 'POST',
79
- path: '/api/tools/update',
80
- handler: async (req, res) => {
81
- res.json({ success: true });
82
- // Batch async update
83
- },
84
- },
85
- {
86
- method: 'GET',
87
- path: '/api/tools/:id/history',
88
- handler: async (req, res) => {
89
- res.json({ history: [] });
90
- },
91
- },
92
- {
93
- method: 'POST',
94
- path: '/api/tools/refresh-all',
95
- handler: async (req, res) => {
96
- await detectTools();
97
- res.json({ success: true });
98
- },
99
- },
100
- ],
101
- wsHandlers: {
102
- tool_install_complete: (data, clients) => {},
103
- tool_install_failed: (data, clients) => {},
104
- tool_update_complete: (data, clients) => {},
105
- tool_update_failed: (data, clients) => {},
106
- },
107
- api: {
108
- detectTools,
109
- getTools: () => Array.from(toolCache.values()),
110
- },
111
- stop: async () => {},
112
- };
113
- },
114
-
115
- async reload(state) {
116
- return state;
117
- },
118
-
119
- async stop() {},
120
- };