@openagents-org/agent-launcher 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/bin/agent-connector.js +4 -0
- package/package.json +42 -0
- package/registry.json +457 -0
- package/src/adapters/base.js +327 -0
- package/src/adapters/claude.js +420 -0
- package/src/adapters/codex.js +260 -0
- package/src/adapters/index.js +39 -0
- package/src/adapters/openclaw.js +264 -0
- package/src/adapters/utils.js +83 -0
- package/src/adapters/workspace-prompt.js +293 -0
- package/src/autostart.js +178 -0
- package/src/cli.js +556 -0
- package/src/config.js +322 -0
- package/src/daemon.js +666 -0
- package/src/env.js +111 -0
- package/src/index.js +205 -0
- package/src/installer.js +588 -0
- package/src/paths.js +276 -0
- package/src/registry.js +197 -0
- package/src/tui.js +540 -0
- package/src/utils.js +93 -0
- package/src/workspace-client.js +338 -0
package/src/tui.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive TUI dashboard for OpenAgents — `agent-connector` or `agent-connector tui`
|
|
3
|
+
*
|
|
4
|
+
* Ported from Python Textual TUI (cli_tui.py). Uses blessed for terminal UI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const blessed = require('blessed');
|
|
10
|
+
const { AgentConnector } = require('./index');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
const { getExtraBinDirs } = require('./paths');
|
|
15
|
+
|
|
16
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
19
|
+
|
|
20
|
+
function getConnector() {
|
|
21
|
+
const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.openagents');
|
|
22
|
+
return new AgentConnector(configDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadAgentRows(connector) {
|
|
26
|
+
const config = connector.getConfig();
|
|
27
|
+
const agents = config.agents || [];
|
|
28
|
+
const status = connector.getDaemonStatus() || {};
|
|
29
|
+
const agentStatuses = status.agents || {};
|
|
30
|
+
const pid = connector.getDaemonPid();
|
|
31
|
+
|
|
32
|
+
return agents.map(agent => {
|
|
33
|
+
const info = agentStatuses[agent.name] || {};
|
|
34
|
+
const state = pid ? (info.state || 'stopped') : 'stopped';
|
|
35
|
+
let workspace = '';
|
|
36
|
+
if (agent.network) {
|
|
37
|
+
const nets = config.networks || [];
|
|
38
|
+
const net = nets.find(n => n.slug === agent.network || n.id === agent.network);
|
|
39
|
+
if (net) {
|
|
40
|
+
workspace = `${net.slug || net.id} (${net.name || ''})`;
|
|
41
|
+
} else {
|
|
42
|
+
workspace = agent.network;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { name: agent.name, type: agent.type, state, workspace, role: agent.role || 'worker' };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadCatalog(connector) {
|
|
50
|
+
const registry = connector.getRegistry();
|
|
51
|
+
const entries = registry.list();
|
|
52
|
+
// Check installed status
|
|
53
|
+
return entries.map(e => {
|
|
54
|
+
let installed = false;
|
|
55
|
+
try {
|
|
56
|
+
const { whichBinary } = require('./paths');
|
|
57
|
+
installed = !!whichBinary(e.install?.binary || e.name);
|
|
58
|
+
} catch {}
|
|
59
|
+
// Also check installed_agents.json marker
|
|
60
|
+
if (!installed) {
|
|
61
|
+
try {
|
|
62
|
+
const markerFile = path.join(connector.config?.configDir || '', 'installed_agents.json');
|
|
63
|
+
if (fs.existsSync(markerFile)) {
|
|
64
|
+
const markers = JSON.parse(fs.readFileSync(markerFile, 'utf-8'));
|
|
65
|
+
installed = !!markers[e.name];
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
name: e.name,
|
|
71
|
+
label: e.label || e.name,
|
|
72
|
+
description: e.description || '',
|
|
73
|
+
installed,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const STATE_COLORS = {
|
|
79
|
+
online: 'green', running: 'green',
|
|
80
|
+
starting: 'yellow', reconnecting: 'yellow',
|
|
81
|
+
stopped: 'white', error: 'red',
|
|
82
|
+
'not configured': 'white',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const STATE_SYMBOLS = {
|
|
86
|
+
online: '●', running: '●',
|
|
87
|
+
starting: '◐', reconnecting: '◐',
|
|
88
|
+
stopped: '○', error: '✗',
|
|
89
|
+
'not configured': '○',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ── Main TUI ─────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function createTUI() {
|
|
95
|
+
const screen = blessed.screen({
|
|
96
|
+
smartCSR: true,
|
|
97
|
+
title: 'OpenAgents',
|
|
98
|
+
fullUnicode: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const connector = getConnector();
|
|
102
|
+
|
|
103
|
+
// ── Layout ──
|
|
104
|
+
|
|
105
|
+
// Header
|
|
106
|
+
const header = blessed.box({
|
|
107
|
+
top: 0, left: 0, width: '100%', height: 3,
|
|
108
|
+
content: '{bold} OpenAgents{/bold} {gray-fg}Interactive Setup{/gray-fg}',
|
|
109
|
+
tags: true,
|
|
110
|
+
style: { bg: 'blue', fg: 'white' },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Agent table
|
|
114
|
+
const agentBox = blessed.box({
|
|
115
|
+
top: 3, left: 0, width: '100%', height: '60%',
|
|
116
|
+
border: { type: 'line' },
|
|
117
|
+
label: ' Agents ',
|
|
118
|
+
tags: true,
|
|
119
|
+
scrollable: true,
|
|
120
|
+
keys: true,
|
|
121
|
+
vi: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const agentList = blessed.list({
|
|
125
|
+
parent: agentBox,
|
|
126
|
+
top: 0, left: 0, width: '100%-2', height: '100%-2',
|
|
127
|
+
tags: true,
|
|
128
|
+
keys: true,
|
|
129
|
+
vi: true,
|
|
130
|
+
mouse: true,
|
|
131
|
+
style: {
|
|
132
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
133
|
+
item: { fg: 'white' },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Activity log
|
|
138
|
+
const logBox = blessed.box({
|
|
139
|
+
top: '60%+3', left: 0, width: '100%', height: '40%-6',
|
|
140
|
+
border: { type: 'line' },
|
|
141
|
+
label: ' Activity Log ',
|
|
142
|
+
tags: true,
|
|
143
|
+
scrollable: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const logContent = blessed.log({
|
|
147
|
+
parent: logBox,
|
|
148
|
+
top: 0, left: 0, width: '100%-2', height: '100%-2',
|
|
149
|
+
tags: true,
|
|
150
|
+
scrollable: true,
|
|
151
|
+
scrollOnInput: true,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Footer with keybindings
|
|
155
|
+
const footer = blessed.box({
|
|
156
|
+
bottom: 0, left: 0, width: '100%', height: 3,
|
|
157
|
+
tags: true,
|
|
158
|
+
style: { bg: 'blue', fg: 'white' },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
screen.append(header);
|
|
162
|
+
screen.append(agentBox);
|
|
163
|
+
screen.append(logBox);
|
|
164
|
+
screen.append(footer);
|
|
165
|
+
|
|
166
|
+
// ── State ──
|
|
167
|
+
|
|
168
|
+
let agentRows = [];
|
|
169
|
+
let currentView = 'main'; // main | install | configure
|
|
170
|
+
|
|
171
|
+
function log(msg) {
|
|
172
|
+
const ts = new Date().toLocaleTimeString();
|
|
173
|
+
logContent.log(`{gray-fg}${ts}{/gray-fg} ${msg}`);
|
|
174
|
+
screen.render();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Refresh ──
|
|
178
|
+
|
|
179
|
+
function refreshAgentTable() {
|
|
180
|
+
try {
|
|
181
|
+
agentRows = loadAgentRows(connector);
|
|
182
|
+
} catch { agentRows = []; }
|
|
183
|
+
|
|
184
|
+
const items = agentRows.length ? agentRows.map(r => {
|
|
185
|
+
const sym = STATE_SYMBOLS[r.state] || '?';
|
|
186
|
+
const color = STATE_COLORS[r.state] || 'white';
|
|
187
|
+
const ws = r.workspace ? ` ${r.workspace}` : '';
|
|
188
|
+
return ` {${color}-fg}${sym}{/${color}-fg} ${r.name.padEnd(20)} ${r.type.padEnd(12)} {${color}-fg}${r.state.padEnd(12)}{/${color}-fg}${ws}`;
|
|
189
|
+
}) : [' {gray-fg}No agents configured — press {bold}i{/bold} to install one{/gray-fg}'];
|
|
190
|
+
|
|
191
|
+
agentList.setItems(items);
|
|
192
|
+
updateFooter();
|
|
193
|
+
screen.render();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function updateFooter() {
|
|
197
|
+
const pid = connector.getDaemonPid();
|
|
198
|
+
const daemonState = pid ? '{green-fg}● running{/green-fg}' : '{yellow-fg}○ idle{/yellow-fg}';
|
|
199
|
+
|
|
200
|
+
const keys = [
|
|
201
|
+
'{bold}i{/bold}:Install', '{bold}n{/bold}:New', '{bold}s{/bold}:Start', '{bold}x{/bold}:Stop',
|
|
202
|
+
'{bold}c{/bold}:Connect', '{bold}d{/bold}:Disconnect', '{bold}u{/bold}:Daemon',
|
|
203
|
+
'{bold}r{/bold}:Refresh', '{bold}q{/bold}:Quit',
|
|
204
|
+
];
|
|
205
|
+
footer.setContent(` Daemon: ${daemonState} │ ${keys.join(' ')}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Install Screen ──
|
|
209
|
+
|
|
210
|
+
function showInstallScreen() {
|
|
211
|
+
currentView = 'install';
|
|
212
|
+
let catalog;
|
|
213
|
+
try {
|
|
214
|
+
catalog = loadCatalog(connector);
|
|
215
|
+
} catch (e) {
|
|
216
|
+
log(`{red-fg}Error loading catalog: ${e.message}{/red-fg}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const installBox = blessed.box({
|
|
221
|
+
top: 0, left: 0, width: '100%', height: '100%',
|
|
222
|
+
tags: true,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const installHeader = blessed.box({
|
|
226
|
+
parent: installBox,
|
|
227
|
+
top: 0, left: 0, width: '100%', height: 3,
|
|
228
|
+
content: '{bold} Install Agent Runtime{/bold} {gray-fg}Enter to install, Escape to go back{/gray-fg}',
|
|
229
|
+
tags: true,
|
|
230
|
+
style: { bg: 'blue', fg: 'white' },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const installList = blessed.list({
|
|
234
|
+
parent: installBox,
|
|
235
|
+
top: 3, left: 0, width: '100%', height: '100%-6',
|
|
236
|
+
border: { type: 'line' },
|
|
237
|
+
tags: true, keys: true, vi: true, mouse: true,
|
|
238
|
+
style: {
|
|
239
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
240
|
+
item: { fg: 'white' },
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const installFooter = blessed.box({
|
|
245
|
+
parent: installBox,
|
|
246
|
+
bottom: 0, left: 0, width: '100%', height: 3,
|
|
247
|
+
tags: true,
|
|
248
|
+
style: { bg: 'blue', fg: 'white' },
|
|
249
|
+
content: ' {bold}Enter{/bold}:Install/Update {bold}Escape{/bold}:Back',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const items = catalog.map(e => {
|
|
253
|
+
const status = e.installed ? '{green-fg}installed{/green-fg}' : '{yellow-fg}not installed{/yellow-fg}';
|
|
254
|
+
const desc = e.description ? ` {gray-fg}${e.description.substring(0, 40)}{/gray-fg}` : '';
|
|
255
|
+
return ` ${e.label.padEnd(25)} ${status}${desc}`;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
installList.setItems(items);
|
|
259
|
+
installList.focus();
|
|
260
|
+
|
|
261
|
+
installList.on('select', (item, idx) => {
|
|
262
|
+
const entry = catalog[idx];
|
|
263
|
+
if (!entry) return;
|
|
264
|
+
|
|
265
|
+
const verb = entry.installed ? 'Update' : 'Install';
|
|
266
|
+
// Show confirm dialog
|
|
267
|
+
const confirm = blessed.question({
|
|
268
|
+
parent: installBox,
|
|
269
|
+
top: 'center', left: 'center',
|
|
270
|
+
width: 50, height: 7,
|
|
271
|
+
border: { type: 'line' },
|
|
272
|
+
tags: true,
|
|
273
|
+
style: { bg: 'black', fg: 'white', border: { fg: 'blue' } },
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
confirm.ask(`${verb} ${entry.label}?`, (err, ok) => {
|
|
277
|
+
confirm.destroy();
|
|
278
|
+
if (!ok) { installList.focus(); screen.render(); return; }
|
|
279
|
+
doInstall(entry, installBox, installList);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
installList.key('escape', () => {
|
|
284
|
+
screen.remove(installBox);
|
|
285
|
+
installBox.destroy();
|
|
286
|
+
currentView = 'main';
|
|
287
|
+
agentList.focus();
|
|
288
|
+
refreshAgentTable();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
screen.append(installBox);
|
|
292
|
+
installList.focus();
|
|
293
|
+
screen.render();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function doInstall(entry, installBox, installList) {
|
|
297
|
+
const statusLine = blessed.box({
|
|
298
|
+
parent: installBox,
|
|
299
|
+
bottom: 3, left: 0, width: '100%', height: 1,
|
|
300
|
+
tags: true,
|
|
301
|
+
content: ` Installing ${entry.name}...`,
|
|
302
|
+
});
|
|
303
|
+
screen.render();
|
|
304
|
+
|
|
305
|
+
const installer = connector.getInstaller();
|
|
306
|
+
const installCmd = installer._resolveInstallCommand(entry.name);
|
|
307
|
+
if (!installCmd) {
|
|
308
|
+
statusLine.setContent(` {red-fg}✗ No install command for ${entry.name}{/red-fg}`);
|
|
309
|
+
screen.render();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
log(`Installing ${entry.name}: ${installCmd}`);
|
|
314
|
+
statusLine.setContent(` {cyan-fg}$ ${installCmd}{/cyan-fg}`);
|
|
315
|
+
screen.render();
|
|
316
|
+
|
|
317
|
+
const env = { ...process.env };
|
|
318
|
+
env.npm_config_yes = 'true';
|
|
319
|
+
env.CI = '1';
|
|
320
|
+
|
|
321
|
+
// Enhance PATH
|
|
322
|
+
const extraDirs = getExtraBinDirs();
|
|
323
|
+
if (extraDirs.length) {
|
|
324
|
+
const sep = IS_WINDOWS ? ';' : ':';
|
|
325
|
+
env.PATH = extraDirs.join(sep) + sep + (env.PATH || '');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const proc = spawn(installCmd, [], {
|
|
329
|
+
shell: true, env,
|
|
330
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
let lastLine = '';
|
|
334
|
+
const onData = (data) => {
|
|
335
|
+
const lines = data.toString().split('\n').filter(l => l.trim());
|
|
336
|
+
for (const line of lines) {
|
|
337
|
+
lastLine = line.trim().substring(0, 100);
|
|
338
|
+
log(` ${lastLine}`);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
proc.stdout.on('data', onData);
|
|
342
|
+
proc.stderr.on('data', onData);
|
|
343
|
+
|
|
344
|
+
proc.on('close', (code) => {
|
|
345
|
+
if (code === 0) {
|
|
346
|
+
statusLine.setContent(` {green-fg}✓ ${entry.name} installed successfully{/green-fg}`);
|
|
347
|
+
log(`{green-fg}✓ ${entry.name} installed{/green-fg}`);
|
|
348
|
+
// Mark as installed
|
|
349
|
+
try {
|
|
350
|
+
const markerFile = path.join(connector.config?.configDir || '', 'installed_agents.json');
|
|
351
|
+
let markers = {};
|
|
352
|
+
try { markers = JSON.parse(fs.readFileSync(markerFile, 'utf-8')); } catch {}
|
|
353
|
+
markers[entry.name] = { installed_at: new Date().toISOString() };
|
|
354
|
+
fs.writeFileSync(markerFile, JSON.stringify(markers, null, 2));
|
|
355
|
+
} catch {}
|
|
356
|
+
// Refresh catalog
|
|
357
|
+
try {
|
|
358
|
+
const newCatalog = loadCatalog(connector);
|
|
359
|
+
const newItems = newCatalog.map(e => {
|
|
360
|
+
const st = e.installed ? '{green-fg}installed{/green-fg}' : '{yellow-fg}not installed{/yellow-fg}';
|
|
361
|
+
const desc = e.description ? ` {gray-fg}${e.description.substring(0, 40)}{/gray-fg}` : '';
|
|
362
|
+
return ` ${e.label.padEnd(25)} ${st}${desc}`;
|
|
363
|
+
});
|
|
364
|
+
installList.setItems(newItems);
|
|
365
|
+
} catch {}
|
|
366
|
+
} else {
|
|
367
|
+
statusLine.setContent(` {red-fg}✗ Install failed (exit ${code}): ${lastLine}{/red-fg}`);
|
|
368
|
+
log(`{red-fg}✗ ${entry.name} install failed (exit ${code}){/red-fg}`);
|
|
369
|
+
}
|
|
370
|
+
installList.focus();
|
|
371
|
+
screen.render();
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── New Agent ──
|
|
376
|
+
|
|
377
|
+
function showNewAgentDialog() {
|
|
378
|
+
const form = blessed.form({
|
|
379
|
+
top: 'center', left: 'center',
|
|
380
|
+
width: 60, height: 14,
|
|
381
|
+
border: { type: 'line' },
|
|
382
|
+
tags: true, keys: true,
|
|
383
|
+
label: ' New Agent ',
|
|
384
|
+
style: { border: { fg: 'blue' } },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
blessed.text({ parent: form, top: 1, left: 2, content: 'Agent name:', tags: true });
|
|
388
|
+
const nameInput = blessed.textbox({
|
|
389
|
+
parent: form, top: 2, left: 2, width: 40, height: 3,
|
|
390
|
+
border: { type: 'line' },
|
|
391
|
+
inputOnFocus: true,
|
|
392
|
+
style: { focus: { border: { fg: 'blue' } } },
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
blessed.text({ parent: form, top: 5, left: 2, content: 'Type (openclaw/claude/codex/aider/goose):', tags: true });
|
|
396
|
+
const typeInput = blessed.textbox({
|
|
397
|
+
parent: form, top: 6, left: 2, width: 40, height: 3,
|
|
398
|
+
border: { type: 'line' },
|
|
399
|
+
inputOnFocus: true,
|
|
400
|
+
value: 'openclaw',
|
|
401
|
+
style: { focus: { border: { fg: 'blue' } } },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
blessed.button({
|
|
405
|
+
parent: form, top: 10, left: 2, width: 12, height: 1,
|
|
406
|
+
content: ' Create ', tags: true,
|
|
407
|
+
style: { bg: 'blue', fg: 'white', focus: { bg: 'cyan' } },
|
|
408
|
+
mouse: true,
|
|
409
|
+
}).on('press', () => {
|
|
410
|
+
const name = nameInput.getValue().trim();
|
|
411
|
+
const type = typeInput.getValue().trim();
|
|
412
|
+
if (!name || !type) return;
|
|
413
|
+
try {
|
|
414
|
+
connector.createAgent(name, type);
|
|
415
|
+
log(`{green-fg}✓ Agent '${name}' (${type}) created{/green-fg}`);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
log(`{red-fg}✗ Error: ${e.message}{/red-fg}`);
|
|
418
|
+
}
|
|
419
|
+
screen.remove(form);
|
|
420
|
+
form.destroy();
|
|
421
|
+
agentList.focus();
|
|
422
|
+
refreshAgentTable();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
form.key('escape', () => {
|
|
426
|
+
screen.remove(form);
|
|
427
|
+
form.destroy();
|
|
428
|
+
agentList.focus();
|
|
429
|
+
screen.render();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
screen.append(form);
|
|
433
|
+
nameInput.focus();
|
|
434
|
+
screen.render();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Keybindings ──
|
|
438
|
+
|
|
439
|
+
screen.key('q', () => process.exit(0));
|
|
440
|
+
screen.key('C-c', () => process.exit(0));
|
|
441
|
+
|
|
442
|
+
screen.key('i', () => {
|
|
443
|
+
if (currentView === 'main') showInstallScreen();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
screen.key('n', () => {
|
|
447
|
+
if (currentView === 'main') showNewAgentDialog();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
screen.key('r', () => {
|
|
451
|
+
if (currentView === 'main') {
|
|
452
|
+
refreshAgentTable();
|
|
453
|
+
log('Refreshed');
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
screen.key('s', () => {
|
|
458
|
+
if (currentView !== 'main') return;
|
|
459
|
+
const idx = agentList.selected;
|
|
460
|
+
const agent = agentRows[idx];
|
|
461
|
+
if (!agent) return;
|
|
462
|
+
try {
|
|
463
|
+
connector.startAgent(agent.name);
|
|
464
|
+
log(`Starting ${agent.name}...`);
|
|
465
|
+
} catch (e) {
|
|
466
|
+
log(`{red-fg}Error: ${e.message}{/red-fg}`);
|
|
467
|
+
}
|
|
468
|
+
setTimeout(refreshAgentTable, 2000);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
screen.key('x', () => {
|
|
472
|
+
if (currentView !== 'main') return;
|
|
473
|
+
const idx = agentList.selected;
|
|
474
|
+
const agent = agentRows[idx];
|
|
475
|
+
if (!agent) return;
|
|
476
|
+
try {
|
|
477
|
+
connector.stopAgent(agent.name);
|
|
478
|
+
log(`Stopped ${agent.name}`);
|
|
479
|
+
} catch (e) {
|
|
480
|
+
log(`{red-fg}Error: ${e.message}{/red-fg}`);
|
|
481
|
+
}
|
|
482
|
+
setTimeout(refreshAgentTable, 1000);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
screen.key('u', () => {
|
|
486
|
+
if (currentView !== 'main') return;
|
|
487
|
+
const pid = connector.getDaemonPid();
|
|
488
|
+
if (pid) {
|
|
489
|
+
connector.stopDaemon();
|
|
490
|
+
log('Daemon stopped');
|
|
491
|
+
} else {
|
|
492
|
+
connector.startDaemon();
|
|
493
|
+
log('Daemon started');
|
|
494
|
+
}
|
|
495
|
+
setTimeout(refreshAgentTable, 2000);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
screen.key('c', () => {
|
|
499
|
+
if (currentView !== 'main') return;
|
|
500
|
+
const idx = agentList.selected;
|
|
501
|
+
const agent = agentRows[idx];
|
|
502
|
+
if (!agent) return;
|
|
503
|
+
// TODO: show workspace picker
|
|
504
|
+
log(`Connect: use 'agent-connector connect ${agent.name} <workspace-slug>' from terminal`);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
screen.key('d', () => {
|
|
508
|
+
if (currentView !== 'main') return;
|
|
509
|
+
const idx = agentList.selected;
|
|
510
|
+
const agent = agentRows[idx];
|
|
511
|
+
if (!agent) return;
|
|
512
|
+
try {
|
|
513
|
+
connector.disconnectAgent(agent.name);
|
|
514
|
+
log(`Disconnected ${agent.name}`);
|
|
515
|
+
} catch (e) {
|
|
516
|
+
log(`{red-fg}Error: ${e.message}{/red-fg}`);
|
|
517
|
+
}
|
|
518
|
+
refreshAgentTable();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// ── Init ──
|
|
522
|
+
|
|
523
|
+
agentList.focus();
|
|
524
|
+
refreshAgentTable();
|
|
525
|
+
log('Ready. Press {bold}i{/bold} to install agents, {bold}n{/bold} to create one.');
|
|
526
|
+
|
|
527
|
+
// Auto-refresh every 5 seconds
|
|
528
|
+
setInterval(refreshAgentTable, 5000);
|
|
529
|
+
|
|
530
|
+
screen.render();
|
|
531
|
+
return screen;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Entry point ──────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
function run() {
|
|
537
|
+
createTUI();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
module.exports = { run };
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test an LLM connection by sending a minimal inference request.
|
|
5
|
+
*
|
|
6
|
+
* Supports OpenAI-compatible and Anthropic APIs.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} env - Env vars (LLM_API_KEY, LLM_BASE_URL, LLM_MODEL, etc.)
|
|
9
|
+
* @returns {Promise<{success: boolean, model?: string, response?: string, error?: string}>}
|
|
10
|
+
*/
|
|
11
|
+
function testLLMConnection(env) {
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
|
|
15
|
+
const apiKey = env.LLM_API_KEY || env.OPENAI_API_KEY || env.ANTHROPIC_API_KEY || '';
|
|
16
|
+
if (!apiKey) return Promise.resolve({ success: false, error: 'No API key provided' });
|
|
17
|
+
|
|
18
|
+
let baseUrl = (env.LLM_BASE_URL || env.OPENAI_BASE_URL || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
19
|
+
const model = env.LLM_MODEL || env.OPENCLAW_MODEL || '';
|
|
20
|
+
const isAnthropic = baseUrl.includes('anthropic');
|
|
21
|
+
|
|
22
|
+
if (!isAnthropic && !baseUrl.endsWith('/v1')) {
|
|
23
|
+
baseUrl += '/v1';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
let url, headers, body;
|
|
28
|
+
|
|
29
|
+
if (isAnthropic) {
|
|
30
|
+
url = 'https://api.anthropic.com/v1/messages';
|
|
31
|
+
headers = {
|
|
32
|
+
'x-api-key': apiKey,
|
|
33
|
+
'anthropic-version': '2023-06-01',
|
|
34
|
+
'content-type': 'application/json',
|
|
35
|
+
};
|
|
36
|
+
body = JSON.stringify({
|
|
37
|
+
model: model || 'claude-sonnet-4-20250514',
|
|
38
|
+
max_tokens: 32,
|
|
39
|
+
messages: [{ role: 'user', content: 'Say hi in 5 words.' }],
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
url = baseUrl + '/chat/completions';
|
|
43
|
+
headers = {
|
|
44
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
};
|
|
47
|
+
body = JSON.stringify({
|
|
48
|
+
model: model || 'gpt-4o-mini',
|
|
49
|
+
max_tokens: 32,
|
|
50
|
+
messages: [{ role: 'user', content: 'Say hi in 5 words.' }],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parsedUrl = new URL(url);
|
|
55
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
56
|
+
|
|
57
|
+
const req = transport.request(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
|
|
60
|
+
timeout: 15000,
|
|
61
|
+
}, (res) => {
|
|
62
|
+
let data = '';
|
|
63
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
64
|
+
res.on('end', () => {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(data);
|
|
67
|
+
let text, usedModel;
|
|
68
|
+
if (isAnthropic) {
|
|
69
|
+
text = (parsed.content || [{}])[0].text || '';
|
|
70
|
+
usedModel = parsed.model || model || '?';
|
|
71
|
+
} else {
|
|
72
|
+
text = (parsed.choices || [{}])[0]?.message?.content || '';
|
|
73
|
+
usedModel = parsed.model || model || '?';
|
|
74
|
+
}
|
|
75
|
+
if (res.statusCode >= 400) {
|
|
76
|
+
resolve({ success: false, error: `HTTP ${res.statusCode}: ${data.slice(0, 200)}` });
|
|
77
|
+
} else {
|
|
78
|
+
resolve({ success: true, model: usedModel, response: text.slice(0, 80) });
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
resolve({ success: false, error: `Invalid response: ${data.slice(0, 200)}` });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
req.on('error', (e) => resolve({ success: false, error: e.message }));
|
|
87
|
+
req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'Request timed out' }); });
|
|
88
|
+
req.write(body);
|
|
89
|
+
req.end();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { testLLMConnection };
|