@addai/node 0.15.0 → 0.16.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/dist/command-runner.js +42 -2
- package/dist/desktop/install-engine.d.ts +12 -0
- package/dist/desktop/install-engine.js +62 -0
- package/dist/desktop/open-viewer.d.ts +17 -0
- package/dist/desktop/open-viewer.js +40 -0
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/dashboard.js +8 -0
- package/dist/tui/desktops.d.ts +11 -0
- package/dist/tui/desktops.js +201 -0
- package/dist/tui/run.js +2 -0
- package/package.json +1 -1
package/dist/command-runner.js
CHANGED
|
@@ -63,6 +63,7 @@ const pty_helper_1 = require("./pty-helper");
|
|
|
63
63
|
const docker_1 = require("./desktop/docker");
|
|
64
64
|
const manager_1 = require("./desktop/manager");
|
|
65
65
|
const creds_1 = require("./desktop/creds");
|
|
66
|
+
const install_engine_1 = require("./desktop/install-engine");
|
|
66
67
|
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
67
68
|
/** Own version, read the same way index.ts does. Resolved here rather than
|
|
68
69
|
* imported from index.ts, which already imports this module. */
|
|
@@ -107,6 +108,12 @@ async function npmGlobalBinFor(name) {
|
|
|
107
108
|
return null;
|
|
108
109
|
}
|
|
109
110
|
const INPUT_POLL_MS = 2_000;
|
|
111
|
+
/** How long to wait on a HUMAN before giving up. Deliberately shorter than
|
|
112
|
+
* COMMAND_TIMEOUT_MS: one command runs at a time, so a login nobody ever
|
|
113
|
+
* pastes a code into blocks every other command on this node for as long as
|
|
114
|
+
* it waits. Three minutes is long enough to fetch a code from a browser and
|
|
115
|
+
* short enough that a forgotten login is not a wedged machine. */
|
|
116
|
+
const INPUT_WAIT_MS = 3 * 60_000;
|
|
110
117
|
const LOG_TAIL_CHARS = 4_000;
|
|
111
118
|
const LOG_PUSH_MS = 1_000;
|
|
112
119
|
let running = false;
|
|
@@ -254,7 +261,7 @@ async function runLoginPty(cmd, spec, login = spec.login) {
|
|
|
254
261
|
return;
|
|
255
262
|
}
|
|
256
263
|
await update(cmd.id, 'running', { step: 'starting-login', instructions: login.instructions });
|
|
257
|
-
const deadline = Date.now() +
|
|
264
|
+
const deadline = Date.now() + INPUT_WAIT_MS;
|
|
258
265
|
const ptyOpts = {
|
|
259
266
|
name: 'xterm-256color', cols: 120, rows: 30,
|
|
260
267
|
cwd: os.homedir(),
|
|
@@ -408,7 +415,7 @@ async function applyApiKey(spec, key, method) {
|
|
|
408
415
|
}
|
|
409
416
|
async function runLoginApiKey(cmd, spec, method, instructions) {
|
|
410
417
|
await update(cmd.id, 'awaiting_input', { needs: 'api_key', instructions: instructions ?? spec.login.instructions });
|
|
411
|
-
const deadline = Date.now() +
|
|
418
|
+
const deadline = Date.now() + INPUT_WAIT_MS;
|
|
412
419
|
const key = cmd.input?.api_key?.trim() || await awaitInput(cmd.id, deadline, i => i.api_key);
|
|
413
420
|
if (key === null) {
|
|
414
421
|
await update(cmd.id, 'canceled', {}, 'canceled or timed out waiting for the key');
|
|
@@ -673,6 +680,35 @@ async function runDesktopCommand(cmd) {
|
|
|
673
680
|
}
|
|
674
681
|
}
|
|
675
682
|
}
|
|
683
|
+
/** Install a container engine, then prove it took. The capability probe is
|
|
684
|
+
* the verdict, not the installer's exit code - Docker Desktop on macOS
|
|
685
|
+
* installs the app but the daemon is not up until someone launches it, so a
|
|
686
|
+
* clean exit does not mean a usable engine. */
|
|
687
|
+
async function runInstallEngine(cmd) {
|
|
688
|
+
let log = '';
|
|
689
|
+
const onLog = (chunk) => {
|
|
690
|
+
log = (log + chunk).slice(-LOG_TAIL_CHARS);
|
|
691
|
+
void update(cmd.id, null, { log });
|
|
692
|
+
};
|
|
693
|
+
await update(cmd.id, 'running', { log });
|
|
694
|
+
try {
|
|
695
|
+
await (0, install_engine_1.installEngine)(onLog);
|
|
696
|
+
(0, docker_1.resetProviderCache)();
|
|
697
|
+
const engine = await (0, docker_1.getProvider)(true);
|
|
698
|
+
if (!engine) {
|
|
699
|
+
await update(cmd.id, 'failed', { log }, 'Installed, but no engine is answering yet. On macOS, Docker Desktop has to be opened once before its daemon runs.');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
onLog(`\n${engine.id} is ready.\n`);
|
|
703
|
+
await update(cmd.id, 'completed', { log, engine: engine.id });
|
|
704
|
+
// Push the new capability immediately so the tab flips without waiting
|
|
705
|
+
// for the next 30s heartbeat.
|
|
706
|
+
await (0, heartbeat_1.beat)();
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
await update(cmd.id, 'failed', { log }, err.message.slice(0, 600));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
676
712
|
/* ── dispatcher ──────────────────────────────────────────────────────── */
|
|
677
713
|
async function execute(cmd) {
|
|
678
714
|
// Not a harness command — dispatch before the harness lookup, which would
|
|
@@ -685,6 +721,10 @@ async function execute(cmd) {
|
|
|
685
721
|
await runSetAutostart(cmd);
|
|
686
722
|
return;
|
|
687
723
|
}
|
|
724
|
+
if (cmd.kind === 'install_engine') {
|
|
725
|
+
await runInstallEngine(cmd);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
688
728
|
if (DESKTOP_KINDS.includes(cmd.kind)) {
|
|
689
729
|
await runDesktopCommand(cmd);
|
|
690
730
|
return;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface InstallPlan {
|
|
2
|
+
/** Null when this platform has no unattended path we trust. */
|
|
3
|
+
file: string | null;
|
|
4
|
+
args: string[];
|
|
5
|
+
/** What a human would have to run, shown when we cannot do it ourselves. */
|
|
6
|
+
manual: string;
|
|
7
|
+
needsRoot: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Pure, so the choice is testable on any machine. */
|
|
10
|
+
export declare function installPlan(platform: NodeJS.Platform, isRoot: boolean): InstallPlan;
|
|
11
|
+
export declare function runningAsRoot(): boolean;
|
|
12
|
+
export declare function installEngine(onLog: (s: string) => void): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installPlan = installPlan;
|
|
4
|
+
exports.runningAsRoot = runningAsRoot;
|
|
5
|
+
exports.installEngine = installEngine;
|
|
6
|
+
// Install a container engine on this machine, on request.
|
|
7
|
+
//
|
|
8
|
+
// Deliberately per-platform and deliberately honest about privilege: the
|
|
9
|
+
// Linux path needs root, and a daemon running as an ordinary user cannot get
|
|
10
|
+
// it. Rather than half-run and leave a broken apt state, it checks first and
|
|
11
|
+
// says exactly what to do instead. A button that fails clearly beats one that
|
|
12
|
+
// fails mysteriously.
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const win_1 = require("../win");
|
|
15
|
+
/** Pure, so the choice is testable on any machine. */
|
|
16
|
+
function installPlan(platform, isRoot) {
|
|
17
|
+
if (platform === 'darwin') {
|
|
18
|
+
// Homebrew installs Docker Desktop without sudo when brew owns its prefix.
|
|
19
|
+
return {
|
|
20
|
+
file: 'brew', args: ['install', '--cask', '--no-quarantine', 'docker'],
|
|
21
|
+
manual: 'brew install --cask docker', needsRoot: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
return {
|
|
26
|
+
file: 'winget',
|
|
27
|
+
args: ['install', '-e', '--id', 'Docker.DockerDesktop', '--accept-package-agreements', '--accept-source-agreements'],
|
|
28
|
+
manual: 'winget install -e --id Docker.DockerDesktop', needsRoot: false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Linux: get.docker.com is the vendor's own script and needs root. Podman
|
|
32
|
+
// from the distro is the lighter option but still needs a package manager.
|
|
33
|
+
return {
|
|
34
|
+
file: isRoot ? 'sh' : null,
|
|
35
|
+
args: ['-c', 'curl -fsSL https://get.docker.com | sh'],
|
|
36
|
+
manual: 'curl -fsSL https://get.docker.com | sudo sh', needsRoot: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function runningAsRoot() {
|
|
40
|
+
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
41
|
+
}
|
|
42
|
+
async function installEngine(onLog) {
|
|
43
|
+
const plan = installPlan(process.platform, runningAsRoot());
|
|
44
|
+
if (!plan.file) {
|
|
45
|
+
throw new Error(`This needs root, and the node is not running as root. Run this on ${require('os').hostname()}:\n ${plan.manual}`);
|
|
46
|
+
}
|
|
47
|
+
onLog(`installing a container engine\n ${plan.manual}\n\n`);
|
|
48
|
+
const inv = (0, win_1.resolveCliInvocation)(plan.file, plan.args);
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
const child = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
51
|
+
env: process.env, windowsHide: true, timeout: 25 * 60_000,
|
|
52
|
+
});
|
|
53
|
+
let tail = '';
|
|
54
|
+
const cap = (b) => { const s = b.toString('utf8'); tail = (tail + s).slice(-2000); onLog(s); };
|
|
55
|
+
child.stdout?.on('data', cap);
|
|
56
|
+
child.stderr?.on('data', cap);
|
|
57
|
+
child.on('error', err => reject(new Error(`${plan.file} is not available here. Install it by hand:\n ${plan.manual}\n(${err.message})`)));
|
|
58
|
+
child.on('exit', code => code === 0
|
|
59
|
+
? resolve()
|
|
60
|
+
: reject(new Error(`install failed (exit ${code}). Try by hand:\n ${plan.manual}\n${tail.slice(-400)}`)));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ViewerTarget {
|
|
2
|
+
vncPort: number | null;
|
|
3
|
+
vncPassword: string | null;
|
|
4
|
+
desktopId: string;
|
|
5
|
+
}
|
|
6
|
+
/** What we would run, as a pure value, so the choice is testable without
|
|
7
|
+
* launching anything. */
|
|
8
|
+
export declare function viewerCommand(platform: NodeJS.Platform, t: ViewerTarget, studioBase?: string): {
|
|
9
|
+
file: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
kind: 'local' | 'browser';
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function openViewer(t: ViewerTarget): {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
kind: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.viewerCommand = viewerCommand;
|
|
4
|
+
exports.openViewer = openViewer;
|
|
5
|
+
// Open a desktop in whatever this machine calls a screen viewer.
|
|
6
|
+
//
|
|
7
|
+
// A terminal cannot draw a remote framebuffer, so "control it from the
|
|
8
|
+
// console" means handing the desktop to something that can. macOS has a VNC
|
|
9
|
+
// client built in and the RFB port is already on loopback, so Screen Sharing
|
|
10
|
+
// connects straight to the container with no relay in the path - the lowest
|
|
11
|
+
// latency route there is. Windows has no built-in VNC client, so there the
|
|
12
|
+
// browser viewer is the honest fallback.
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
/** What we would run, as a pure value, so the choice is testable without
|
|
15
|
+
* launching anything. */
|
|
16
|
+
function viewerCommand(platform, t, studioBase = 'https://entities.add.ai') {
|
|
17
|
+
if (platform === 'darwin' && t.vncPort) {
|
|
18
|
+
// The password rides in the URL so Screen Sharing does not prompt. It
|
|
19
|
+
// never leaves this machine: the whole URL is handed to a local app.
|
|
20
|
+
const auth = t.vncPassword ? `:${encodeURIComponent(t.vncPassword)}@` : '';
|
|
21
|
+
return { file: 'open', args: [`vnc://${auth}127.0.0.1:${t.vncPort}`], kind: 'local' };
|
|
22
|
+
}
|
|
23
|
+
const url = `${studioBase}/desktops/${t.desktopId}`;
|
|
24
|
+
if (platform === 'win32')
|
|
25
|
+
return { file: 'cmd', args: ['/c', 'start', '', url], kind: 'browser' };
|
|
26
|
+
return { file: 'xdg-open', args: [url], kind: 'browser' };
|
|
27
|
+
}
|
|
28
|
+
function openViewer(t) {
|
|
29
|
+
const cmd = viewerCommand(process.platform, t);
|
|
30
|
+
if (!cmd)
|
|
31
|
+
return { ok: false, kind: 'none', error: 'no viewer for this platform' };
|
|
32
|
+
try {
|
|
33
|
+
const child = (0, child_process_1.spawn)(cmd.file, cmd.args, { detached: true, stdio: 'ignore' });
|
|
34
|
+
child.unref();
|
|
35
|
+
return { ok: true, kind: cmd.kind };
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return { ok: false, kind: cmd.kind, error: err.message };
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/tui/dashboard.d.ts
CHANGED
package/dist/tui/dashboard.js
CHANGED
|
@@ -26,6 +26,7 @@ const autostart_1 = require("../autostart");
|
|
|
26
26
|
exports.MENU = [
|
|
27
27
|
{ key: 'activity', label: 'Activity', hint: 'Every request this node has handled', shortcut: 'a' },
|
|
28
28
|
{ key: 'harnesses', label: 'Harnesses', hint: 'Install / log in agent CLIs', shortcut: 'h' },
|
|
29
|
+
{ key: 'desktops', label: 'Desktops', hint: 'Isolated environments entities work in', shortcut: 'd' },
|
|
29
30
|
{ key: 'logs', label: 'Logs', hint: 'What the daemon is saying', shortcut: 'l' },
|
|
30
31
|
];
|
|
31
32
|
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
@@ -388,6 +389,7 @@ function createDashboardScreen(deps) {
|
|
|
388
389
|
{ keys: '⏎', label: 'open the destination, or watch the selected run' },
|
|
389
390
|
{ keys: 'a', label: 'activity — full request history' },
|
|
390
391
|
{ keys: 'h', label: 'harnesses — install / log in agent CLIs' },
|
|
392
|
+
{ keys: 'd', label: 'desktops — the environments entities work in' },
|
|
391
393
|
{ keys: 'l', label: 'logs — daemon output' },
|
|
392
394
|
{ keys: 's', label: 'start this node at login (on / off)' },
|
|
393
395
|
{ keys: 'r', label: 'refresh now' },
|
|
@@ -412,6 +414,10 @@ function createDashboardScreen(deps) {
|
|
|
412
414
|
deps.openHarnesses();
|
|
413
415
|
return;
|
|
414
416
|
}
|
|
417
|
+
if (key.name === 'd') {
|
|
418
|
+
deps.openDesktops();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
415
421
|
if (key.name === 'l') {
|
|
416
422
|
deps.openLogs();
|
|
417
423
|
return;
|
|
@@ -445,6 +451,8 @@ function createDashboardScreen(deps) {
|
|
|
445
451
|
const target = exports.MENU[st.sel]?.key;
|
|
446
452
|
if (target === 'harnesses')
|
|
447
453
|
deps.openHarnesses();
|
|
454
|
+
else if (target === 'desktops')
|
|
455
|
+
deps.openDesktops();
|
|
448
456
|
else if (target === 'logs')
|
|
449
457
|
deps.openLogs();
|
|
450
458
|
else
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DesktopRow } from '../desktop/spec';
|
|
2
|
+
import { type AppHost, type Screen } from './app';
|
|
3
|
+
/** A desktop with a recent tool call is being worked in right now. Same 90s
|
|
4
|
+
* definition the chat card uses, so the console and chat never disagree. */
|
|
5
|
+
export declare function isBusyWith(row: {
|
|
6
|
+
last_used_at?: string | null;
|
|
7
|
+
}): boolean;
|
|
8
|
+
export declare function desktopLine(d: DesktopRow, selected: boolean, width: number): string;
|
|
9
|
+
export declare function createDesktopsScreen(deps: {
|
|
10
|
+
host: AppHost;
|
|
11
|
+
}): Screen;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The desktops on this machine, and what is happening in them.
|
|
3
|
+
//
|
|
4
|
+
// A screen on the stack like Harnesses, so it inherits the diff painter, the
|
|
5
|
+
// footer grammar and `?` help for free. Actions go through the same command
|
|
6
|
+
// path Studio uses rather than touching docker directly — one way to start a
|
|
7
|
+
// desktop, whoever asked for it, so the two surfaces can never disagree.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.isBusyWith = isBusyWith;
|
|
10
|
+
exports.desktopLine = desktopLine;
|
|
11
|
+
exports.createDesktopsScreen = createDesktopsScreen;
|
|
12
|
+
const manager_1 = require("../desktop/manager");
|
|
13
|
+
const docker_1 = require("../desktop/docker");
|
|
14
|
+
const open_viewer_1 = require("../desktop/open-viewer");
|
|
15
|
+
const render_1 = require("./render");
|
|
16
|
+
const app_1 = require("./app");
|
|
17
|
+
function tone(status) {
|
|
18
|
+
if (status === 'running')
|
|
19
|
+
return render_1.green;
|
|
20
|
+
if (status === 'failed')
|
|
21
|
+
return render_1.red;
|
|
22
|
+
if (status === 'creating' || status === 'starting' || status === 'deleting')
|
|
23
|
+
return render_1.yellow;
|
|
24
|
+
return render_1.grey;
|
|
25
|
+
}
|
|
26
|
+
/** A desktop with a recent tool call is being worked in right now. Same 90s
|
|
27
|
+
* definition the chat card uses, so the console and chat never disagree. */
|
|
28
|
+
function isBusyWith(row) {
|
|
29
|
+
if (!row.last_used_at)
|
|
30
|
+
return false;
|
|
31
|
+
return Date.now() - new Date(row.last_used_at).getTime() < 90_000;
|
|
32
|
+
}
|
|
33
|
+
function desktopLine(d, selected, width) {
|
|
34
|
+
const mark = selected ? (0, render_1.bold)('▸ ') : ' ';
|
|
35
|
+
const status = tone(d.status)(d.status.padEnd(9));
|
|
36
|
+
const size = (0, render_1.grey)(`${d.cpus}c ${Math.round(d.memory_mb / 1024)}g`);
|
|
37
|
+
const port = d.vnc_port ? (0, render_1.grey)(`:${d.vnc_port}`) : (0, render_1.grey)('—');
|
|
38
|
+
const name = (0, render_1.truncate)(d.name, Math.max(12, width - 34));
|
|
39
|
+
return `${mark}${status} ${name.padEnd(Math.max(12, width - 34))} ${size} ${port}`;
|
|
40
|
+
}
|
|
41
|
+
function createDesktopsScreen(deps) {
|
|
42
|
+
const st = { rows: null, sel: 0, spin: 0, busy: null, confirmDelete: null, note: null };
|
|
43
|
+
const current = () => st.rows?.[st.sel] ?? null;
|
|
44
|
+
const act = async (label, fn) => {
|
|
45
|
+
st.busy = label;
|
|
46
|
+
deps.host.redraw();
|
|
47
|
+
try {
|
|
48
|
+
await fn();
|
|
49
|
+
st.note = null;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
st.note = err.message.slice(0, 120);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
st.busy = null;
|
|
56
|
+
await refresh();
|
|
57
|
+
deps.host.redraw();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const refresh = async () => {
|
|
61
|
+
try {
|
|
62
|
+
st.rows = await (0, manager_1.listDesktops)();
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
st.note = err.message.slice(0, 120);
|
|
66
|
+
st.rows = st.rows ?? [];
|
|
67
|
+
}
|
|
68
|
+
if (st.sel >= (st.rows?.length ?? 0))
|
|
69
|
+
st.sel = Math.max(0, (st.rows?.length ?? 1) - 1);
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
id: 'desktops',
|
|
73
|
+
title: 'Desktops',
|
|
74
|
+
async poll() { await refresh(); },
|
|
75
|
+
pollMs: () => 5_000,
|
|
76
|
+
tick(n) { st.spin = n; return st.busy !== null; },
|
|
77
|
+
keys() {
|
|
78
|
+
return [
|
|
79
|
+
{ keys: '↑/↓', label: 'choose a desktop' },
|
|
80
|
+
{ keys: '↵', label: 'open it — Screen Sharing on this Mac' },
|
|
81
|
+
{ keys: 's', label: 'start / stop' },
|
|
82
|
+
{ keys: 'r', label: 'rebuild from the image' },
|
|
83
|
+
{ keys: 'd', label: 'delete (asks first)' },
|
|
84
|
+
];
|
|
85
|
+
},
|
|
86
|
+
render(width, height) {
|
|
87
|
+
const out = [];
|
|
88
|
+
out.push((0, app_1.heading)('Desktops', 'on this machine'));
|
|
89
|
+
out.push('');
|
|
90
|
+
if (st.rows === null) {
|
|
91
|
+
out.push((0, render_1.dim)(` ${render_1.SPIN[st.spin % render_1.SPIN.length]} looking…`));
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
if (st.rows.length === 0) {
|
|
95
|
+
out.push((0, render_1.dim)(' No desktops here yet.'));
|
|
96
|
+
out.push('');
|
|
97
|
+
out.push((0, render_1.dim)(' Create one from the machine\'s page in Entity Studio.'));
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
const live = st.rows.filter(d => d.status === 'running').length;
|
|
101
|
+
out.push((0, render_1.dim)(` ${live} running · ${st.rows.length} total`));
|
|
102
|
+
out.push('');
|
|
103
|
+
const room = Math.max(3, height - 8);
|
|
104
|
+
st.rows.slice(0, room).forEach((d, i) => out.push(desktopLine(d, i === st.sel, width)));
|
|
105
|
+
const sel = current();
|
|
106
|
+
if (sel?.status_message) {
|
|
107
|
+
out.push('');
|
|
108
|
+
out.push((0, render_1.dim)(` ${(0, render_1.truncate)(sel.status_message, width - 4)}`));
|
|
109
|
+
}
|
|
110
|
+
if (st.busy) {
|
|
111
|
+
out.push('');
|
|
112
|
+
out.push((0, render_1.dim)(` ${render_1.SPIN[st.spin % render_1.SPIN.length]} ${st.busy}…`));
|
|
113
|
+
}
|
|
114
|
+
if (st.confirmDelete) {
|
|
115
|
+
out.push('');
|
|
116
|
+
out.push((0, render_1.red)(` Delete "${st.confirmDelete}"? y to confirm, any other key to keep it.`));
|
|
117
|
+
}
|
|
118
|
+
if (st.note) {
|
|
119
|
+
out.push('');
|
|
120
|
+
out.push((0, render_1.red)(` ${(0, render_1.truncate)(st.note, width - 4)}`));
|
|
121
|
+
}
|
|
122
|
+
out.push('');
|
|
123
|
+
out.push((0, app_1.footerHint)(this.keys ? this.keys() : []));
|
|
124
|
+
return out;
|
|
125
|
+
},
|
|
126
|
+
async onKey(key) {
|
|
127
|
+
if (st.confirmDelete) {
|
|
128
|
+
const name = st.confirmDelete;
|
|
129
|
+
st.confirmDelete = null;
|
|
130
|
+
if (key.name !== 'y') {
|
|
131
|
+
deps.host.redraw();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const row = st.rows?.find(d => d.name === name);
|
|
135
|
+
if (row) {
|
|
136
|
+
await act('deleting', async () => {
|
|
137
|
+
const p = await (0, docker_1.getProvider)();
|
|
138
|
+
if (!p)
|
|
139
|
+
throw new Error('no container engine on this machine');
|
|
140
|
+
await p.remove(row);
|
|
141
|
+
await (0, manager_1.setStatus)(row.id, { status: 'deleting' });
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const rows = st.rows ?? [];
|
|
147
|
+
if (key.name === 'up') {
|
|
148
|
+
st.sel = Math.max(0, st.sel - 1);
|
|
149
|
+
deps.host.redraw();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (key.name === 'down') {
|
|
153
|
+
st.sel = Math.min(rows.length - 1, st.sel + 1);
|
|
154
|
+
deps.host.redraw();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const row = current();
|
|
158
|
+
if (!row)
|
|
159
|
+
return;
|
|
160
|
+
if (key.name === 'return') {
|
|
161
|
+
if (row.status !== 'running') {
|
|
162
|
+
st.note = 'Start it first — there is nothing to show yet.';
|
|
163
|
+
deps.host.redraw();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const r = (0, open_viewer_1.openViewer)({ vncPort: row.vnc_port, vncPassword: row.vnc_password, desktopId: row.id });
|
|
167
|
+
st.note = r.ok
|
|
168
|
+
? (r.kind === 'local' ? 'Opening in Screen Sharing…' : 'Opening the viewer in your browser…')
|
|
169
|
+
: `Could not open it: ${r.error ?? 'unknown'}`;
|
|
170
|
+
deps.host.redraw();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (key.name === 's') {
|
|
174
|
+
await act(row.status === 'running' ? 'stopping' : 'starting', async () => {
|
|
175
|
+
const p = await (0, docker_1.getProvider)();
|
|
176
|
+
if (!p)
|
|
177
|
+
throw new Error('no container engine on this machine');
|
|
178
|
+
if (row.status === 'running') {
|
|
179
|
+
await p.stop(row);
|
|
180
|
+
await (0, manager_1.setStatus)(row.id, { status: 'stopped' });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
await p.start(row);
|
|
184
|
+
await (0, manager_1.setStatus)(row.id, { status: 'running', status_message: null });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (key.name === 'r') {
|
|
190
|
+
st.note = 'Rebuild runs from Entity Studio — it re-pulls the image and copies logins in.';
|
|
191
|
+
deps.host.redraw();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (key.name === 'd') {
|
|
195
|
+
st.confirmDelete = row.name;
|
|
196
|
+
deps.host.redraw();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
package/dist/tui/run.js
CHANGED
|
@@ -48,6 +48,7 @@ const dashboard_1 = require("./dashboard");
|
|
|
48
48
|
const requests_1 = require("./requests");
|
|
49
49
|
const transcript_1 = require("./transcript");
|
|
50
50
|
const harnesses_1 = require("./harnesses");
|
|
51
|
+
const desktops_1 = require("./desktops");
|
|
51
52
|
const logs_1 = require("./logs");
|
|
52
53
|
const render_1 = require("./render");
|
|
53
54
|
const console_capture_1 = require("./console-capture");
|
|
@@ -208,6 +209,7 @@ async function runDashboard(opts) {
|
|
|
208
209
|
openTranscript,
|
|
209
210
|
openRequests: () => host.push((0, requests_1.createRequestsScreen)({ data, host, openTranscript })),
|
|
210
211
|
openHarnesses: () => host.push((0, harnesses_1.createHarnessesScreen)({ host, suspend })),
|
|
212
|
+
openDesktops: () => host.push((0, desktops_1.createDesktopsScreen)({ host })),
|
|
211
213
|
openLogs: () => host.push((0, logs_1.createLogsScreen)({
|
|
212
214
|
host,
|
|
213
215
|
logs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|