agentgui 1.0.917 → 1.0.919

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 (53) 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/acp-plugin.js +1 -2
  7. package/lib/plugins/database-plugin.js +1 -1
  8. package/lib/process-message.js +2 -2
  9. package/lib/provider-config.js +0 -16
  10. package/lib/recovery.js +2 -12
  11. package/lib/routes-agent-actions.js +2 -58
  12. package/lib/routes-debug.js +1 -7
  13. package/lib/routes-registry.js +5 -17
  14. package/lib/server-startup.js +1 -59
  15. package/lib/stream-event-handler.js +1 -3
  16. package/lib/ws-handlers-session2.js +2 -23
  17. package/lib/ws-handlers-util.js +106 -175
  18. package/lib/ws-legacy-handlers.js +1 -104
  19. package/lib/ws-setup.js +1 -3
  20. package/package.json +1 -15
  21. package/server.js +9 -26
  22. package/test.js +1 -21
  23. package/ecosystem.config.cjs +0 -22
  24. package/lib/checkpoint-manager.js +0 -182
  25. package/lib/db-queries-tools.js +0 -131
  26. package/lib/db-queries-voice.js +0 -85
  27. package/lib/oauth-codex.js +0 -164
  28. package/lib/oauth-common.js +0 -92
  29. package/lib/oauth-gemini.js +0 -199
  30. package/lib/plugins/auth-plugin.js +0 -132
  31. package/lib/plugins/speech-plugin.js +0 -72
  32. package/lib/plugins/tools-plugin.js +0 -120
  33. package/lib/pm2-manager.js +0 -170
  34. package/lib/routes-oauth.js +0 -105
  35. package/lib/routes-speech.js +0 -173
  36. package/lib/routes-tools.js +0 -184
  37. package/lib/speech-manager.js +0 -200
  38. package/lib/speech.js +0 -50
  39. package/lib/tool-install-machine.js +0 -157
  40. package/lib/tool-manager.js +0 -98
  41. package/lib/tool-provisioner.js +0 -98
  42. package/lib/tool-spawner.js +0 -163
  43. package/lib/tool-version-check.js +0 -196
  44. package/lib/tool-version-fetch.js +0 -68
  45. package/lib/ws-handlers-oauth.js +0 -76
  46. package/static/js/agent-auth-oauth.js +0 -159
  47. package/static/js/pm2-monitor.js +0 -151
  48. package/static/js/stt-handler.js +0 -147
  49. package/static/js/tool-install-machine.js +0 -155
  50. package/static/js/tools-manager-ui.js +0 -124
  51. package/static/js/tools-manager.js +0 -172
  52. package/static/js/voice-machine.js +0 -145
  53. 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
- };