@addai/node 0.5.0 → 0.5.1

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.
@@ -19,7 +19,16 @@ function buildCapturePayload(req, userText, assistantText, transcriptRef) {
19
19
  transcript_ref: transcriptRef,
20
20
  };
21
21
  }
22
- async function postCapture(baseUrl, serviceToken, payload, timeoutMs = 5000) {
22
+ // timeoutMs default is 15000 (not 5000): the entity-memory-capture edge fn
23
+ // synchronously summarizes each turn via an Anthropic Haiku call on its
24
+ // DEFAULT path, so its server-side latency is inherently ~3-5s (measured over
25
+ // 24h: p50 ~3.4s, p95 ~4.75s, p99 ~6.35s). A 5s abort clipped that long tail,
26
+ // surfacing "operation was aborted due to timeout" and dropping those turns'
27
+ // captures. postCapture is fire-and-forget (void, post-terminal — see
28
+ // session-runner notifyCaptureIfEnabled), so a longer timeout NEVER slows a
29
+ // run; it only lets slow/tail captures land. 15s clears observed p99 with 2x
30
+ // headroom for network/TLS.
31
+ async function postCapture(baseUrl, serviceToken, payload, timeoutMs = 15000) {
23
32
  try {
24
33
  const res = await fetch(`${baseUrl}/functions/v1/entity-memory-capture`, {
25
34
  method: 'POST',
@@ -29,6 +29,17 @@ export declare const MENU: Array<{
29
29
  /** Live rows the NOW band shows before it starts counting the rest. */
30
30
  export declare const MAX_NOW_ROWS = 6;
31
31
  export declare function liveRequests(rows: RequestRow[]): RequestRow[];
32
+ /**
33
+ * The node's own state, as a label.
34
+ *
35
+ * A daemon is running — we either started it or the lockfile proved it — so
36
+ * the node IS online, whatever the server has got round to saying. Waiting
37
+ * for `runtime_self` to come back before admitting that meant the first
38
+ * second of every session claimed the node was in an unknown state.
39
+ */
40
+ export declare function stateLabel(st: DashboardState): string;
41
+ /** 1204 reads as a year; 1,204 reads as a count. */
42
+ export declare function fmtCount(n: number | null | undefined): string;
32
43
  export declare function nowLines(st: DashboardState, width: number, rows: number): string[];
33
44
  export declare function renderDashboard(st: DashboardState, width: number, height: number): string[];
34
45
  export declare function createDashboardScreen(deps: {
@@ -8,6 +8,8 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.MAX_NOW_ROWS = exports.MENU = void 0;
10
10
  exports.liveRequests = liveRequests;
11
+ exports.stateLabel = stateLabel;
12
+ exports.fmtCount = fmtCount;
11
13
  exports.nowLines = nowLines;
12
14
  exports.renderDashboard = renderDashboard;
13
15
  exports.createDashboardScreen = createDashboardScreen;
@@ -16,51 +18,86 @@ const request_row_1 = require("./request-row");
16
18
  const app_1 = require("./app");
17
19
  /** The landing screen's menu. */
18
20
  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' },
21
+ { key: 'activity', label: 'Activity', hint: 'Every request this node has handled', shortcut: 'a' },
22
+ { key: 'harnesses', label: 'Harnesses', hint: 'Install / log in agent CLIs', shortcut: 'h' },
23
+ { key: 'logs', label: 'Logs', hint: 'What the daemon is saying', shortcut: 'l' },
22
24
  ];
23
25
  /** Live rows the NOW band shows before it starts counting the rest. */
24
26
  exports.MAX_NOW_ROWS = 6;
25
27
  function liveRequests(rows) {
26
28
  return rows.filter(r => request_row_1.ACTIVE.has(r.status));
27
29
  }
30
+ /**
31
+ * The node's own state, as a label.
32
+ *
33
+ * A daemon is running — we either started it or the lockfile proved it — so
34
+ * the node IS online, whatever the server has got round to saying. Waiting
35
+ * for `runtime_self` to come back before admitting that meant the first
36
+ * second of every session claimed the node was in an unknown state.
37
+ */
38
+ function stateLabel(st) {
39
+ const reported = st.self?.effective_status;
40
+ if (!reported)
41
+ return (0, render_1.green)('Online');
42
+ if (reported === 'online')
43
+ return (0, render_1.green)('Online');
44
+ return (0, render_1.yellow)(reported.charAt(0).toUpperCase() + reported.slice(1));
45
+ }
28
46
  function headerLines(st) {
29
47
  if (!st.paired) {
30
48
  return [
31
- (0, render_1.yellow)('not paired'),
49
+ (0, render_1.yellow)('Not paired'),
32
50
  (0, render_1.dim)('run `ainode` and follow the prompt to vault.add.ai/entity/connect/<CODE>'),
33
51
  ];
34
52
  }
35
53
  const self = st.self;
36
- const name = self?.name ?? self?.hostname ?? 'this node';
37
- const state = self?.effective_status === 'online'
38
- ? (0, render_1.green)('online')
39
- : (0, render_1.yellow)(self?.effective_status ?? 'unknown');
54
+ const name = self?.name ?? self?.hostname ?? 'This node';
40
55
  const paired = self?.created_at ? `paired ${(0, render_1.fmtRelative)(self.created_at, st.now)}` : '';
41
- const first = `${(0, render_1.bold)(name)} ${(0, render_1.dim)('·')} ${state} ${(0, render_1.dim)('·')} ${(0, render_1.dim)(paired)}`;
56
+ const first = `${(0, render_1.bold)(name)} ${(0, render_1.dim)('·')} ${stateLabel(st)}${paired ? ` ${(0, render_1.dim)('·')} ${(0, render_1.dim)(paired)}` : ''}`;
42
57
  const up = st.startedAt ? (0, render_1.fmtDuration)(st.now - st.startedAt) : '—';
43
58
  const daemon = st.viewerMode
44
- ? `${(0, render_1.cyan)('⏺')} viewer ${(0, render_1.dim)( another ainode owns this node')} ${(0, render_1.dim)(`pid ${st.pid ?? '?'}`)}`
45
- : `${(0, render_1.green)('⏺')} running ${(0, render_1.dim)(`pid ${st.pid ?? '?'}`)} ${(0, render_1.dim)(`up ${up}`)} ${(0, render_1.dim)(`${st.inflight} in flight`)}`;
46
- const chip = st.offline ? ` ${(0, render_1.yellow)('⚠ offline · retrying')}` : '';
59
+ ? `${(0, render_1.cyan)('⏺')} Viewer ${(0, render_1.dim)(`pid ${st.pid ?? '?'}`)} ${(0, render_1.dim)( another process owns this node')}`
60
+ : `${(0, render_1.green)('⏺')} Running ${(0, render_1.dim)(`pid ${st.pid ?? '?'}`)} ${(0, render_1.dim)(`up ${up}`)} ${(0, render_1.dim)(`${st.inflight} in flight`)}`;
61
+ const chip = st.offline ? ` ${(0, render_1.yellow)('⚠ Offline · retrying')}` : '';
47
62
  return [first, daemon + chip];
48
63
  }
64
+ /** 1204 reads as a year; 1,204 reads as a count. */
65
+ function fmtCount(n) {
66
+ if (n == null)
67
+ return '—';
68
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
69
+ }
70
+ /** A labelled figure, padded so the labels line up down the screen. */
71
+ function stat(label, value, width = 14) {
72
+ return (0, render_1.padEndVisible)(`${(0, render_1.dim)(label)} ${value}`, width);
73
+ }
49
74
  function statsLines(st) {
50
75
  const s = st.stats;
51
- if (!s)
52
- return [(0, render_1.dim)('stats unavailable')];
76
+ // Keep the shape while the first fetch is in flight, so the band doesn't
77
+ // pop into place a second after the header.
78
+ if (!s) {
79
+ return [
80
+ `${stat('Today', (0, render_1.dim)('—'))}${stat('24h', (0, render_1.dim)('—'), 26)}${stat('Avg', (0, render_1.dim)('—'))}`,
81
+ `${stat('Week', (0, render_1.dim)('—'))}${stat('Month', (0, render_1.dim)('—'), 26)}${stat('Total', (0, render_1.dim)('—'))}`,
82
+ ];
83
+ }
53
84
  const agents = Object.entries(s.by_agent)
54
85
  .sort((a, b) => b[1] - a[1])
55
- .map(([k, v]) => `${k} ${v}`)
56
- .join(' · ');
57
- const cancels = s.canceled_24h ? (0, render_1.dim)(` ${s.canceled_24h} cancelled`) : '';
58
- // The ✓/✗/avg figures are a rolling 24h window while "Today" is the
59
- // calendar day, so they are labelled — otherwise ✓3 next to Today 2
60
- // looks like an arithmetic bug.
86
+ .map(([k, v]) => `${k.charAt(0).toUpperCase()}${k.slice(1)} ${fmtCount(v)}`)
87
+ .join((0, render_1.dim)(' · '));
88
+ // The ✓/✗ figures are a rolling 24h window while "Today" is the calendar
89
+ // day, so the window is labelled otherwise ✓3 next to Today 2 looks
90
+ // like an arithmetic bug.
91
+ const outcomes = [
92
+ (0, render_1.green)(`✓ ${fmtCount(s.succeeded_24h)}`),
93
+ s.failed_24h ? (0, render_1.red)(`✗ ${fmtCount(s.failed_24h)}`) : (0, render_1.dim)('✗ 0'),
94
+ s.canceled_24h ? (0, render_1.grey)(`⊘ ${fmtCount(s.canceled_24h)}`) : '',
95
+ ].filter(Boolean).join(' ');
96
+ // Both rows share one three-column grid, so Month sits under 24h and
97
+ // Total under Avg rather than the two rows drifting apart.
61
98
  return [
62
- `${(0, render_1.bold)(`Today ${s.today}`)} ${(0, render_1.dim)('·')} ${(0, render_1.dim)('24h')} ${(0, render_1.green)(`✓ ${s.succeeded_24h}`)} ${s.failed_24h ? (0, render_1.red)(`✗ ${s.failed_24h}`) : (0, render_1.dim)('✗ 0')}${cancels} ${(0, render_1.dim)(`avg ${(0, render_1.fmtDuration)(s.avg_duration_ms)}`)} ${(0, render_1.dim)(agents)}`,
63
- (0, render_1.dim)(`Week ${s.week} Month ${s.month} Total ${s.total}`),
99
+ `${stat('Today', (0, render_1.bold)(fmtCount(s.today)))}${stat('24h', outcomes, 26)}${stat('Avg', (0, render_1.fmtDuration)(s.avg_duration_ms))}`,
100
+ `${stat('Week', fmtCount(s.week))}${stat('Month', fmtCount(s.month), 26)}${stat('Total', fmtCount(s.total))}${(0, render_1.dim)(agents)}`,
64
101
  ];
65
102
  }
66
103
  /** The most recent request that actually finished, for the idle line. */
@@ -69,12 +106,12 @@ function lastFinished(rows) {
69
106
  }
70
107
  function nowLines(st, width, rows) {
71
108
  const live = liveRequests(st.recent);
72
- const out = [` ${(0, render_1.bold)('NOW')}`];
109
+ const out = [` ${(0, render_1.bold)('Now')}`];
73
110
  if (live.length === 0) {
74
111
  const last = lastFinished(st.recent);
75
112
  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'));
113
+ ? (0, render_1.dim)(` Idle — last run finished ${(0, render_1.fmtRelative)(last.finished_at, st.now)}`)
114
+ : (0, render_1.dim)(' Idle'));
78
115
  return out;
79
116
  }
80
117
  out.push((0, request_row_1.requestHeader)(width));
@@ -86,7 +123,7 @@ function nowLines(st, width, rows) {
86
123
  return out;
87
124
  }
88
125
  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);
126
+ const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`@addai/node v${st.version}`)}`, headerLines(st), width);
90
127
  if (!st.paired) {
91
128
  return [...head, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
92
129
  }
@@ -89,10 +89,10 @@ function formatVersion(v) {
89
89
  }
90
90
  function accountLabel(p) {
91
91
  if (!p?.available)
92
- return 'not installed';
92
+ return 'Not installed';
93
93
  if (!p.authed)
94
- return 'not logged in';
95
- const who = p.account ?? p.accountKind ?? 'logged in';
94
+ return 'Not logged in';
95
+ const who = p.account ?? p.accountKind ?? 'Logged in';
96
96
  return p.plan ? `${who} · ${p.plan}` : who;
97
97
  }
98
98
  /**
@@ -120,7 +120,10 @@ function harnessRow(id, p, selected, width) {
120
120
  const spec = harness_registry_1.HARNESSES[id];
121
121
  const w = harnessColumns(width);
122
122
  const act = actionFor(id, p);
123
- const action = act === 'ok' ? (0, render_1.green)('✓ ready') : act === 'log out' ? (0, render_1.dim)(`⏎ ${act}`) : (0, render_1.cyan)(`⏎ ${act}`);
123
+ // The verb is Title Case in the row the same way every other label is; the
124
+ // value actionFor returns stays lowercase because the key handler reads it.
125
+ const verb = act.charAt(0).toUpperCase() + act.slice(1);
126
+ const action = act === 'ok' ? (0, render_1.green)('✓ Ready') : act === 'log out' ? (0, render_1.dim)(`⏎ ${verb}`) : (0, render_1.cyan)(`⏎ ${verb}`);
124
127
  const line = (0, render_1.tableRow)([
125
128
  dot(p),
126
129
  selected ? (0, render_1.bold)(spec.label) : spec.label,
@@ -135,14 +138,14 @@ function harnessRow(id, p, selected, width) {
135
138
  function renderHarnesses(st, width, height) {
136
139
  const out = [];
137
140
  const authed = st.caps ? harness_registry_1.HARNESS_IDS.filter(id => st.caps?.[id]?.authed).length : 0;
138
- out.push((0, app_1.heading)('HARNESSES', st.caps ? `${authed}/${harness_registry_1.HARNESS_IDS.length} ready` : 'probing…'));
141
+ out.push((0, app_1.heading)('Harnesses', st.caps ? `${authed}/${harness_registry_1.HARNESS_IDS.length} ready` : 'Probing…'));
139
142
  out.push('');
140
143
  if (!st.caps) {
141
- out.push(` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${(0, render_1.dim)('probing installed harnesses…')}`);
144
+ out.push(` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${(0, render_1.dim)('Probing installed harnesses…')}`);
142
145
  }
143
146
  else {
144
147
  const w = harnessColumns(width);
145
- out.push((0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'HARNESS', 'VERSION', 'ACCOUNT', 'MODELS', 'EFFORTS', ''], w)));
148
+ out.push((0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'Harness', 'Version', 'Account', 'Models', 'Efforts', ''], w)));
146
149
  harness_registry_1.HARNESS_IDS.forEach((id, i) => out.push(harnessRow(id, st.caps?.[id], i === st.sel, width)));
147
150
  }
148
151
  if (st.busy)
@@ -353,7 +356,7 @@ async function plainStatus() {
353
356
  const caps = await (0, capabilities_1.probeCapabilities)();
354
357
  for (const id of harness_registry_1.HARNESS_IDS) {
355
358
  const p = caps[id];
356
- const state = !p?.available ? 'not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'not logged in';
359
+ const state = !p?.available ? 'Not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'Not logged in';
357
360
  console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
358
361
  }
359
362
  }
package/dist/tui/logs.js CHANGED
@@ -40,13 +40,13 @@ function renderLogs(st, lines, width, height, emptyHint) {
40
40
  const scope = st.filter
41
41
  ? `${shown.length} matching “${st.filter}”`
42
42
  : `${shown.length} lines this session`;
43
- out.push((0, app_1.heading)('LOGS', scope));
43
+ out.push((0, app_1.heading)('Logs', scope));
44
44
  out.push('');
45
45
  if (shown.length === 0) {
46
46
  if (st.filter)
47
- out.push((0, render_1.dim)(' nothing matches that filter'));
47
+ out.push((0, render_1.dim)(' Nothing matches that filter'));
48
48
  else
49
- out.push((0, render_1.dim)(` ${emptyHint ?? 'the daemon has said nothing yet'}`));
49
+ out.push((0, render_1.dim)(` ${emptyHint ?? 'The daemon has said nothing yet'}`));
50
50
  }
51
51
  else {
52
52
  const maxScroll = Math.max(0, shown.length - rows);
@@ -10,6 +10,12 @@ export declare const CANCELLED: Set<string>;
10
10
  * this column needs.
11
11
  */
12
12
  export declare function shortVia(via: string | null | undefined): string;
13
+ /**
14
+ * Model ids carry a vendor prefix and a release date the column has no room
15
+ * for: `claude-haiku-4-5-20251001` was rendering as `haiku-4-…`, which loses
16
+ * the one part you were reading it for.
17
+ */
18
+ export declare function shortModel(model: string | null | undefined): string;
13
19
  export declare function statusLabel(status: string): string;
14
20
  export declare function statusColour(status: string): (s: string) => string;
15
21
  /** Live rows get a moving spinner; settled rows get a dot. */
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.CANCELLED = exports.ACTIVE = void 0;
10
10
  exports.shortVia = shortVia;
11
+ exports.shortModel = shortModel;
11
12
  exports.statusLabel = statusLabel;
12
13
  exports.statusColour = statusColour;
13
14
  exports.marker = marker;
@@ -48,14 +49,24 @@ function shortVia(via) {
48
49
  return map[via];
49
50
  return via.replace(/^entity_/, '').replace(/_trigger$/, '');
50
51
  }
52
+ /**
53
+ * Model ids carry a vendor prefix and a release date the column has no room
54
+ * for: `claude-haiku-4-5-20251001` was rendering as `haiku-4-…`, which loses
55
+ * the one part you were reading it for.
56
+ */
57
+ function shortModel(model) {
58
+ if (!model)
59
+ return '—';
60
+ return model.replace(/^claude-/, '').replace(/-\d{8}$/, '');
61
+ }
51
62
  function statusLabel(status) {
52
63
  if (status === 'completed')
53
- return 'DONE';
64
+ return 'Done';
54
65
  if (exports.CANCELLED.has(status))
55
- return 'CANCEL';
66
+ return 'Cancelled';
56
67
  if (status === 'dead_letter')
57
- return 'DEAD';
58
- return status.toUpperCase();
68
+ return 'Dead';
69
+ return status.charAt(0).toUpperCase() + status.slice(1);
59
70
  }
60
71
  function statusColour(status) {
61
72
  if (exports.ACTIVE.has(status))
@@ -91,9 +102,9 @@ function requestColumns(width) {
91
102
  // marker, status, entity, agent/mode, model, via, duration, prompt
92
103
  return (0, render_1.layoutColumns)([
93
104
  { min: 1 },
94
- // STARTING is the longest label; anything shorter than it clips the
105
+ // 'Cancelled' is the longest label; anything shorter than it clips the
95
106
  // status, which is the one column that must never be ambiguous.
96
- { min: 8 },
107
+ { min: 9 },
97
108
  { min: 10, grow: 1 },
98
109
  { min: 12 },
99
110
  // 'haiku-4-5' is nine columns once the claude- prefix is stripped.
@@ -106,7 +117,7 @@ function requestColumns(width) {
106
117
  const ALIGN = ['left', 'left', 'left', 'left', 'left', 'left', 'right', 'left'];
107
118
  function requestHeader(width) {
108
119
  const w = requestColumns(width);
109
- return (0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'STATUS', 'ENTITY', 'AGENT', 'MODEL', 'VIA', 'DUR', 'PROMPT'], w, ALIGN));
120
+ return (0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'Status', 'Entity', 'Agent', 'Model', 'Via', 'Dur', 'Prompt'], w, ALIGN));
110
121
  }
111
122
  function requestLine(r, now, selected, width, spin = 0) {
112
123
  const w = requestColumns(width);
@@ -123,7 +134,7 @@ function requestLine(r, now, selected, width, spin = 0) {
123
134
  colour(statusLabel(r.status)),
124
135
  r.entity_name ?? '—',
125
136
  (0, render_1.dim)(`${r.agent}/${r.mode}`),
126
- (0, render_1.dim)((r.model ?? '—').replace(/^claude-/, '')),
137
+ (0, render_1.dim)(shortModel(r.model)),
127
138
  (0, render_1.dim)(shortVia(r.issued_via)),
128
139
  (0, render_1.dim)((0, render_1.fmtDuration)(durationOf(r, now))),
129
140
  tail,
@@ -41,10 +41,10 @@ function renderRequests(st, width, height) {
41
41
  st.top = windowTop(st.sel, st.top, rows, shown.length);
42
42
  const out = [];
43
43
  const scope = st.filter ? `${shown.length} matching “${st.filter}”` : `${shown.length} loaded`;
44
- out.push((0, app_1.heading)('ACTIVITY', scope));
44
+ out.push((0, app_1.heading)('Activity', scope));
45
45
  out.push('');
46
46
  if (shown.length === 0) {
47
- out.push((0, render_1.dim)(st.filter ? ' no requests match that filter' : ' no requests yet'));
47
+ out.push((0, render_1.dim)(st.filter ? ' No requests match that filter' : ' No requests yet'));
48
48
  }
49
49
  else {
50
50
  out.push((0, request_row_1.requestHeader)(width));
@@ -57,9 +57,9 @@ function renderRequests(st, width, height) {
57
57
  while (out.length < height - 2)
58
58
  out.push('');
59
59
  if (st.loading)
60
- out.push((0, render_1.dim)(' loading more…'));
60
+ out.push((0, render_1.dim)(' Loading more…'));
61
61
  else if (st.exhausted)
62
- out.push((0, render_1.dim)(` end of history — ${shown.length} rows`));
62
+ out.push((0, render_1.dim)(` End of history — ${shown.length} rows`));
63
63
  else
64
64
  out.push((0, render_1.dim)(' ↓ past the last row loads more'));
65
65
  out.push(st.filtering
package/dist/tui.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runHarnessesTui(): Promise<void>;
package/dist/tui.js ADDED
@@ -0,0 +1,314 @@
1
+ "use strict";
2
+ // `entities-runtime harnesses` — interactive harness manager in the
3
+ // Claude Code terminal idiom: alt-screen, rounded panels, dim chrome,
4
+ // ❯ selection, braille spinner, ⏺ status dots. Hand-rolled ANSI — the
5
+ // daemon deliberately has no TUI framework dependency.
6
+ //
7
+ // Local actions hand the real terminal to the harness CLI itself (there
8
+ // is nothing to bridge when you're already at the machine): install
9
+ // streams npm output into a panel; login leaves the alt screen, runs the
10
+ // CLI's own login flow attached to your TTY, then re-probes on return.
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.runHarnessesTui = runHarnessesTui;
46
+ const child_process_1 = require("child_process");
47
+ const readline = __importStar(require("readline"));
48
+ const capabilities_1 = require("./capabilities");
49
+ const win_1 = require("./win");
50
+ const fs = __importStar(require("fs"));
51
+ const os = __importStar(require("os"));
52
+ const path = __importStar(require("path"));
53
+ const harness_registry_1 = require("./harness-registry");
54
+ /* ── ansi helpers ────────────────────────────────────────────────────── */
55
+ const ESC = '\x1b[';
56
+ const dim = (s) => `${ESC}2m${s}${ESC}22m`;
57
+ const bold = (s) => `${ESC}1m${s}${ESC}22m`;
58
+ const fg = (n, s) => `${ESC}38;5;${n}m${s}${ESC}39m`;
59
+ const green = (s) => fg(114, s);
60
+ const yellow = (s) => fg(179, s);
61
+ const grey = (s) => fg(244, s);
62
+ const cyan = (s) => fg(80, s);
63
+ const altOn = () => process.stdout.write(`${ESC}?1049h${ESC}?25l`);
64
+ const altOff = () => process.stdout.write(`${ESC}?1049l${ESC}?25h`);
65
+ const home = () => process.stdout.write(`${ESC}H${ESC}2J`);
66
+ const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
67
+ const width = () => Math.min(process.stdout.columns || 100, 110);
68
+ function panel(title, lines) {
69
+ const w = width() - 2;
70
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
71
+ const top = `${dim('╭─')} ${bold(title)} ${dim('─'.repeat(Math.max(1, w - strip(title).length - 4)) + '╮')}`;
72
+ const body = lines.map(l => {
73
+ const pad = Math.max(0, w - 2 - strip(l).length);
74
+ return `${dim('│')} ${l}${' '.repeat(pad)} ${dim('│')}`;
75
+ });
76
+ const bottom = dim(`╰${'─'.repeat(w)}╯`);
77
+ return [top, ...body, bottom].join('\n');
78
+ }
79
+ function dot(p) {
80
+ if (!p?.available)
81
+ return grey('⏺');
82
+ return p.authed ? green('⏺') : yellow('⏺');
83
+ }
84
+ function actionFor(id, p) {
85
+ if (!p?.available)
86
+ return (0, harness_registry_1.isInstallable)(id) ? 'install' : 'how to install';
87
+ if (!p.authed)
88
+ return 'log in';
89
+ return harness_registry_1.HARNESSES[id].logout ? 'log out' : 'ok';
90
+ }
91
+ function row(id, p, selected) {
92
+ const spec = harness_registry_1.HARNESSES[id];
93
+ const name = spec.label.padEnd(13);
94
+ const ver = (p?.version ? `v${p.version}` : '—').padEnd(12).slice(0, 12);
95
+ const who = (p?.authed
96
+ ? `${p.account ?? p.accountKind ?? 'logged in'}${p.plan ? ` · ${p.plan}` : ''}`
97
+ : p?.available ? 'not logged in' : 'not installed').padEnd(28).slice(0, 28);
98
+ const models = `${p?.models?.length ?? 0} models`.padEnd(10);
99
+ const efforts = spec.efforts.length ? spec.efforts.join('/') : '—';
100
+ const act = actionFor(id, p);
101
+ const actTxt = act === 'ok' ? green('✓') : act === 'log out' ? dim('↵ log out') : cyan(`↵ ${act}`);
102
+ const line = `${dot(p)} ${name} ${dim(ver)} ${who} ${dim(models)} ${dim(efforts.padEnd(26).slice(0, 26))} ${actTxt}`;
103
+ return selected ? `${cyan('❯')} ${bold(line)}` : ` ${line}`;
104
+ }
105
+ function render(st) {
106
+ home();
107
+ const lines = [];
108
+ if (!st.caps) {
109
+ lines.push(`${cyan(SPIN[st.spin % SPIN.length])} probing installed harnesses…`);
110
+ }
111
+ else {
112
+ lines.push(dim(' harness version account models efforts'));
113
+ harness_registry_1.HARNESS_IDS.forEach((id, i) => lines.push(row(id, st.caps[id], i === st.sel)));
114
+ }
115
+ if (st.busy)
116
+ lines.push('', `${cyan(SPIN[st.spin % SPIN.length])} ${st.busy}`);
117
+ if (st.log.length) {
118
+ lines.push('');
119
+ st.log.slice(-8).forEach(l => lines.push(dim(l.slice(0, width() - 8))));
120
+ }
121
+ if (st.note)
122
+ lines.push('', st.note);
123
+ process.stdout.write(panel('Harnesses', lines) + '\n');
124
+ process.stdout.write(dim(' ↑/↓ move · ↵ install / log in · r refresh · q quit\n'));
125
+ }
126
+ /* ── actions ─────────────────────────────────────────────────────────── */
127
+ async function installSelected(st, id, redraw) {
128
+ const spec = harness_registry_1.HARNESSES[id];
129
+ if (!(0, harness_registry_1.isInstallable)(id)) {
130
+ st.note = yellow(`${spec.label}: ${spec.login.instructions}`);
131
+ return;
132
+ }
133
+ st.busy = `installing ${spec.label} (npm i -g ${spec.npmPackage})`;
134
+ st.log = [];
135
+ redraw();
136
+ const inv = (0, win_1.resolveCliInvocation)('npm', ['install', '-g', spec.npmPackage]);
137
+ await new Promise(resolve => {
138
+ const child = (0, child_process_1.spawn)(inv.file, inv.args, { env: process.env });
139
+ const onChunk = (b) => {
140
+ st.log.push(...b.toString('utf8').split('\n').filter(Boolean));
141
+ redraw();
142
+ };
143
+ child.stdout?.on('data', onChunk);
144
+ child.stderr?.on('data', onChunk);
145
+ child.on('exit', code => {
146
+ st.busy = null;
147
+ st.note = code === 0 ? green(`✓ ${spec.label} installed`) : fg(203, `✗ npm exited ${code}`);
148
+ resolve();
149
+ });
150
+ child.on('error', err => { st.busy = null; st.note = fg(203, `✗ ${err.message}`); resolve(); });
151
+ });
152
+ }
153
+ function logoutSelected(id) {
154
+ const spec = harness_registry_1.HARNESSES[id];
155
+ if (!spec.logout)
156
+ return `${spec.label} can only be logged out on the device`;
157
+ if (spec.logout.cmd) {
158
+ const bin = spec.findBinary();
159
+ if (!bin)
160
+ return `${spec.label} is not installed`;
161
+ const inv = (0, win_1.resolveCliInvocation)(bin, spec.logout.cmd);
162
+ const res = (0, child_process_1.spawnSync)(inv.file, inv.args, { stdio: 'ignore', env: process.env, timeout: 60_000 });
163
+ if (res.status !== 0)
164
+ return `logout exited with code ${res.status ?? '?'}`;
165
+ }
166
+ for (const rel of spec.logout.files ?? []) {
167
+ try {
168
+ fs.rmSync(path.join(os.homedir(), rel), { force: true });
169
+ }
170
+ catch { /* best effort */ }
171
+ }
172
+ for (const [rel, varName] of spec.logout.envStrip ?? []) {
173
+ const envPath = path.join(os.homedir(), rel);
174
+ try {
175
+ const body = fs.readFileSync(envPath, 'utf8');
176
+ fs.writeFileSync(envPath, body.split('\n').filter(l => !l.startsWith(`${varName}=`)).join('\n'), { mode: 0o600 });
177
+ }
178
+ catch { /* nothing to strip */ }
179
+ }
180
+ return null;
181
+ }
182
+ function loginSelected(id) {
183
+ const spec = harness_registry_1.HARNESSES[id];
184
+ const bin = spec.findBinary();
185
+ if (!bin)
186
+ return `${spec.label} is not installed`;
187
+ // Hand the real terminal to the CLI — leave the alt screen first.
188
+ altOff();
189
+ console.log(cyan(`\n— ${spec.label} login —`));
190
+ console.log(dim(spec.login.instructions) + '\n');
191
+ const args = spec.login.strategy === 'pty-url-code' ? (spec.login.loginArgs ?? []) : [];
192
+ const inv = (0, win_1.resolveCliInvocation)(bin, args);
193
+ const res = (0, child_process_1.spawnSync)(inv.file, inv.args, { stdio: 'inherit', env: process.env });
194
+ altOn();
195
+ return res.status === 0 ? null : `login exited with code ${res.status ?? '?'}`;
196
+ }
197
+ /* ── entry ───────────────────────────────────────────────────────────── */
198
+ async function plainStatus() {
199
+ const caps = await (0, capabilities_1.probeCapabilities)();
200
+ for (const id of harness_registry_1.HARNESS_IDS) {
201
+ const p = caps[id];
202
+ const state = !p?.available ? 'not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'not logged in';
203
+ console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
204
+ }
205
+ }
206
+ async function runHarnessesTui() {
207
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
208
+ await plainStatus();
209
+ return;
210
+ }
211
+ const st = { caps: null, sel: 0, spin: 0, busy: null, log: [], note: null, confirmLogout: null };
212
+ const redraw = () => render(st);
213
+ altOn();
214
+ readline.emitKeypressEvents(process.stdin);
215
+ process.stdin.setRawMode(true);
216
+ process.stdin.resume();
217
+ const spinTimer = setInterval(() => { st.spin++; if (st.busy || !st.caps)
218
+ redraw(); }, 90);
219
+ const quit = () => {
220
+ clearInterval(spinTimer);
221
+ process.stdin.setRawMode(false);
222
+ process.stdin.pause();
223
+ altOff();
224
+ process.exit(0);
225
+ };
226
+ const reprobe = async () => {
227
+ st.caps = null;
228
+ redraw();
229
+ st.caps = await (0, capabilities_1.probeCapabilities)();
230
+ redraw();
231
+ };
232
+ let acting = false;
233
+ process.stdin.on('keypress', (_str, key) => {
234
+ void (async () => {
235
+ if (!key || acting)
236
+ return;
237
+ if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
238
+ quit();
239
+ return;
240
+ }
241
+ if (!st.caps)
242
+ return;
243
+ if (st.confirmLogout) {
244
+ const target = st.confirmLogout;
245
+ st.confirmLogout = null;
246
+ if (key.name === 'y') {
247
+ acting = true;
248
+ try {
249
+ const err = logoutSelected(target);
250
+ st.note = err ? fg(203, `✗ ${err}`) : green(`✓ ${harness_registry_1.HARNESSES[target].label} logged out`);
251
+ await reprobe();
252
+ }
253
+ finally {
254
+ acting = false;
255
+ }
256
+ }
257
+ else {
258
+ st.note = dim('logout cancelled');
259
+ redraw();
260
+ }
261
+ return;
262
+ }
263
+ if (key.name === 'up' || key.name === 'k') {
264
+ st.sel = (st.sel + harness_registry_1.HARNESS_IDS.length - 1) % harness_registry_1.HARNESS_IDS.length;
265
+ redraw();
266
+ return;
267
+ }
268
+ if (key.name === 'down' || key.name === 'j') {
269
+ st.sel = (st.sel + 1) % harness_registry_1.HARNESS_IDS.length;
270
+ redraw();
271
+ return;
272
+ }
273
+ if (key.name === 'r') {
274
+ await reprobe();
275
+ return;
276
+ }
277
+ if (key.name !== 'return')
278
+ return;
279
+ const id = harness_registry_1.HARNESS_IDS[st.sel];
280
+ const p = st.caps[id];
281
+ acting = true;
282
+ try {
283
+ if (!p?.available) {
284
+ await installSelected(st, id, redraw);
285
+ await reprobe();
286
+ }
287
+ else if (!p.authed) {
288
+ const err = loginSelected(id);
289
+ st.note = err ? fg(203, `✗ ${err}`) : green('✓ login flow finished — verifying…');
290
+ await reprobe();
291
+ const now = st.caps?.[id];
292
+ if (now?.authed)
293
+ st.note = green(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
294
+ else if (!err)
295
+ st.note = yellow(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
296
+ redraw();
297
+ }
298
+ else if (harness_registry_1.HARNESSES[id].logout) {
299
+ st.confirmLogout = id;
300
+ st.note = yellow(`log out of ${harness_registry_1.HARNESSES[id].label}? press y to confirm`);
301
+ redraw();
302
+ }
303
+ else {
304
+ st.note = green(`${harness_registry_1.HARNESSES[id].label} is installed and logged in.`);
305
+ redraw();
306
+ }
307
+ }
308
+ finally {
309
+ acting = false;
310
+ }
311
+ })();
312
+ });
313
+ await reprobe();
314
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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": [