@addai/node 0.14.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.
@@ -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() + COMMAND_TIMEOUT_MS;
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() + COMMAND_TIMEOUT_MS;
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
+ }
@@ -68,7 +68,10 @@ let idleTimer = null;
68
68
  let liveTimer = null;
69
69
  /** Set on every pong; cleared when a ping goes out. Two misses = dead. */
70
70
  let missedPongs = 0;
71
- /** desktopId -> the local TCP socket to that desktop's published RFB port. */
71
+ /** viewerId -> that viewer's OWN TCP socket to the desktop's RFB port.
72
+ * Keyed per viewer, not per desktop: RFB is a stateful 1:1 protocol, so two
73
+ * people watching the same screen each need their own connection and their
74
+ * own handshake. x11vnc runs with -shared precisely so it will serve them. */
72
75
  const bridges = new Map();
73
76
  function dropBridges() {
74
77
  for (const s of bridges.values()) {
@@ -155,24 +158,24 @@ async function connect() {
155
158
  tcp.on('data', chunk => {
156
159
  if (sock.readyState === ws_1.default.OPEN) {
157
160
  sock.send(JSON.stringify({
158
- type: 'data', desktopId: msg.desktopId, b64: chunk.toString('base64'),
161
+ type: 'data', viewerId: msg.viewerId, b64: chunk.toString('base64'),
159
162
  }));
160
163
  }
161
164
  });
162
- const forget = () => { bridges.delete(msg.desktopId); };
165
+ const forget = () => { bridges.delete(msg.viewerId); };
163
166
  tcp.on('close', forget);
164
167
  tcp.on('error', forget);
165
- bridges.get(msg.desktopId)?.destroy();
166
- bridges.set(msg.desktopId, tcp);
168
+ bridges.get(msg.viewerId)?.destroy();
169
+ bridges.set(msg.viewerId, tcp);
167
170
  return;
168
171
  }
169
172
  if (msg.type === 'data' && msg.b64) {
170
- bridges.get(msg.desktopId)?.write(Buffer.from(msg.b64, 'base64'));
173
+ bridges.get(msg.viewerId)?.write(Buffer.from(msg.b64, 'base64'));
171
174
  return;
172
175
  }
173
176
  if (msg.type === 'detach') {
174
- bridges.get(msg.desktopId)?.destroy();
175
- bridges.delete(msg.desktopId);
177
+ bridges.get(msg.viewerId)?.destroy();
178
+ bridges.delete(msg.viewerId);
176
179
  }
177
180
  });
178
181
  const retry = () => {
@@ -86,5 +86,6 @@ export declare function createDashboardScreen(deps: {
86
86
  openTranscript(r: RequestRow): void;
87
87
  openRequests(): void;
88
88
  openHarnesses(): void;
89
+ openDesktops(): void;
89
90
  openLogs(): void;
90
91
  }): Screen;
@@ -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.14.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": [