@addai/node 0.5.0 → 0.6.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/autostart-mac.d.ts +62 -0
- package/dist/autostart-mac.js +306 -0
- package/dist/autostart-win.d.ts +46 -0
- package/dist/autostart-win.js +290 -0
- package/dist/autostart.d.ts +81 -0
- package/dist/autostart.js +224 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +87 -1
- package/dist/command-runner.js +37 -0
- package/dist/index.js +23 -10
- package/dist/memory-capture.js +10 -1
- package/dist/tui/dashboard.d.ts +25 -0
- package/dist/tui/dashboard.js +130 -27
- package/dist/tui/data.d.ts +9 -0
- package/dist/tui/harnesses.js +11 -8
- package/dist/tui/logs.js +3 -3
- package/dist/tui/request-row.d.ts +7 -1
- package/dist/tui/request-row.js +41 -16
- package/dist/tui/requests.js +4 -4
- package/dist/tui/run.js +3 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +314 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// ainode run this node: daemon + live dashboard
|
|
6
6
|
// ainode harnesses interactive harness manager (install / login)
|
|
7
|
+
// ainode startup … start this node when the machine starts
|
|
7
8
|
// ainode unpair --yes disconnect from +Ai, cancel in-flight work
|
|
8
9
|
// ainode version print version
|
|
9
10
|
//
|
|
@@ -15,12 +16,46 @@
|
|
|
15
16
|
//
|
|
16
17
|
// Unpair reaches Supabase via the daemon token in ~/.ainode/state.json and
|
|
17
18
|
// does not require the daemon to be running.
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
18
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
53
|
const lockfile_1 = require("./lockfile");
|
|
20
54
|
const index_1 = require("./index");
|
|
21
55
|
const store_1 = require("./store");
|
|
22
56
|
const supabase_client_1 = require("./supabase-client");
|
|
23
57
|
const run_1 = require("./tui/run");
|
|
58
|
+
const autostart = __importStar(require("./autostart"));
|
|
24
59
|
const args = process.argv.slice(2);
|
|
25
60
|
function token() {
|
|
26
61
|
const p = (0, store_1.readPairing)();
|
|
@@ -53,9 +88,53 @@ async function cmdUnpair() {
|
|
|
53
88
|
(0, store_1.clearPairing)();
|
|
54
89
|
console.log('Unpaired. Run `ainode` to pair again.');
|
|
55
90
|
}
|
|
91
|
+
function cmdStartup() {
|
|
92
|
+
const sub = args[1] ?? 'status';
|
|
93
|
+
if (sub === 'status') {
|
|
94
|
+
console.log(autostart.describeStatus(autostart.status()));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (sub === 'enable') {
|
|
98
|
+
const r = autostart.enable({ force: args.includes('--force') });
|
|
99
|
+
if (!r.ok) {
|
|
100
|
+
console.error(r.error ?? 'could not enable startup');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
for (const w of r.warnings)
|
|
104
|
+
console.log(` note: ${w}`);
|
|
105
|
+
console.log('This node now starts when you log in.');
|
|
106
|
+
if (r.path)
|
|
107
|
+
console.log(` definition: ${r.path}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (sub === 'disable') {
|
|
111
|
+
const before = autostart.status();
|
|
112
|
+
const r = autostart.disable();
|
|
113
|
+
if (!r.ok) {
|
|
114
|
+
console.error(r.error ?? 'could not disable startup');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
console.log('This node no longer starts at login.');
|
|
118
|
+
// launchd's bootout stops the job it was supervising, so the node just
|
|
119
|
+
// went offline. Saying so beats letting the user discover it later.
|
|
120
|
+
if (before.supervisor === 'launchd' && before.enabled) {
|
|
121
|
+
console.log(' the supervised node was stopped — run `ainode` to bring it back');
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
console.error(`unknown startup command: ${sub}`);
|
|
126
|
+
console.error('try: ainode startup enable | disable | status');
|
|
127
|
+
process.exit(2);
|
|
128
|
+
}
|
|
56
129
|
async function main() {
|
|
57
130
|
const cmd = args[0];
|
|
58
131
|
if (!cmd || cmd === 'run' || cmd === 'start' || cmd === '--run') {
|
|
132
|
+
// `--ensure` means "make sure a node is running here, then get out of the
|
|
133
|
+
// way". Windows' startup task repeats every five minutes to catch a
|
|
134
|
+
// crashed daemon; without this it would stack a second one every time.
|
|
135
|
+
if (args.includes('--ensure') && (0, lockfile_1.lockfileAlive)((0, lockfile_1.readLockfile)())) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
59
138
|
const tui = (0, run_1.shouldRenderTui)({
|
|
60
139
|
isTTY: Boolean(process.stdout.isTTY),
|
|
61
140
|
stdinTTY: Boolean(process.stdin.isTTY),
|
|
@@ -99,6 +178,9 @@ async function main() {
|
|
|
99
178
|
case 'harnesses':
|
|
100
179
|
await (0, run_1.runHarnessesTui)();
|
|
101
180
|
return;
|
|
181
|
+
case 'startup':
|
|
182
|
+
cmdStartup();
|
|
183
|
+
return;
|
|
102
184
|
case 'unpair':
|
|
103
185
|
await cmdUnpair();
|
|
104
186
|
return;
|
|
@@ -115,11 +197,15 @@ async function main() {
|
|
|
115
197
|
usage:
|
|
116
198
|
ainode run this +Ai Node — daemon + live dashboard
|
|
117
199
|
ainode harnesses install / log in agent harnesses (interactive)
|
|
200
|
+
ainode startup enable start this node whenever you log in
|
|
201
|
+
ainode startup disable stop starting at login
|
|
202
|
+
ainode startup status where the startup entry is and what it runs
|
|
118
203
|
ainode unpair --yes disconnect from +Ai
|
|
119
204
|
ainode version print version
|
|
120
205
|
|
|
121
206
|
Piped or headless output falls back to plain log lines. Set AINODE_NO_TUI=1
|
|
122
|
-
to force that on a terminal.
|
|
207
|
+
to force that on a terminal. \`ainode run --ensure\` starts a node only if one
|
|
208
|
+
isn't already running — what the Windows startup task uses.
|
|
123
209
|
|
|
124
210
|
Pair the node by running it with no args and following the prompt
|
|
125
211
|
to vault.add.ai/entity/connect/<CODE>.
|
package/dist/command-runner.js
CHANGED
|
@@ -56,6 +56,7 @@ const heartbeat_1 = require("./heartbeat");
|
|
|
56
56
|
const win_1 = require("./win");
|
|
57
57
|
const harness_registry_1 = require("./harness-registry");
|
|
58
58
|
const self_update_1 = require("./self-update");
|
|
59
|
+
const autostart_1 = require("./autostart");
|
|
59
60
|
const paths_1 = require("./paths");
|
|
60
61
|
const pty_helper_1 = require("./pty-helper");
|
|
61
62
|
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -505,6 +506,38 @@ async function runUpdateRuntime(cmd) {
|
|
|
505
506
|
await update(cmd.id, 'failed', { mode }, `restart failed: ${err.message}`);
|
|
506
507
|
}
|
|
507
508
|
}
|
|
509
|
+
/* ── set_autostart ───────────────────────────────────────────────────────
|
|
510
|
+
* Arm (or disarm) "start when the machine starts" on a node you are not
|
|
511
|
+
* sitting at. Unlike a roll, this one CAN honestly report its own outcome:
|
|
512
|
+
* nothing restarts, so the process that did the work is still here to say
|
|
513
|
+
* what happened — and it reports the state it actually READ BACK afterwards,
|
|
514
|
+
* not the state it intended.
|
|
515
|
+
*
|
|
516
|
+
* Enabling under launchd hands the node over: a daemon started by hand is
|
|
517
|
+
* asked to drain and stop so launchd can own it. That means this command can
|
|
518
|
+
* legitimately stop and restart the daemon underneath the caller. It does not
|
|
519
|
+
* restart THIS process, so the reporting contract still holds. */
|
|
520
|
+
async function runSetAutostart(cmd) {
|
|
521
|
+
const want = cmd.input?.enabled !== false;
|
|
522
|
+
await update(cmd.id, 'running', { step: want ? 'enabling' : 'disabling' });
|
|
523
|
+
if (want) {
|
|
524
|
+
const r = (0, autostart_1.enable)({ force: cmd.input?.force === true });
|
|
525
|
+
const state = (0, autostart_1.status)();
|
|
526
|
+
if (!r.ok) {
|
|
527
|
+
await update(cmd.id, 'failed', { autostart: state }, r.error ?? 'could not enable startup');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
await update(cmd.id, 'completed', { autostart: state, path: r.path, warnings: r.warnings });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const r = (0, autostart_1.disable)();
|
|
534
|
+
const state = (0, autostart_1.status)();
|
|
535
|
+
if (!r.ok) {
|
|
536
|
+
await update(cmd.id, 'failed', { autostart: state }, r.error ?? 'could not disable startup');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
await update(cmd.id, 'completed', { autostart: state });
|
|
540
|
+
}
|
|
508
541
|
/* ── dispatcher ──────────────────────────────────────────────────────── */
|
|
509
542
|
async function execute(cmd) {
|
|
510
543
|
// Not a harness command — dispatch before the harness lookup, which would
|
|
@@ -513,6 +546,10 @@ async function execute(cmd) {
|
|
|
513
546
|
await runUpdateRuntime(cmd);
|
|
514
547
|
return;
|
|
515
548
|
}
|
|
549
|
+
if (cmd.kind === 'set_autostart') {
|
|
550
|
+
await runSetAutostart(cmd);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
516
553
|
const spec = (0, harness_registry_1.harness)(cmd.harness ?? '');
|
|
517
554
|
if (!spec) {
|
|
518
555
|
await update(cmd.id, 'failed', {}, `unknown harness: ${cmd.harness}`);
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ const pairing_1 = require("./pairing");
|
|
|
49
49
|
const heartbeat_1 = require("./heartbeat");
|
|
50
50
|
const command_runner_1 = require("./command-runner");
|
|
51
51
|
const self_update_1 = require("./self-update");
|
|
52
|
+
const autostart_1 = require("./autostart");
|
|
52
53
|
const sleep_detector_1 = require("./sleep-detector");
|
|
53
54
|
const claude_config_1 = require("./claude-config");
|
|
54
55
|
const request_pump_1 = require("./request-pump");
|
|
@@ -236,13 +237,21 @@ async function start() {
|
|
|
236
237
|
(0, lockfile_1.releaseLockfile)();
|
|
237
238
|
};
|
|
238
239
|
// Remote roll: drain like a clean shutdown, install the target version if
|
|
239
|
-
// this launch mode can, then hand over to a
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
240
|
+
// this launch mode can, then hand over to a replacement. The handoff file is
|
|
241
|
+
// written only AFTER the replacement is arranged, so a failure anywhere
|
|
242
|
+
// before that leaves this daemon running and the command is failed by the
|
|
243
|
+
// caller instead of silently taking the node down.
|
|
244
|
+
//
|
|
245
|
+
// WHO starts the replacement depends on whether anything is supervising us.
|
|
246
|
+
// Historically nothing was, so this spawned a detached child of its own. On
|
|
247
|
+
// a node with `ainode startup enable` under launchd that would be a second
|
|
248
|
+
// daemon for one node — the failure the lockfile-identity fix was about. So
|
|
249
|
+
// when launchd is holding us up, the roll simply STANDS DOWN and lets
|
|
250
|
+
// KeepAlive do the starting.
|
|
243
251
|
(0, command_runner_1.setRestartHook)(async ({ commandId, version }) => {
|
|
244
252
|
const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
|
|
245
253
|
const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
|
|
254
|
+
const supervised = (0, autostart_1.isSupervised)();
|
|
246
255
|
if (plan.installFirst) {
|
|
247
256
|
const err = await (0, self_update_1.installGlobal)(version);
|
|
248
257
|
if (err)
|
|
@@ -251,11 +260,13 @@ async function start() {
|
|
|
251
260
|
// Drain generously — a restart is elective, so it should never guillotine
|
|
252
261
|
// a live entity turn the way a 15s SIGTERM drain would.
|
|
253
262
|
await stop(RESTART_DRAIN_MS);
|
|
254
|
-
// Point of no return. Past this line a replacement daemon exists
|
|
255
|
-
// MUST exit — two live daemons on
|
|
256
|
-
//
|
|
257
|
-
// never abort the exit.
|
|
258
|
-
|
|
263
|
+
// Point of no return. Past this line a replacement daemon either exists or
|
|
264
|
+
// is guaranteed by the supervisor, so we MUST exit — two live daemons on
|
|
265
|
+
// one node is worse than a command left unreported. Hence the handoff
|
|
266
|
+
// write (which only affects reporting) can never abort the exit.
|
|
267
|
+
if (!supervised) {
|
|
268
|
+
(0, self_update_1.spawnReplacement)(plan, path.join(paths_1.RUNTIME_HOME, 'daemon.log'), process.cwd());
|
|
269
|
+
}
|
|
259
270
|
try {
|
|
260
271
|
(0, self_update_1.writeHandoff)(paths_1.RUNTIME_HOME, {
|
|
261
272
|
command_id: commandId,
|
|
@@ -270,7 +281,9 @@ async function start() {
|
|
|
270
281
|
catch (err) {
|
|
271
282
|
console.error('[restart] handoff write failed — the roll still happened, it just cannot self-report:', err.message);
|
|
272
283
|
}
|
|
273
|
-
console.log(
|
|
284
|
+
console.log(supervised
|
|
285
|
+
? `[restart] standing down for launchd to restart us (${mode} → ${version}); exiting`
|
|
286
|
+
: `[restart] handed over to ${plan.file} (${mode} → ${version}); exiting`);
|
|
274
287
|
setTimeout(() => process.exit(0), 250).unref?.();
|
|
275
288
|
});
|
|
276
289
|
process.once('SIGINT', () => { stop().finally(() => process.exit(0)); });
|
package/dist/memory-capture.js
CHANGED
|
@@ -19,7 +19,16 @@ function buildCapturePayload(req, userText, assistantText, transcriptRef) {
|
|
|
19
19
|
transcript_ref: transcriptRef,
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
|
|
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',
|
package/dist/tui/dashboard.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AppHost, type Screen } from './app';
|
|
2
2
|
import type { DataLayer, NodeSelf, NodeStats, RequestRow } from './data';
|
|
3
|
+
import { type AutostartState } from '../autostart';
|
|
3
4
|
export interface DashboardState {
|
|
4
5
|
self: NodeSelf | null;
|
|
5
6
|
stats: NodeStats | null;
|
|
@@ -18,6 +19,11 @@ export interface DashboardState {
|
|
|
18
19
|
version: string;
|
|
19
20
|
/** How many daemon log lines have been captured this session. */
|
|
20
21
|
logCount: number;
|
|
22
|
+
/** Does this node come back after a reboot? Null until first read. */
|
|
23
|
+
autostart: AutostartState | null;
|
|
24
|
+
/** Set while a toggle is in flight, and to whatever it had to say after —
|
|
25
|
+
* enabling can stop and restart the daemon, which deserves a sentence. */
|
|
26
|
+
autostartNote: string | null;
|
|
21
27
|
}
|
|
22
28
|
/** The landing screen's menu. */
|
|
23
29
|
export declare const MENU: Array<{
|
|
@@ -29,6 +35,25 @@ export declare const MENU: Array<{
|
|
|
29
35
|
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
30
36
|
export declare const MAX_NOW_ROWS = 6;
|
|
31
37
|
export declare function liveRequests(rows: RequestRow[]): RequestRow[];
|
|
38
|
+
/**
|
|
39
|
+
* The node's own state, as a label.
|
|
40
|
+
*
|
|
41
|
+
* A daemon is running — we either started it or the lockfile proved it — so
|
|
42
|
+
* the node IS online, whatever the server has got round to saying. Waiting
|
|
43
|
+
* for `runtime_self` to come back before admitting that meant the first
|
|
44
|
+
* second of every session claimed the node was in an unknown state.
|
|
45
|
+
*/
|
|
46
|
+
export declare function stateLabel(st: DashboardState): string;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the box comes back on its own.
|
|
49
|
+
*
|
|
50
|
+
* On the header, not buried in a menu: "will this node survive a reboot" is a
|
|
51
|
+
* property of the node in the same way "is it online" is, and the answer was
|
|
52
|
+
* previously knowable only by reading the docs and looking for a plist.
|
|
53
|
+
*/
|
|
54
|
+
export declare function autostartLine(st: DashboardState): string;
|
|
55
|
+
/** 1204 reads as a year; 1,204 reads as a count. */
|
|
56
|
+
export declare function fmtCount(n: number | null | undefined): string;
|
|
32
57
|
export declare function nowLines(st: DashboardState, width: number, rows: number): string[];
|
|
33
58
|
export declare function renderDashboard(st: DashboardState, width: number, height: number): string[];
|
|
34
59
|
export declare function createDashboardScreen(deps: {
|
package/dist/tui/dashboard.js
CHANGED
|
@@ -8,59 +8,118 @@
|
|
|
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.autostartLine = autostartLine;
|
|
13
|
+
exports.fmtCount = fmtCount;
|
|
11
14
|
exports.nowLines = nowLines;
|
|
12
15
|
exports.renderDashboard = renderDashboard;
|
|
13
16
|
exports.createDashboardScreen = createDashboardScreen;
|
|
14
17
|
const render_1 = require("./render");
|
|
15
18
|
const request_row_1 = require("./request-row");
|
|
16
19
|
const app_1 = require("./app");
|
|
20
|
+
const autostart_1 = require("../autostart");
|
|
17
21
|
/** The landing screen's menu. */
|
|
18
22
|
exports.MENU = [
|
|
19
|
-
{ key: 'activity', label: 'Activity', hint: '
|
|
20
|
-
{ key: 'harnesses', label: 'Harnesses', hint: '
|
|
21
|
-
{ key: 'logs', label: 'Logs', hint: '
|
|
23
|
+
{ key: 'activity', label: 'Activity', hint: 'Every request this node has handled', shortcut: 'a' },
|
|
24
|
+
{ key: 'harnesses', label: 'Harnesses', hint: 'Install / log in agent CLIs', shortcut: 'h' },
|
|
25
|
+
{ key: 'logs', label: 'Logs', hint: 'What the daemon is saying', shortcut: 'l' },
|
|
22
26
|
];
|
|
23
27
|
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
24
28
|
exports.MAX_NOW_ROWS = 6;
|
|
25
29
|
function liveRequests(rows) {
|
|
26
30
|
return rows.filter(r => request_row_1.ACTIVE.has(r.status));
|
|
27
31
|
}
|
|
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
|
+
function stateLabel(st) {
|
|
41
|
+
const reported = st.self?.effective_status;
|
|
42
|
+
if (!reported)
|
|
43
|
+
return (0, render_1.green)('Online');
|
|
44
|
+
if (reported === 'online')
|
|
45
|
+
return (0, render_1.green)('Online');
|
|
46
|
+
return (0, render_1.yellow)(reported.charAt(0).toUpperCase() + reported.slice(1));
|
|
47
|
+
}
|
|
28
48
|
function headerLines(st) {
|
|
29
49
|
if (!st.paired) {
|
|
30
50
|
return [
|
|
31
|
-
(0, render_1.yellow)('
|
|
51
|
+
(0, render_1.yellow)('Not paired'),
|
|
32
52
|
(0, render_1.dim)('run `ainode` and follow the prompt to vault.add.ai/entity/connect/<CODE>'),
|
|
33
53
|
];
|
|
34
54
|
}
|
|
35
55
|
const self = st.self;
|
|
36
|
-
const name = self?.name ?? self?.hostname ?? '
|
|
37
|
-
const state = self?.effective_status === 'online'
|
|
38
|
-
? (0, render_1.green)('online')
|
|
39
|
-
: (0, render_1.yellow)(self?.effective_status ?? 'unknown');
|
|
56
|
+
const name = self?.name ?? self?.hostname ?? 'This node';
|
|
40
57
|
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)('·')} ${
|
|
58
|
+
const first = `${(0, render_1.bold)(name)} ${(0, render_1.dim)('·')} ${stateLabel(st)}${paired ? ` ${(0, render_1.dim)('·')} ${(0, render_1.dim)(paired)}` : ''}`;
|
|
42
59
|
const up = st.startedAt ? (0, render_1.fmtDuration)(st.now - st.startedAt) : '—';
|
|
43
60
|
const daemon = st.viewerMode
|
|
44
|
-
? `${(0, render_1.cyan)('⏺')}
|
|
45
|
-
: `${(0, render_1.green)('⏺')}
|
|
46
|
-
const chip = st.offline ? ` ${(0, render_1.yellow)('⚠
|
|
47
|
-
return [first, daemon + chip];
|
|
61
|
+
? `${(0, render_1.cyan)('⏺')} Viewer ${(0, render_1.dim)(`pid ${st.pid ?? '?'}`)} ${(0, render_1.dim)('· another process owns this node')}`
|
|
62
|
+
: `${(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`)}`;
|
|
63
|
+
const chip = st.offline ? ` ${(0, render_1.yellow)('⚠ Offline · retrying')}` : '';
|
|
64
|
+
return [first, daemon + chip, autostartLine(st)];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether the box comes back on its own.
|
|
68
|
+
*
|
|
69
|
+
* On the header, not buried in a menu: "will this node survive a reboot" is a
|
|
70
|
+
* property of the node in the same way "is it online" is, and the answer was
|
|
71
|
+
* previously knowable only by reading the docs and looking for a plist.
|
|
72
|
+
*/
|
|
73
|
+
function autostartLine(st) {
|
|
74
|
+
if (st.autostartNote)
|
|
75
|
+
return (0, render_1.dim)(` ${st.autostartNote}`);
|
|
76
|
+
const a = st.autostart;
|
|
77
|
+
if (!a)
|
|
78
|
+
return (0, render_1.dim)(' Starts at login …');
|
|
79
|
+
if (!a.supported)
|
|
80
|
+
return (0, render_1.dim)(` Starts at login ${(0, render_1.grey)('not supported on this platform')}`);
|
|
81
|
+
const issue = a.issues?.length ? ` ${(0, render_1.yellow)(`⚠ ${a.issues[0]}`)}` : '';
|
|
82
|
+
return a.enabled
|
|
83
|
+
? ` ${(0, render_1.dim)('Starts at login')} ${(0, render_1.green)('yes')}${(0, render_1.dim)(a.supervisor ? ` · ${a.supervisor}` : '')}${issue}`
|
|
84
|
+
: ` ${(0, render_1.dim)('Starts at login')} ${(0, render_1.yellow)('no')} ${(0, render_1.dim)('press s to turn it on')}`;
|
|
85
|
+
}
|
|
86
|
+
/** 1204 reads as a year; 1,204 reads as a count. */
|
|
87
|
+
function fmtCount(n) {
|
|
88
|
+
if (n == null)
|
|
89
|
+
return '—';
|
|
90
|
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
91
|
+
}
|
|
92
|
+
/** A labelled figure, padded so the labels line up down the screen. */
|
|
93
|
+
function stat(label, value, width = 14) {
|
|
94
|
+
return (0, render_1.padEndVisible)(`${(0, render_1.dim)(label)} ${value}`, width);
|
|
48
95
|
}
|
|
49
96
|
function statsLines(st) {
|
|
50
97
|
const s = st.stats;
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
// Keep the shape while the first fetch is in flight, so the band doesn't
|
|
99
|
+
// pop into place a second after the header.
|
|
100
|
+
if (!s) {
|
|
101
|
+
return [
|
|
102
|
+
`${stat('Today', (0, render_1.dim)('—'))}${stat('24h', (0, render_1.dim)('—'), 26)}${stat('Avg', (0, render_1.dim)('—'))}`,
|
|
103
|
+
`${stat('Week', (0, render_1.dim)('—'))}${stat('Month', (0, render_1.dim)('—'), 26)}${stat('Total', (0, render_1.dim)('—'))}`,
|
|
104
|
+
];
|
|
105
|
+
}
|
|
53
106
|
const agents = Object.entries(s.by_agent)
|
|
54
107
|
.sort((a, b) => b[1] - a[1])
|
|
55
|
-
.map(([k, v]) => `${k} ${v}`)
|
|
56
|
-
.join(' · ');
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
108
|
+
.map(([k, v]) => `${k.charAt(0).toUpperCase()}${k.slice(1)} ${fmtCount(v)}`)
|
|
109
|
+
.join((0, render_1.dim)(' · '));
|
|
110
|
+
// The ✓/✗ figures are a rolling 24h window while "Today" is the calendar
|
|
111
|
+
// day, so the window is labelled — otherwise ✓3 next to Today 2 looks
|
|
112
|
+
// like an arithmetic bug.
|
|
113
|
+
const outcomes = [
|
|
114
|
+
(0, render_1.green)(`✓ ${fmtCount(s.succeeded_24h)}`),
|
|
115
|
+
s.failed_24h ? (0, render_1.red)(`✗ ${fmtCount(s.failed_24h)}`) : (0, render_1.dim)('✗ 0'),
|
|
116
|
+
s.canceled_24h ? (0, render_1.grey)(`⊘ ${fmtCount(s.canceled_24h)}`) : '',
|
|
117
|
+
].filter(Boolean).join(' ');
|
|
118
|
+
// Both rows share one three-column grid, so Month sits under 24h and
|
|
119
|
+
// Total under Avg rather than the two rows drifting apart.
|
|
61
120
|
return [
|
|
62
|
-
`${(0, render_1.bold)(
|
|
63
|
-
(
|
|
121
|
+
`${stat('Today', (0, render_1.bold)(fmtCount(s.today)))}${stat('24h', outcomes, 26)}${stat('Avg', (0, render_1.fmtDuration)(s.avg_duration_ms))}`,
|
|
122
|
+
`${stat('Week', fmtCount(s.week))}${stat('Month', fmtCount(s.month), 26)}${stat('Total', fmtCount(s.total))}${(0, render_1.dim)(agents)}`,
|
|
64
123
|
];
|
|
65
124
|
}
|
|
66
125
|
/** The most recent request that actually finished, for the idle line. */
|
|
@@ -69,12 +128,12 @@ function lastFinished(rows) {
|
|
|
69
128
|
}
|
|
70
129
|
function nowLines(st, width, rows) {
|
|
71
130
|
const live = liveRequests(st.recent);
|
|
72
|
-
const out = [` ${(0, render_1.bold)('
|
|
131
|
+
const out = [` ${(0, render_1.bold)('Now')}`];
|
|
73
132
|
if (live.length === 0) {
|
|
74
133
|
const last = lastFinished(st.recent);
|
|
75
134
|
out.push(last
|
|
76
|
-
? (0, render_1.dim)(`
|
|
77
|
-
: (0, render_1.dim)('
|
|
135
|
+
? (0, render_1.dim)(` Idle — last run finished ${(0, render_1.fmtRelative)(last.finished_at, st.now)}`)
|
|
136
|
+
: (0, render_1.dim)(' Idle'));
|
|
78
137
|
return out;
|
|
79
138
|
}
|
|
80
139
|
out.push((0, request_row_1.requestHeader)(width));
|
|
@@ -86,7 +145,7 @@ function nowLines(st, width, rows) {
|
|
|
86
145
|
return out;
|
|
87
146
|
}
|
|
88
147
|
function renderDashboard(st, width, height) {
|
|
89
|
-
const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(
|
|
148
|
+
const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`@addai/node v${st.version}`)}`, headerLines(st), width);
|
|
90
149
|
if (!st.paired) {
|
|
91
150
|
return [...head, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
|
|
92
151
|
}
|
|
@@ -104,6 +163,7 @@ function renderDashboard(st, width, height) {
|
|
|
104
163
|
const foot = (0, app_1.footerHint)([
|
|
105
164
|
{ keys: '↑↓', label: 'move' },
|
|
106
165
|
{ keys: '⏎', label: 'open' },
|
|
166
|
+
{ keys: 's', label: st.autostart?.enabled ? 'startup off' : 'startup on' },
|
|
107
167
|
{ keys: 'r', label: 'refresh' },
|
|
108
168
|
{ keys: '?', label: 'keys' },
|
|
109
169
|
{ keys: 'q', label: 'quit' },
|
|
@@ -142,7 +202,45 @@ function createDashboardScreen(deps) {
|
|
|
142
202
|
if (recent.length)
|
|
143
203
|
st.recent = recent;
|
|
144
204
|
st.offline = deps.data.offline();
|
|
205
|
+
// Reads a file (and, on Windows, a cached schtasks query) — cheap enough
|
|
206
|
+
// to ride the same poll, so the header can't disagree with reality after
|
|
207
|
+
// someone changes it from Studio or the CLI.
|
|
208
|
+
st.autostart = (0, autostart_1.status)();
|
|
209
|
+
deps.host.redraw();
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Turn "start at login" on or off from here.
|
|
213
|
+
*
|
|
214
|
+
* Enabling can stop a daemon — a node started by hand has to stand down so
|
|
215
|
+
* launchd can own it — so the outcome is reported in the header rather than
|
|
216
|
+
* left for the user to infer. The work is deferred a tick so the "Working…"
|
|
217
|
+
* line is actually on screen before the blocking call begins.
|
|
218
|
+
*/
|
|
219
|
+
const toggleAutostart = async () => {
|
|
220
|
+
const on = st.autostart?.enabled === true;
|
|
221
|
+
if (st.autostart && !st.autostart.supported) {
|
|
222
|
+
st.autostartNote = `Starts at login isn't supported on ${process.platform}`;
|
|
223
|
+
deps.host.redraw();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
st.autostartNote = on ? 'Turning off start at login…' : 'Turning on start at login…';
|
|
145
227
|
deps.host.redraw();
|
|
228
|
+
await new Promise(r => setTimeout(r, 0));
|
|
229
|
+
if (on) {
|
|
230
|
+
const r = (0, autostart_1.disable)();
|
|
231
|
+
st.autostartNote = r.ok ? 'This node no longer starts at login' : `Could not turn it off: ${r.error}`;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const r = (0, autostart_1.enable)();
|
|
235
|
+
st.autostartNote = r.ok
|
|
236
|
+
? ['This node now starts when you log in', ...r.warnings].join(' — ')
|
|
237
|
+
: `Could not turn it on: ${r.error}`;
|
|
238
|
+
}
|
|
239
|
+
st.autostart = (0, autostart_1.status)();
|
|
240
|
+
deps.host.redraw();
|
|
241
|
+
// Let the sentence sit long enough to read, then fall back to the state
|
|
242
|
+
// line — a note that never clears becomes furniture.
|
|
243
|
+
setTimeout(() => { st.autostartNote = null; deps.host.redraw(); }, 8000).unref?.();
|
|
146
244
|
};
|
|
147
245
|
return {
|
|
148
246
|
id: 'dashboard',
|
|
@@ -165,6 +263,7 @@ function createDashboardScreen(deps) {
|
|
|
165
263
|
{ keys: 'a', label: 'activity — full request history' },
|
|
166
264
|
{ keys: 'h', label: 'harnesses — install / log in agent CLIs' },
|
|
167
265
|
{ keys: 'l', label: 'logs — daemon output' },
|
|
266
|
+
{ keys: 's', label: 'start this node at login (on / off)' },
|
|
168
267
|
{ keys: 'r', label: 'refresh now' },
|
|
169
268
|
],
|
|
170
269
|
async onKey(key) {
|
|
@@ -190,6 +289,10 @@ function createDashboardScreen(deps) {
|
|
|
190
289
|
deps.openLogs();
|
|
191
290
|
return;
|
|
192
291
|
}
|
|
292
|
+
if (key.name === 's') {
|
|
293
|
+
await toggleAutostart();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
193
296
|
if (key.name === 'r') {
|
|
194
297
|
await refresh();
|
|
195
298
|
return;
|
package/dist/tui/data.d.ts
CHANGED
|
@@ -2,6 +2,15 @@ export interface RequestRow {
|
|
|
2
2
|
id: string;
|
|
3
3
|
entity_name: string | null;
|
|
4
4
|
agent: string;
|
|
5
|
+
/** Where the row ENDED up. `agent` is what this node actually spawned — the
|
|
6
|
+
* ladder mutates the request's agent column in place, so the two differ on
|
|
7
|
+
* a run that hopped. */
|
|
8
|
+
current_agent?: string | null;
|
|
9
|
+
ladder_hops?: number | null;
|
|
10
|
+
/** This node ran it but no longer owns it — it was handed to another
|
|
11
|
+
* machine, and that machine finished it. */
|
|
12
|
+
handed_off?: boolean | null;
|
|
13
|
+
to_runtime_name?: string | null;
|
|
5
14
|
mode: string;
|
|
6
15
|
model: string | null;
|
|
7
16
|
issued_via: string | null;
|
package/dist/tui/harnesses.js
CHANGED
|
@@ -89,10 +89,10 @@ function formatVersion(v) {
|
|
|
89
89
|
}
|
|
90
90
|
function accountLabel(p) {
|
|
91
91
|
if (!p?.available)
|
|
92
|
-
return '
|
|
92
|
+
return 'Not installed';
|
|
93
93
|
if (!p.authed)
|
|
94
|
-
return '
|
|
95
|
-
const who = p.account ?? p.accountKind ?? '
|
|
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
|
-
|
|
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)('
|
|
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)('
|
|
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)(['', '
|
|
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 ? '
|
|
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)('
|
|
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)('
|
|
47
|
+
out.push((0, render_1.dim)(' Nothing matches that filter'));
|
|
48
48
|
else
|
|
49
|
-
out.push((0, render_1.dim)(` ${emptyHint ?? '
|
|
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);
|