agentgui 1.0.759 → 1.0.761

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.
@@ -0,0 +1,185 @@
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', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
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 = toolManager.getAllTools();
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) {
74
+ const status = await toolManager.checkToolStatusAsync(tool.id);
75
+ if (status?.upgradeNeeded) queries.updateToolStatus(tool.id, { update_available: 1, latest_version: status.publishedVersion });
76
+ }
77
+ }
78
+ broadcastSync({ type: 'tools_refresh_complete', data: tools });
79
+ });
80
+ };
81
+
82
+ routes['_match'] = (method, pathOnly) => {
83
+ const key = `${method} ${pathOnly}`;
84
+ if (routes[key]) return routes[key];
85
+ let m;
86
+ if ((m = pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/))) return (req, res) => handleToolStatus(req, res, m[1]);
87
+ if (method === 'POST' && (m = pathOnly.match(/^\/api\/tools\/([^/]+)\/install$/))) return (req, res) => handleToolInstall(req, res, m[1]);
88
+ if (method === 'POST' && (m = pathOnly.match(/^\/api\/tools\/([^/]+)\/update$/))) return (req, res) => handleToolUpdate(req, res, m[1]);
89
+ if (method === 'GET' && (m = pathOnly.match(/^\/api\/tools\/([^/]+)\/history$/))) return (req, res) => handleToolHistory(req, res, m[1]);
90
+ return null;
91
+ };
92
+
93
+ async function handleToolStatus(req, res, toolId) {
94
+ const dbStatus = queries.getToolStatus(toolId);
95
+ const tmStatus = toolManager.checkToolStatus(toolId);
96
+ if (!tmStatus && !dbStatus) { sendJSON(req, res, 404, { error: 'Tool not found' }); return; }
97
+ const status = {
98
+ toolId, installed: tmStatus?.installed || (dbStatus?.status === 'installed'),
99
+ isUpToDate: tmStatus?.isUpToDate || false, upgradeNeeded: tmStatus?.upgradeNeeded || false,
100
+ status: dbStatus?.status || (tmStatus?.installed ? 'installed' : 'not_installed'),
101
+ installedVersion: dbStatus?.version || tmStatus?.installedVersion || null,
102
+ timestamp: Date.now(), error_message: dbStatus?.error_message || null
103
+ };
104
+ if (status.installed) { const updates = await toolManager.checkForUpdates(toolId); status.hasUpdate = updates.needsUpdate || false; }
105
+ sendJSON(req, res, 200, status);
106
+ }
107
+
108
+ async function handleToolInstall(req, res, toolId) {
109
+ const tool = toolManager.getToolConfig(toolId);
110
+ if (!tool) { sendJSON(req, res, 404, { error: 'Tool not found' }); return; }
111
+ const existing = queries.getToolStatus(toolId);
112
+ if (!existing) queries.insertToolInstallation(toolId, { status: 'not_installed' });
113
+ queries.updateToolStatus(toolId, { status: 'installing' });
114
+ sendJSON(req, res, 200, { success: true, installing: true, estimatedTime: 60000 });
115
+ let done = false;
116
+ const timeout = setTimeout(() => {
117
+ 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'); }
118
+ }, 360000);
119
+ toolManager.install(toolId, (msg) => broadcastSync({ type: 'tool_install_progress', toolId, data: msg })).then(async (result) => {
120
+ clearTimeout(timeout); if (done) return; done = true;
121
+ if (result.success) {
122
+ const version = result.version || null;
123
+ queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
124
+ const freshStatus = await toolManager.checkToolStatusAsync(toolId);
125
+ broadcastSync({ type: 'tool_install_complete', toolId, data: { success: true, version, ...freshStatus } });
126
+ queries.addToolInstallHistory(toolId, 'install', 'success', null);
127
+ } else {
128
+ queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
129
+ broadcastSync({ type: 'tool_install_failed', toolId, data: result });
130
+ queries.addToolInstallHistory(toolId, 'install', 'failed', result.error);
131
+ }
132
+ }).catch((err) => {
133
+ clearTimeout(timeout); if (done) return; done = true;
134
+ const error = err?.message || 'Unknown error';
135
+ logError('toolInstall', err, { toolId });
136
+ queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
137
+ broadcastSync({ type: 'tool_install_failed', toolId, data: { success: false, error } });
138
+ queries.addToolInstallHistory(toolId, 'install', 'failed', error);
139
+ });
140
+ }
141
+
142
+ async function handleToolUpdate(req, res, toolId) {
143
+ const body = await parseBody(req);
144
+ const tool = toolManager.getToolConfig(toolId);
145
+ if (!tool) { sendJSON(req, res, 404, { error: 'Tool not found' }); return; }
146
+ const current = await toolManager.checkToolStatusAsync(toolId);
147
+ if (!current || !current.installed) { sendJSON(req, res, 400, { error: 'Tool not installed' }); return; }
148
+ queries.updateToolStatus(toolId, { status: 'updating' });
149
+ sendJSON(req, res, 200, { success: true, updating: true });
150
+ let done = false;
151
+ const timeout = setTimeout(() => {
152
+ 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'); }
153
+ }, 360000);
154
+ toolManager.update(toolId, (msg) => broadcastSync({ type: 'tool_update_progress', toolId, data: msg })).then(async (result) => {
155
+ clearTimeout(timeout); if (done) return; done = true;
156
+ if (result.success) {
157
+ const version = result.version || null;
158
+ queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
159
+ const freshStatus = await toolManager.checkToolStatusAsync(toolId);
160
+ broadcastSync({ type: 'tool_update_complete', toolId, data: { success: true, version, ...freshStatus } });
161
+ queries.addToolInstallHistory(toolId, 'update', 'success', null);
162
+ } else {
163
+ queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
164
+ broadcastSync({ type: 'tool_update_failed', toolId, data: result });
165
+ queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
166
+ }
167
+ }).catch((err) => {
168
+ clearTimeout(timeout); if (done) return; done = true;
169
+ const error = err?.message || 'Unknown error';
170
+ logError('toolUpdate', err, { toolId });
171
+ queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
172
+ broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error } });
173
+ queries.addToolInstallHistory(toolId, 'update', 'failed', error);
174
+ });
175
+ }
176
+
177
+ async function handleToolHistory(req, res, toolId) {
178
+ const url = new URL(req.url, 'http://localhost');
179
+ const limit = Math.min(parseInt(url.searchParams.get('limit')) || 20, 100);
180
+ const offset = parseInt(url.searchParams.get('offset')) || 0;
181
+ sendJSON(req, res, 200, { history: queries.getToolInstallHistory(toolId, limit, offset) });
182
+ }
183
+
184
+ return routes;
185
+ }
@@ -0,0 +1,106 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { execSync } from 'child_process';
5
+
6
+ export function register(deps) {
7
+ const { sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION } = deps;
8
+ const routes = {};
9
+
10
+ routes['GET /api/import/claude-code'] = async (req, res) => {
11
+ const result = queries.importClaudeCodeConversations();
12
+ sendJSON(req, res, 200, { imported: result });
13
+ };
14
+
15
+ routes['GET /api/discover/claude-code'] = async (req, res) => {
16
+ const discovered = queries.discoverClaudeCodeConversations();
17
+ sendJSON(req, res, 200, { discovered });
18
+ };
19
+
20
+ routes['GET /api/home'] = async (req, res) => {
21
+ sendJSON(req, res, 200, { home: os.homedir(), cwd: STARTUP_CWD });
22
+ };
23
+
24
+ routes['GET /api/version'] = async (req, res) => {
25
+ sendJSON(req, res, 200, { version: PKG_VERSION });
26
+ };
27
+
28
+ routes['POST /api/clone'] = async (req, res) => {
29
+ const body = await parseBody(req);
30
+ const repo = (body.repo || '').trim();
31
+ if (!repo || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo)) {
32
+ sendJSON(req, res, 400, { error: 'Invalid repo format. Use org/repo or user/repo' });
33
+ return;
34
+ }
35
+ const cloneDir = STARTUP_CWD || os.homedir();
36
+ const repoName = repo.split('/')[1];
37
+ const targetPath = path.join(cloneDir, repoName);
38
+ if (fs.existsSync(targetPath)) {
39
+ sendJSON(req, res, 409, { error: `Directory already exists: ${repoName}`, path: targetPath });
40
+ return;
41
+ }
42
+ try {
43
+ const isWindows = os.platform() === 'win32';
44
+ execSync('git clone https://github.com/' + repo + '.git', {
45
+ cwd: cloneDir, encoding: 'utf-8', timeout: 120000,
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
48
+ shell: isWindows
49
+ });
50
+ sendJSON(req, res, 200, { ok: true, repo, path: targetPath, name: repoName });
51
+ } catch (err) {
52
+ sendJSON(req, res, 500, { error: (err.stderr || err.message || 'Clone failed').trim() });
53
+ }
54
+ };
55
+
56
+ routes['POST /api/folders'] = async (req, res) => {
57
+ const body = await parseBody(req);
58
+ const folderPath = body.path || STARTUP_CWD;
59
+ try {
60
+ const expandedPath = folderPath.startsWith('~') ? folderPath.replace('~', os.homedir()) : folderPath;
61
+ const entries = fs.readdirSync(expandedPath, { withFileTypes: true });
62
+ const folders = entries
63
+ .filter(e => e.isDirectory() && !e.name.startsWith('.'))
64
+ .map(e => ({ name: e.name }))
65
+ .sort((a, b) => a.name.localeCompare(b.name));
66
+ sendJSON(req, res, 200, { folders });
67
+ } catch (err) {
68
+ sendJSON(req, res, 400, { error: err.message });
69
+ }
70
+ };
71
+
72
+ routes['GET /api/git/check-remote-ownership'] = async (req, res) => {
73
+ try {
74
+ const isWindows = os.platform() === 'win32';
75
+ const result = execSync('git remote get-url origin' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
76
+ const remoteUrl = result.trim();
77
+ const statusResult = execSync('git status --porcelain' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
78
+ const hasChanges = statusResult.trim().length > 0;
79
+ const unpushedResult = execSync('git rev-list --count --not --remotes' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
80
+ const hasUnpushed = parseInt(unpushedResult.trim() || '0', 10) > 0;
81
+ const ownsRemote = !remoteUrl.includes('github.com/') || remoteUrl.includes(process.env.GITHUB_USER || '');
82
+ sendJSON(req, res, 200, { ownsRemote, hasChanges, hasUnpushed, remoteUrl });
83
+ } catch {
84
+ sendJSON(req, res, 200, { ownsRemote: false, hasChanges: false, hasUnpushed: false, remoteUrl: '' });
85
+ }
86
+ };
87
+
88
+ routes['POST /api/git/push'] = async (req, res) => {
89
+ try {
90
+ const isWindows = os.platform() === 'win32';
91
+ const gitCommand = isWindows
92
+ ? 'git add -A & git commit -m "Auto-commit" & git push'
93
+ : 'git add -A && git commit -m "Auto-commit" && git push';
94
+ execSync(gitCommand, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
95
+ sendJSON(req, res, 200, { success: true });
96
+ } catch (err) {
97
+ sendJSON(req, res, 500, { error: err.message });
98
+ }
99
+ };
100
+
101
+ routes['_match'] = (method, pathOnly) => {
102
+ return routes[`${method} ${pathOnly}`] || null;
103
+ };
104
+
105
+ return routes;
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.759",
3
+ "version": "1.0.761",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/server.js CHANGED
@@ -21,6 +21,8 @@ import { getGeminiOAuthCreds, startGeminiOAuth, exchangeGeminiOAuthCode, handleG
21
21
  import { initSpeechManager, getSpeech, ensurePocketTtsSetup, voiceCacheManager, modelDownloadState, broadcastModelProgress, ensureModelsDownloaded, eagerTTS } from './lib/speech-manager.js';
22
22
  import { register as registerSpeechRoutes } from './lib/routes-speech.js';
23
23
  import { register as registerOAuthRoutes } from './lib/routes-oauth.js';
24
+ import { register as registerUtilRoutes } from './lib/routes-util.js';
25
+ import { register as registerToolRoutes } from './lib/routes-tools.js';
24
26
  import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
25
27
  import { WSOptimizer } from './lib/ws-optimizer.js';
26
28
  import { WsRouter } from './lib/ws-protocol.js';
@@ -1277,264 +1279,8 @@ const server = http.createServer(async (req, res) => {
1277
1279
  return;
1278
1280
  }
1279
1281
 
1280
- if (pathOnly === '/api/tools' && req.method === 'GET') {
1281
- console.log('[TOOLS-API] Handling GET /api/tools');
1282
- try {
1283
- // Return immediately with cached data (non-blocking) - skip network version checks
1284
- const tools = await Promise.race([
1285
- toolManager.getAllToolsAsync(true), // skipPublishedVersion=true for fast response
1286
- new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
1287
- ]);
1288
- const result = tools.map((t) => ({
1289
- id: t.id,
1290
- name: t.name,
1291
- pkg: t.pkg,
1292
- category: t.category || 'plugin',
1293
- installed: t.installed,
1294
- status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
1295
- isUpToDate: t.isUpToDate,
1296
- upgradeNeeded: t.upgradeNeeded,
1297
- hasUpdate: t.upgradeNeeded && t.installed,
1298
- installedVersion: t.installedVersion,
1299
- publishedVersion: t.publishedVersion,
1300
- machineState: toolInstallMachine.getState(t.id),
1301
- }));
1302
- sendJSON(req, res, 200, { tools: result });
1303
- } catch (err) {
1304
- console.log('[TOOLS-API] Error getting tools, returning cached status:', err.message);
1305
- const tools = toolManager.getAllToolsSync().map((t) => ({
1306
- id: t.id,
1307
- name: t.name,
1308
- pkg: t.pkg,
1309
- category: t.category || 'plugin',
1310
- installed: t.installed || false,
1311
- status: (t.installed) ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
1312
- isUpToDate: t.isUpToDate || false,
1313
- upgradeNeeded: t.upgradeNeeded || false,
1314
- hasUpdate: (t.upgradeNeeded && t.installed) || false,
1315
- installedVersion: t.installedVersion || null,
1316
- publishedVersion: t.publishedVersion || null,
1317
- machineState: toolInstallMachine.getState(t.id),
1318
- }));
1319
- sendJSON(req, res, 200, { tools });
1320
- }
1321
- return;
1322
- }
1323
-
1324
- if (pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/)) {
1325
- const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/)[1];
1326
- const dbStatus = queries.getToolStatus(toolId);
1327
- const tmStatus = toolManager.checkToolStatus(toolId);
1328
- if (!tmStatus && !dbStatus) {
1329
- sendJSON(req, res, 404, { error: 'Tool not found' });
1330
- return;
1331
- }
1332
-
1333
- // Merge database status with tool manager status
1334
- const status = {
1335
- toolId,
1336
- installed: tmStatus?.installed || (dbStatus?.status === 'installed'),
1337
- isUpToDate: tmStatus?.isUpToDate || false,
1338
- upgradeNeeded: tmStatus?.upgradeNeeded || false,
1339
- status: dbStatus?.status || (tmStatus?.installed ? 'installed' : 'not_installed'),
1340
- installedVersion: dbStatus?.version || tmStatus?.installedVersion || null,
1341
- timestamp: Date.now(),
1342
- error_message: dbStatus?.error_message || null
1343
- };
1344
-
1345
- if (status.installed) {
1346
- const updates = await toolManager.checkForUpdates(toolId);
1347
- status.hasUpdate = updates.needsUpdate || false;
1348
- }
1349
- sendJSON(req, res, 200, status);
1350
- return;
1351
- }
1352
-
1353
- if (pathOnly.match(/^\/api\/tools\/([^/]+)\/install$/) && req.method === 'POST') {
1354
- const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/install$/)[1];
1355
- const tool = toolManager.getToolConfig(toolId);
1356
- if (!tool) {
1357
- sendJSON(req, res, 404, { error: 'Tool not found' });
1358
- return;
1359
- }
1360
- const existing = queries.getToolStatus(toolId);
1361
- if (!existing) {
1362
- queries.insertToolInstallation(toolId, { status: 'not_installed' });
1363
- }
1364
- queries.updateToolStatus(toolId, { status: 'installing' });
1365
- sendJSON(req, res, 200, { success: true, installing: true, estimatedTime: 60000 });
1366
-
1367
- let installCompleted = false;
1368
- const installTimeout = setTimeout(() => {
1369
- if (!installCompleted) {
1370
- installCompleted = true;
1371
- queries.updateToolStatus(toolId, { status: 'failed', error_message: 'Install timeout after 6 minutes' });
1372
- broadcastSync({ type: 'tool_install_failed', toolId, data: { success: false, error: 'Install timeout after 6 minutes' } });
1373
- queries.addToolInstallHistory(toolId, 'install', 'failed', 'Install timeout after 6 minutes');
1374
- }
1375
- }, 360000);
1376
-
1377
- toolManager.install(toolId, (msg) => {
1378
- broadcastSync({ type: 'tool_install_progress', toolId, data: msg });
1379
- }).then(async (result) => {
1380
- clearTimeout(installTimeout);
1381
- if (installCompleted) return;
1382
- installCompleted = true;
1383
- if (result.success) {
1384
- const version = result.version || null;
1385
- console.log(`[TOOLS-API] Install succeeded for ${toolId}, version: ${version}`);
1386
- queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
1387
- const freshStatus = await toolManager.checkToolStatusAsync(toolId);
1388
- console.log(`[TOOLS-API] Fresh status after install for ${toolId}:`, JSON.stringify(freshStatus));
1389
- broadcastSync({ type: 'tool_install_complete', toolId, data: { success: true, version, ...freshStatus } });
1390
- queries.addToolInstallHistory(toolId, 'install', 'success', null);
1391
- } else {
1392
- console.error(`[TOOLS-API] Install failed for ${toolId}:`, result.error);
1393
- queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
1394
- broadcastSync({ type: 'tool_install_failed', toolId, data: result });
1395
- queries.addToolInstallHistory(toolId, 'install', 'failed', result.error);
1396
- }
1397
- }).catch((err) => {
1398
- clearTimeout(installTimeout);
1399
- if (installCompleted) return;
1400
- installCompleted = true;
1401
- const error = err?.message || 'Unknown error';
1402
- console.error(`[TOOLS-API] Install error for ${toolId}:`, error);
1403
- logError('toolInstall', err, { toolId });
1404
- queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
1405
- broadcastSync({ type: 'tool_install_failed', toolId, data: { success: false, error } });
1406
- queries.addToolInstallHistory(toolId, 'install', 'failed', error);
1407
- });
1408
- return;
1409
- }
1410
-
1411
- if (pathOnly.match(/^\/api\/tools\/([^/]+)\/update$/) && req.method === 'POST') {
1412
- const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/update$/)[1];
1413
- const body = await parseBody(req);
1414
- const tool = toolManager.getToolConfig(toolId);
1415
- if (!tool) {
1416
- sendJSON(req, res, 404, { error: 'Tool not found' });
1417
- return;
1418
- }
1419
- const current = await toolManager.checkToolStatusAsync(toolId);
1420
- if (!current || !current.installed) {
1421
- sendJSON(req, res, 400, { error: 'Tool not installed' });
1422
- return;
1423
- }
1424
- queries.updateToolStatus(toolId, { status: 'updating' });
1425
- sendJSON(req, res, 200, { success: true, updating: true });
1426
-
1427
- let updateCompleted = false;
1428
- const updateTimeout = setTimeout(() => {
1429
- if (!updateCompleted) {
1430
- updateCompleted = true;
1431
- queries.updateToolStatus(toolId, { status: 'failed', error_message: 'Update timeout after 6 minutes' });
1432
- broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error: 'Update timeout after 6 minutes' } });
1433
- queries.addToolInstallHistory(toolId, 'update', 'failed', 'Update timeout after 6 minutes');
1434
- }
1435
- }, 360000);
1436
-
1437
- toolManager.update(toolId, (msg) => {
1438
- broadcastSync({ type: 'tool_update_progress', toolId, data: msg });
1439
- }).then(async (result) => {
1440
- clearTimeout(updateTimeout);
1441
- if (updateCompleted) return;
1442
- updateCompleted = true;
1443
- if (result.success) {
1444
- const version = result.version || null;
1445
- console.log(`[TOOLS-API] Update succeeded for ${toolId}, version: ${version}`);
1446
- queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
1447
- const freshStatus = await toolManager.checkToolStatusAsync(toolId);
1448
- console.log(`[TOOLS-API] Fresh status after update for ${toolId}:`, JSON.stringify(freshStatus));
1449
- broadcastSync({ type: 'tool_update_complete', toolId, data: { success: true, version, ...freshStatus } });
1450
- queries.addToolInstallHistory(toolId, 'update', 'success', null);
1451
- } else {
1452
- console.error(`[TOOLS-API] Update failed for ${toolId}:`, result.error);
1453
- queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
1454
- broadcastSync({ type: 'tool_update_failed', toolId, data: result });
1455
- queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
1456
- }
1457
- }).catch((err) => {
1458
- clearTimeout(updateTimeout);
1459
- if (updateCompleted) return;
1460
- updateCompleted = true;
1461
- const error = err?.message || 'Unknown error';
1462
- console.error(`[TOOLS-API] Update error for ${toolId}:`, error);
1463
- logError('toolUpdate', err, { toolId });
1464
- queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
1465
- broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error } });
1466
- queries.addToolInstallHistory(toolId, 'update', 'failed', error);
1467
- });
1468
- return;
1469
- }
1470
-
1471
- if (pathOnly.match(/^\/api\/tools\/([^/]+)\/history$/) && req.method === 'GET') {
1472
- const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/history$/)[1];
1473
- const url = new URL(req.url, 'http://localhost');
1474
- const limit = Math.min(parseInt(url.searchParams.get('limit')) || 20, 100);
1475
- const offset = parseInt(url.searchParams.get('offset')) || 0;
1476
- const history = queries.getToolInstallHistory(toolId, limit, offset);
1477
- sendJSON(req, res, 200, { history });
1478
- return;
1479
- }
1480
-
1481
- if (pathOnly === '/api/tools/update' && req.method === 'POST') {
1482
- const allToolIds = ['cli-claude', 'cli-opencode', 'cli-gemini', 'cli-kilo', 'cli-codex', 'gm-cc', 'gm-oc', 'gm-gc', 'gm-kilo'];
1483
- sendJSON(req, res, 200, { updating: true, toolCount: allToolIds.length });
1484
- broadcastSync({ type: 'tools_update_started', tools: allToolIds });
1485
- setImmediate(async () => {
1486
- const toolIds = allToolIds;
1487
- const results = {};
1488
- for (const toolId of toolIds) {
1489
- try {
1490
- const result = await toolManager.update(toolId, (msg) => {
1491
- broadcastSync({ type: 'tool_update_progress', toolId, data: msg });
1492
- });
1493
- results[toolId] = result;
1494
- if (result.success) {
1495
- const version = result.version || null;
1496
- queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
1497
- queries.addToolInstallHistory(toolId, 'update', 'success', null);
1498
- const freshStatus = await toolManager.checkToolStatusAsync(toolId);
1499
- broadcastSync({ type: 'tool_update_complete', toolId, data: { ...result, ...freshStatus } });
1500
- } else {
1501
- queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
1502
- queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
1503
- broadcastSync({ type: 'tool_update_failed', toolId, data: result });
1504
- }
1505
- } catch (err) {
1506
- queries.updateToolStatus(toolId, { status: 'failed', error_message: err.message });
1507
- queries.addToolInstallHistory(toolId, 'update', 'failed', err.message);
1508
- broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error: err.message } });
1509
- }
1510
- }
1511
- broadcastSync({ type: 'tools_update_complete', data: results });
1512
- });
1513
- return;
1514
- }
1515
-
1516
- if (pathOnly === '/api/tools/refresh-all' && req.method === 'POST') {
1517
- sendJSON(req, res, 200, { refreshing: true, toolCount: 4 });
1518
- broadcastSync({ type: 'tools_refresh_started' });
1519
- setImmediate(async () => {
1520
- const tools = toolManager.getAllTools();
1521
- for (const tool of tools) {
1522
- queries.updateToolStatus(tool.id, {
1523
- status: tool.installed ? 'installed' : 'not_installed',
1524
- version: tool.installedVersion,
1525
- last_check_at: Date.now()
1526
- });
1527
- if (tool.installed) {
1528
- const status = await toolManager.checkToolStatusAsync(tool.id);
1529
- if (status && status.upgradeNeeded) {
1530
- queries.updateToolStatus(tool.id, { update_available: 1, latest_version: status.publishedVersion });
1531
- }
1532
- }
1533
- }
1534
- broadcastSync({ type: 'tools_refresh_complete', data: tools });
1535
- });
1536
- return;
1537
- }
1282
+ const toolHandler = _toolRoutes._match(req.method, pathOnly);
1283
+ if (toolHandler) { await toolHandler(req, res); return; }
1538
1284
 
1539
1285
  if (pathOnly === '/api/ws-stats' && req.method === 'GET') {
1540
1286
  const stats = wsOptimizer.getStats();
@@ -2175,113 +1921,12 @@ const server = http.createServer(async (req, res) => {
2175
1921
  return;
2176
1922
  }
2177
1923
 
2178
- if (pathOnly === '/api/import/claude-code' && req.method === 'GET') {
2179
- const result = queries.importClaudeCodeConversations();
2180
- sendJSON(req, res, 200, { imported: result });
2181
- return;
2182
- }
2183
-
2184
- if (pathOnly === '/api/discover/claude-code' && req.method === 'GET') {
2185
- const discovered = queries.discoverClaudeCodeConversations();
2186
- sendJSON(req, res, 200, { discovered });
2187
- return;
2188
- }
2189
-
2190
- if (pathOnly === '/api/home' && req.method === 'GET') {
2191
- sendJSON(req, res, 200, { home: os.homedir(), cwd: STARTUP_CWD });
2192
- return;
2193
- }
2194
-
2195
- if (pathOnly === '/api/version' && req.method === 'GET') {
2196
- sendJSON(req, res, 200, { version: PKG_VERSION });
2197
- return;
2198
- }
2199
-
2200
1924
  const speechHandler = _speechRoutes._match(req.method, pathOnly);
2201
1925
  if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
2202
1926
 
2203
- if (pathOnly === '/api/clone' && req.method === 'POST') {
2204
- const body = await parseBody(req);
2205
- const repo = (body.repo || '').trim();
2206
- if (!repo || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo)) {
2207
- sendJSON(req, res, 400, { error: 'Invalid repo format. Use org/repo or user/repo' });
2208
- return;
2209
- }
2210
- const cloneDir = STARTUP_CWD || os.homedir();
2211
- const repoName = repo.split('/')[1];
2212
- const targetPath = path.join(cloneDir, repoName);
2213
- if (fs.existsSync(targetPath)) {
2214
- sendJSON(req, res, 409, { error: `Directory already exists: ${repoName}`, path: targetPath });
2215
- return;
2216
- }
2217
- try {
2218
- const isWindows = os.platform() === 'win32';
2219
- execSync('git clone https://github.com/' + repo + '.git', {
2220
- cwd: cloneDir,
2221
- encoding: 'utf-8',
2222
- timeout: 120000,
2223
- stdio: ['pipe', 'pipe', 'pipe'],
2224
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
2225
- shell: isWindows
2226
- });
2227
- sendJSON(req, res, 200, { ok: true, repo, path: targetPath, name: repoName });
2228
- } catch (err) {
2229
- const stderr = err.stderr || err.message || 'Clone failed';
2230
- sendJSON(req, res, 500, { error: stderr.trim() });
2231
- }
2232
- return;
2233
- }
2234
-
2235
- if (pathOnly === '/api/folders' && req.method === 'POST') {
2236
- const body = await parseBody(req);
2237
- const folderPath = body.path || STARTUP_CWD;
2238
- try {
2239
- const expandedPath = folderPath.startsWith('~') ?
2240
- folderPath.replace('~', os.homedir()) : folderPath;
2241
- const entries = fs.readdirSync(expandedPath, { withFileTypes: true });
2242
- const folders = entries
2243
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
2244
- .map(e => ({ name: e.name }))
2245
- .sort((a, b) => a.name.localeCompare(b.name));
2246
- sendJSON(req, res, 200, { folders });
2247
- } catch (err) {
2248
- sendJSON(req, res, 400, { error: err.message });
2249
- }
2250
- return;
2251
- }
1927
+ const utilHandler = _utilRoutes._match(req.method, pathOnly);
1928
+ if (utilHandler) { await utilHandler(req, res); return; }
2252
1929
 
2253
- if (pathOnly === '/api/git/check-remote-ownership' && req.method === 'GET') {
2254
- try {
2255
- const isWindows = os.platform() === 'win32';
2256
- const result = execSync('git remote get-url origin' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2257
- const remoteUrl = result.trim();
2258
- const statusResult = execSync('git status --porcelain' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2259
- const hasChanges = statusResult.trim().length > 0;
2260
- const unpushedResult = execSync('git rev-list --count --not --remotes' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2261
- const hasUnpushed = parseInt(unpushedResult.trim() || '0', 10) > 0;
2262
- const ownsRemote = !remoteUrl.includes('github.com/') || remoteUrl.includes(process.env.GITHUB_USER || '');
2263
- sendJSON(req, res, 200, { ownsRemote, hasChanges, hasUnpushed, remoteUrl });
2264
- } catch {
2265
- sendJSON(req, res, 200, { ownsRemote: false, hasChanges: false, hasUnpushed: false, remoteUrl: '' });
2266
- }
2267
- return;
2268
- }
2269
-
2270
- if (pathOnly === '/api/git/push' && req.method === 'POST') {
2271
- try {
2272
- const isWindows = os.platform() === 'win32';
2273
- const gitCommand = isWindows
2274
- ? 'git add -A & git commit -m "Auto-commit" & git push'
2275
- : 'git add -A && git commit -m "Auto-commit" && git push';
2276
- execSync(gitCommand, { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
2277
- sendJSON(req, res, 200, { success: true });
2278
- } catch (err) {
2279
- sendJSON(req, res, 500, { error: err.message });
2280
- }
2281
- return;
2282
- }
2283
-
2284
- // ============================================================
2285
1930
  // THREAD API ENDPOINTS (ACP v0.2.3)
2286
1931
  // ============================================================
2287
1932
 
@@ -3425,6 +3070,8 @@ const wsRouter = new WsRouter();
3425
3070
  initSpeechManager({ broadcastSync, syncClients, queries });
3426
3071
  const _speechRoutes = registerSpeechRoutes({ sendJSON, parseBody, broadcastSync, debugLog });
3427
3072
  const _oauthRoutes = registerOAuthRoutes({ sendJSON, parseBody, PORT, BASE_URL, rootDir });
3073
+ const _utilRoutes = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_CWD, PKG_VERSION });
3074
+ const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
3428
3075
 
3429
3076
  registerConvHandlers(wsRouter, {
3430
3077
  queries, activeExecutions, rateLimitState,