@addai/node 0.4.0 → 0.5.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.
@@ -7,20 +7,65 @@
7
7
  // every one of those lines lands in the middle of a frame, so the screen
8
8
  // appears to flicker between the UI and log output.
9
9
  //
10
- // Capturing rather than silencing: the lines are kept in a ring buffer, the
11
- // newest is surfaced in the UI, and the whole buffer is replayed to the real
12
- // stdout on exit, so running the daemon under the dashboard loses nothing
13
- // you would have seen without it.
10
+ // Capturing rather than silencing: the lines are kept in a ring buffer that
11
+ // the Logs screen reads, and appended to the daemon log file as they arrive.
12
+ // Nothing is lost by running the daemon under the console and because the
13
+ // file is written live, nothing has to be dumped into your shell on the way
14
+ // out either.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
14
48
  Object.defineProperty(exports, "__esModule", { value: true });
15
49
  exports.captureConsole = captureConsole;
16
- const MAX_LINES = 500;
17
- function captureConsole() {
50
+ const fs = __importStar(require("fs"));
51
+ const MAX_LINES = 2000;
52
+ function captureConsole(opts = {}) {
18
53
  const ring = [];
54
+ const listeners = [];
19
55
  const original = {
20
56
  log: console.log.bind(console),
21
57
  warn: console.warn.bind(console),
22
58
  error: console.error.bind(console),
23
59
  };
60
+ let sink = null;
61
+ if (opts.logFile) {
62
+ try {
63
+ sink = fs.openSync(opts.logFile, 'a');
64
+ }
65
+ catch {
66
+ sink = null;
67
+ }
68
+ }
24
69
  const format = (args) => args.map(a => {
25
70
  if (typeof a === 'string')
26
71
  return a;
@@ -34,9 +79,22 @@ function captureConsole() {
34
79
  }
35
80
  }).join(' ');
36
81
  const push = (level) => (...args) => {
37
- ring.push({ level, text: format(args), at: Date.now() });
82
+ const line = { level, text: format(args), at: Date.now() };
83
+ ring.push(line);
38
84
  if (ring.length > MAX_LINES)
39
85
  ring.shift();
86
+ if (sink !== null) {
87
+ try {
88
+ fs.writeSync(sink, `${line.text}\n`);
89
+ }
90
+ catch { /* disk full, keep the UI alive */ }
91
+ }
92
+ for (const fn of listeners) {
93
+ try {
94
+ fn(line);
95
+ }
96
+ catch { /* a listener must never break logging */ }
97
+ }
40
98
  };
41
99
  console.log = push('log');
42
100
  console.warn = push('warn');
@@ -45,18 +103,18 @@ function captureConsole() {
45
103
  lines: () => [...ring],
46
104
  last: () => (ring.length ? ring[ring.length - 1] : null),
47
105
  count: () => ring.length,
48
- restoreAndReplay() {
106
+ onLine(fn) { listeners.push(fn); },
107
+ restore() {
49
108
  console.log = original.log;
50
109
  console.warn = original.warn;
51
110
  console.error = original.error;
52
- for (const l of ring) {
53
- if (l.level === 'error')
54
- original.error(l.text);
55
- else if (l.level === 'warn')
56
- original.warn(l.text);
57
- else
58
- original.log(l.text);
111
+ if (sink !== null) {
112
+ try {
113
+ fs.closeSync(sink);
114
+ }
115
+ catch { /* already gone */ }
59
116
  }
117
+ sink = null;
60
118
  },
61
119
  };
62
120
  }
@@ -1,9 +1,11 @@
1
+ import { type AppHost, type Screen } from './app';
1
2
  import type { DataLayer, NodeSelf, NodeStats, RequestRow } from './data';
2
- import type { AppHost, Screen } from './app';
3
3
  export interface DashboardState {
4
4
  self: NodeSelf | null;
5
5
  stats: NodeStats | null;
6
- requests: RequestRow[];
6
+ /** Recent requests, newest first — the NOW band reads the live ones off
7
+ * the top and the idle line reads the most recent finish. */
8
+ recent: RequestRow[];
7
9
  sel: number;
8
10
  spin: number;
9
11
  pid: number | null;
@@ -14,24 +16,21 @@ export interface DashboardState {
14
16
  offline: boolean;
15
17
  now: number;
16
18
  version: string;
17
- /** Newest line the daemon logged while the UI owned the screen. */
18
- lastLog: string | null;
19
+ /** How many daemon log lines have been captured this session. */
19
20
  logCount: number;
20
21
  }
21
- export declare const FEED_ROWS = 12;
22
- /** Trigger sources are DB-shaped ('self_improve_distil'); the column is
23
- * nine columns wide and "self_impr" tells you nothing. */
24
- export declare function shortVia(via: string | null | undefined): string;
25
- export declare function requestLine(r: RequestRow, now: number, selected: boolean, width: number): string;
26
- /** The landing screen's menu. Activity and Harnesses are places you go,
27
- * not things that crowd the overview. */
22
+ /** The landing screen's menu. */
28
23
  export declare const MENU: Array<{
29
24
  key: string;
30
25
  label: string;
31
26
  hint: string;
32
27
  shortcut: string;
33
28
  }>;
34
- export declare function renderDashboard(st: DashboardState, width: number): string[];
29
+ /** Live rows the NOW band shows before it starts counting the rest. */
30
+ export declare const MAX_NOW_ROWS = 6;
31
+ export declare function liveRequests(rows: RequestRow[]): RequestRow[];
32
+ export declare function nowLines(st: DashboardState, width: number, rows: number): string[];
33
+ export declare function renderDashboard(st: DashboardState, width: number, height: number): string[];
35
34
  export declare function createDashboardScreen(deps: {
36
35
  data: DataLayer;
37
36
  host: AppHost;
@@ -39,4 +38,5 @@ export declare function createDashboardScreen(deps: {
39
38
  openTranscript(r: RequestRow): void;
40
39
  openRequests(): void;
41
40
  openHarnesses(): void;
41
+ openLogs(): void;
42
42
  }): Screen;
@@ -1,87 +1,29 @@
1
1
  "use strict";
2
- // Home screen: is this node alive, what has it been doing, what is it
3
- // doing right now.
2
+ // The cockpit: is this node alive, what is it doing right now, and what has
3
+ // it done today.
4
4
  //
5
- // Status vocabulary comes from the DB, where terminal success is
6
- // 'completed' and 'canceled' is a deliberate user actionshown apart
7
- // from failures so a healthy node isn't slandered by its own cancels.
5
+ // The landing screen answers "is the box earning its keep" without a
6
+ // keystroke. Everything that needs scrolling history, harnesses, logs is
7
+ // a place you go, not something that crowds the overview.
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.MENU = exports.FEED_ROWS = void 0;
10
- exports.shortVia = shortVia;
11
- exports.requestLine = requestLine;
9
+ exports.MAX_NOW_ROWS = exports.MENU = void 0;
10
+ exports.liveRequests = liveRequests;
11
+ exports.nowLines = nowLines;
12
12
  exports.renderDashboard = renderDashboard;
13
13
  exports.createDashboardScreen = createDashboardScreen;
14
14
  const render_1 = require("./render");
15
- exports.FEED_ROWS = 12;
16
- const ACTIVE = new Set(['pending', 'starting', 'running']);
17
- const CANCELLED = new Set(['canceled', 'cancelled']);
18
- /** Trigger sources are DB-shaped ('self_improve_distil'); the column is
19
- * nine columns wide and "self_impr" tells you nothing. */
20
- function shortVia(via) {
21
- if (!via)
22
- return '—';
23
- const map = {
24
- self_improve_distil: 'distil',
25
- self_improve: 'improve',
26
- entity_chat: 'chat',
27
- chat: 'chat',
28
- schedule: 'schedule',
29
- flow: 'flow',
30
- api: 'api',
31
- studio: 'studio',
32
- };
33
- return map[via] ?? via;
34
- }
35
- function statusLabel(status) {
36
- if (status === 'completed')
37
- return 'DONE';
38
- if (CANCELLED.has(status))
39
- return 'CANCEL';
40
- return status.toUpperCase();
41
- }
42
- function statusCell(status) {
43
- const label = statusLabel(status).padEnd(8).slice(0, 8);
44
- if (ACTIVE.has(status))
45
- return (0, render_1.cyan)(label);
46
- if (status === 'completed')
47
- return (0, render_1.green)(label);
48
- if (CANCELLED.has(status))
49
- return (0, render_1.grey)(label);
50
- return (0, render_1.red)(label);
51
- }
52
- function dot(status) {
53
- if (ACTIVE.has(status))
54
- return (0, render_1.cyan)('⏺');
55
- if (status === 'completed')
56
- return (0, render_1.green)('⏺');
57
- if (CANCELLED.has(status))
58
- return (0, render_1.grey)('⏺');
59
- return (0, render_1.red)('⏺');
60
- }
61
- function durationOf(r, now) {
62
- const start = r.picked_up_at ?? r.created_at;
63
- if (!start)
64
- return null;
65
- const end = r.finished_at ? new Date(r.finished_at).getTime() : now;
66
- return Math.max(0, end - new Date(start).getTime());
67
- }
68
- function requestLine(r, now, selected, width) {
69
- const entity = (0, render_1.padEndVisible)((0, render_1.truncate)(r.entity_name ?? '—', 12), 12);
70
- const agent = `${r.agent}/${r.mode}`.padEnd(12).slice(0, 12);
71
- const model = (r.model ?? '—').replace(/^claude-/, '').padEnd(7).slice(0, 7);
72
- const via = shortVia(r.issued_via).padEnd(9).slice(0, 9);
73
- const dur = (0, render_1.fmtDuration)(durationOf(r, now)).padStart(7);
74
- // A run that failed once and then succeeded on retry still carries the
75
- // old error_code. Showing it on a DONE row reads as "this broke" when
76
- // the work actually landed — so the code is only for rows that ended badly.
77
- const ended_badly = r.status !== 'completed' && !ACTIVE.has(r.status);
78
- const tail = r.error_code && ended_badly
79
- ? (0, render_1.red)(`[${r.error_code}]`)
80
- : (r.prompt ?? '').replace(/\s+/g, ' ');
81
- const prefix = `${dot(r.status)} ${statusCell(r.status)} ${entity} ${(0, render_1.dim)(agent)} ${(0, render_1.dim)(model)} ${(0, render_1.dim)(via)} ${dur} `;
82
- const room = Math.max(8, width - 2 - (0, render_1.visibleWidth)(prefix));
83
- const line = `${prefix}${(0, render_1.truncate)(tail, room)}`;
84
- return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
15
+ const request_row_1 = require("./request-row");
16
+ const app_1 = require("./app");
17
+ /** The landing screen's menu. */
18
+ exports.MENU = [
19
+ { key: 'activity', label: 'Activity', hint: 'every request this node has handled', shortcut: 'a' },
20
+ { key: 'harnesses', label: 'Harnesses', hint: 'install / log in agent CLIs', shortcut: 'h' },
21
+ { key: 'logs', label: 'Logs', hint: 'what the daemon is saying', shortcut: 'l' },
22
+ ];
23
+ /** Live rows the NOW band shows before it starts counting the rest. */
24
+ exports.MAX_NOW_ROWS = 6;
25
+ function liveRequests(rows) {
26
+ return rows.filter(r => request_row_1.ACTIVE.has(r.status));
85
27
  }
86
28
  function headerLines(st) {
87
29
  if (!st.paired) {
@@ -121,56 +63,110 @@ function statsLines(st) {
121
63
  (0, render_1.dim)(`Week ${s.week} Month ${s.month} Total ${s.total}`),
122
64
  ];
123
65
  }
124
- /** The landing screen's menu. Activity and Harnesses are places you go,
125
- * not things that crowd the overview. */
126
- exports.MENU = [
127
- { key: 'activity', label: 'Activity', hint: 'requests this node has handled', shortcut: 'a' },
128
- { key: 'harnesses', label: 'Harnesses', hint: 'install / log in agent CLIs', shortcut: 'h' },
129
- ];
130
- function renderDashboard(st, width) {
131
- const out = [];
132
- out.push(...(0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`ainode v${st.version}`)}`, headerLines(st), width));
133
- out.push('');
134
- if (st.paired) {
135
- out.push(...statsLines(st).map(l => ` ${l}`));
136
- out.push('');
137
- exports.MENU.forEach((m, i) => {
138
- const selected = i === st.sel;
139
- const label = selected ? (0, render_1.bold)(m.label.padEnd(12)) : m.label.padEnd(12);
140
- const marker = selected ? (0, render_1.cyan)('❯') : ' ';
141
- out.push(`${marker} ${label} ${(0, render_1.dim)(m.hint)} ${(0, render_1.dim)(m.shortcut)}`);
142
- });
66
+ /** The most recent request that actually finished, for the idle line. */
67
+ function lastFinished(rows) {
68
+ return rows.find(r => r.finished_at) ?? null;
69
+ }
70
+ function nowLines(st, width, rows) {
71
+ const live = liveRequests(st.recent);
72
+ const out = [` ${(0, render_1.bold)('NOW')}`];
73
+ if (live.length === 0) {
74
+ const last = lastFinished(st.recent);
75
+ out.push(last
76
+ ? (0, render_1.dim)(` idle — last run finished ${(0, render_1.fmtRelative)(last.finished_at, st.now)}`)
77
+ : (0, render_1.dim)(' idle'));
78
+ return out;
143
79
  }
144
- out.push('');
145
- if (st.lastLog) {
146
- out.push((0, render_1.dim)(` log ${st.lastLog}`));
147
- if (st.logCount > 1)
148
- out.push((0, render_1.dim)(` ${st.logCount} lines captured printed on exit`));
80
+ out.push((0, request_row_1.requestHeader)(width));
81
+ const shown = live.slice(0, Math.max(1, rows));
82
+ shown.forEach(r => out.push((0, request_row_1.requestLine)(r, st.now, false, width, st.spin)));
83
+ if (live.length > shown.length) {
84
+ out.push((0, render_1.dim)(` +${live.length - shown.length} more running`));
85
+ }
86
+ return out;
87
+ }
88
+ function renderDashboard(st, width, height) {
89
+ const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`ainode v${st.version}`)}`, headerLines(st), width);
90
+ if (!st.paired) {
91
+ return [...head, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
149
92
  }
150
- out.push('');
151
- out.push((0, render_1.dim)(' ↑↓ move ⏎ open r refresh q quit'));
152
- return out.map(l => (0, render_1.truncate)(l, width));
93
+ const stats = statsLines(st).map(l => ` ${l}`);
94
+ // The shortcut letters line up in a column of their own, so the eye can
95
+ // find them without reading the hints.
96
+ const hintWidth = Math.max(...exports.MENU.map(m => m.hint.length)) + 2;
97
+ const menu = exports.MENU.map((m, i) => {
98
+ const selected = i === st.sel;
99
+ const label = selected ? (0, render_1.bold)(m.label.padEnd(11)) : m.label.padEnd(11);
100
+ const cursor = selected ? (0, render_1.cyan)('❯') : ' ';
101
+ const badge = m.key === 'logs' && st.logCount ? `${st.logCount} lines` : '';
102
+ return `${cursor} ${label} ${(0, render_1.dim)(m.hint.padEnd(hintWidth))}${(0, render_1.dim)(badge.padEnd(10))}${(0, render_1.dim)(m.shortcut)}`;
103
+ });
104
+ const foot = (0, app_1.footerHint)([
105
+ { keys: '↑↓', label: 'move' },
106
+ { keys: '⏎', label: 'open' },
107
+ { keys: 'r', label: 'refresh' },
108
+ { keys: '?', label: 'keys' },
109
+ { keys: 'q', label: 'quit' },
110
+ ]);
111
+ // Everything but the NOW band is fixed height, so the band takes what is
112
+ // left: heading + header row + rows + the "+N more" line.
113
+ const fixed = head.length + 1 + stats.length + 1 + 3 + 1 + menu.length + 1 + 1;
114
+ const room = Math.min(exports.MAX_NOW_ROWS, Math.max(1, height - fixed));
115
+ return [
116
+ ...head,
117
+ '',
118
+ ...stats,
119
+ '',
120
+ ...nowLines(st, width, room),
121
+ '',
122
+ ...menu,
123
+ '',
124
+ foot,
125
+ ].map(l => (0, render_1.truncate)(l, width)).slice(0, height);
153
126
  }
154
127
  function createDashboardScreen(deps) {
155
128
  const st = deps.state;
156
- // The landing screen shows identity and numbers only, so it fetches two
157
- // rows of counters — not the request feed. Activity pages that in itself.
158
129
  const refresh = async () => {
159
130
  st.now = Date.now();
160
- const [self, stats] = await Promise.all([deps.data.self(), deps.data.stats()]);
131
+ const [self, stats, recent] = await Promise.all([
132
+ deps.data.self(),
133
+ deps.data.stats(),
134
+ // Enough rows to hold every plausible concurrent run plus something
135
+ // finished for the idle line, and small enough to poll every 2s.
136
+ deps.data.requests({ limit: 20 }),
137
+ ]);
161
138
  if (self)
162
139
  st.self = self;
163
140
  if (stats)
164
141
  st.stats = stats;
142
+ if (recent.length)
143
+ st.recent = recent;
165
144
  st.offline = deps.data.offline();
166
145
  deps.host.redraw();
167
146
  };
168
147
  return {
169
148
  id: 'dashboard',
170
149
  title: '+Ai Node',
171
- render: (width) => renderDashboard(st, width),
172
- pollMs: () => (st.stats && st.stats.active > 0 ? 2000 : 5000),
150
+ render: (width, height) => renderDashboard(st, width, height),
151
+ // Poll hard while something is moving; an idle node doesn't need traffic.
152
+ pollMs: () => (liveRequests(st.recent).length > 0 || st.inflight > 0 ? 2000 : 5000),
173
153
  poll: refresh,
154
+ tick(n) {
155
+ st.spin = n;
156
+ // Only the NOW band animates, and only when something is in it.
157
+ if (liveRequests(st.recent).length === 0)
158
+ return false;
159
+ st.now = Date.now();
160
+ return true;
161
+ },
162
+ keys: () => [
163
+ { keys: '↑↓ / jk', label: 'move between destinations' },
164
+ { keys: '⏎', label: 'open the selected destination' },
165
+ { keys: 'a', label: 'activity — full request history' },
166
+ { keys: 'h', label: 'harnesses — install / log in agent CLIs' },
167
+ { keys: 'l', label: 'logs — daemon output' },
168
+ { keys: 'r', label: 'refresh now' },
169
+ ],
174
170
  async onKey(key) {
175
171
  if (key.name === 'up' || key.name === 'k') {
176
172
  st.sel = Math.max(0, st.sel - 1);
@@ -190,13 +186,20 @@ function createDashboardScreen(deps) {
190
186
  deps.openHarnesses();
191
187
  return;
192
188
  }
189
+ if (key.name === 'l') {
190
+ deps.openLogs();
191
+ return;
192
+ }
193
193
  if (key.name === 'r') {
194
194
  await refresh();
195
195
  return;
196
196
  }
197
197
  if (key.name === 'return') {
198
- if (exports.MENU[st.sel]?.key === 'harnesses')
198
+ const target = exports.MENU[st.sel]?.key;
199
+ if (target === 'harnesses')
199
200
  deps.openHarnesses();
201
+ else if (target === 'logs')
202
+ deps.openLogs();
200
203
  else
201
204
  deps.openRequests();
202
205
  }
@@ -1,16 +1,53 @@
1
+ import { type HarnessId } from '../harness-registry';
2
+ import { type AppHost, type Screen } from './app';
3
+ export type Probe = {
4
+ available?: boolean;
5
+ authed?: boolean;
6
+ version?: string;
7
+ account?: string;
8
+ plan?: string;
9
+ accountKind?: string;
10
+ models?: string[];
11
+ };
12
+ export type Caps = Record<string, Probe>;
13
+ export interface HarnessesState {
14
+ caps: Caps | null;
15
+ sel: number;
16
+ spin: number;
17
+ /** Spinner label while an action runs; null when idle. */
18
+ busy: string | null;
19
+ /** Enter on an authed row arms this; y fires it. */
20
+ confirmLogout: HarnessId | null;
21
+ /** Tail of an install's npm output. */
22
+ log: string[];
23
+ note: string | null;
24
+ }
25
+ export declare function actionFor(id: HarnessId, p: Probe | undefined): string;
1
26
  /**
2
- * Run the harness manager.
3
- *
4
- * The returned promise resolves when the USER LEAVES the screen — not when
5
- * the first capability probe finishes. Resolving early is what made the
6
- * embedded case flicker: the dashboard re-attached its own keypress handler
7
- * and repainted while this screen was still live, and the two fought over
8
- * the terminal.
27
+ * Harness CLIs report their version in whatever shape they feel like:
28
+ * `2.1.220 (Claude Code)`, `codex-cli 0.139.0`, `0.52.0`. Prefixing all of
29
+ * those with a `v` produced `vcodex-cli 0.139.0`. Pull out the version
30
+ * itself and leave the branding to the label column.
31
+ */
32
+ export declare function formatVersion(v: string | undefined): string;
33
+ export declare function accountLabel(p: Probe | undefined): string;
34
+ /**
35
+ * Column widths.
9
36
  *
10
- * `embedded` distinguishes `ainode harnesses` (q exits the process) from
11
- * the dashboard's `h` (q hands control back to the caller). Ctrl-C always
12
- * ends the process.
37
+ * The account and the effort ladder vary most, so they take the spare width;
38
+ * version and action are fixed at what their values actually need. The old
39
+ * fixed widths cut `codex-cli` down to `vcodex-cli` and
40
+ * `low/medium/high/xhigh` to `low/medium/high/x`.
13
41
  */
14
- export declare function runHarnessesTui(opts?: {
15
- embedded?: boolean;
16
- }): Promise<void>;
42
+ export declare function harnessColumns(width: number): number[];
43
+ export declare function harnessRow(id: HarnessId, p: Probe | undefined, selected: boolean, width: number): string;
44
+ export declare function renderHarnesses(st: HarnessesState, width: number, height: number): string[];
45
+ export declare function logoutSelected(id: HarnessId): string | null;
46
+ export declare function createHarnessesScreen(deps: {
47
+ host: AppHost;
48
+ /** Hand the raw terminal to a vendor CLI, then take it back. */
49
+ suspend(fn: () => void | Promise<void>): Promise<void>;
50
+ probe?: () => Promise<Caps>;
51
+ }): Screen;
52
+ /** `ainode harnesses` on a pipe: no screen, just the facts. */
53
+ export declare function plainStatus(): Promise<void>;