@addai/node 0.8.2 → 0.10.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.
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ // Open a URL in whatever the user calls their browser.
3
+ //
4
+ // No dependency for this. The three commands are stable, and a package that
5
+ // shells out for you is a package that can break pairing on a platform we
6
+ // cannot test from here.
7
+ //
8
+ // Windows deliberately does NOT go through cmd.exe. `start` is a shell
9
+ // builtin, so reaching it means spawning a shell around a URL, and this
10
+ // codebase has already been burned by putting cmd.exe near strings it did not
11
+ // control. rundll32 takes the URL as a plain argument and hands it to the
12
+ // registered handler.
13
+ //
14
+ // Everything here is best-effort by design. A browser that fails to open is
15
+ // not a failed pairing — the terminal still shows the code and the link, and
16
+ // the user carries on by hand. So this never throws and never blocks: it
17
+ // reports whether the launch was ACCEPTED, which is all the caller can
18
+ // honestly say.
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.openBrowser = openBrowser;
21
+ const child_process_1 = require("child_process");
22
+ function openBrowser(url, platform = process.platform) {
23
+ const [cmd, args] = platform === 'darwin' ? ['open', [url]]
24
+ : platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
25
+ : ['xdg-open', [url]];
26
+ try {
27
+ const child = (0, child_process_1.spawn)(cmd, args, {
28
+ stdio: 'ignore',
29
+ // The browser outlives us — and on a machine where the daemon is about
30
+ // to take over the terminal, a child holding the TTY would fight it.
31
+ detached: true,
32
+ });
33
+ // A missing xdg-open surfaces here rather than as an unhandled error that
34
+ // takes the daemon down with it.
35
+ child.on('error', () => { });
36
+ child.unref();
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
@@ -0,0 +1,20 @@
1
+ export interface PairDestination {
2
+ /** What to call it on screen. */
3
+ product: string;
4
+ /** Where the user goes to confirm. */
5
+ url: string;
6
+ /** That URL carrying the code, which is what actually gets opened. */
7
+ link(code: string): string;
8
+ }
9
+ /**
10
+ * @param argv process.argv.slice(2)
11
+ * @param env process.env
12
+ */
13
+ export declare function pairDestination(argv: string[], env?: NodeJS.ProcessEnv): PairDestination;
14
+ export interface BrowserEnv {
15
+ isTTY: boolean;
16
+ platform: NodeJS.Platform | string;
17
+ env: NodeJS.ProcessEnv;
18
+ }
19
+ /** Would opening a browser here actually show the user anything? */
20
+ export declare function canOpenBrowser({ isTTY, platform, env }: BrowserEnv): boolean;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ // Which product this machine is joining, and whether we can open a browser
3
+ // to finish the job.
4
+ //
5
+ // The command the user ran is the answer to the first question. They copied it
6
+ // off a page that already knew — so asking "which product?" in the terminal
7
+ // would be asking them to repeat themselves. `ainode entities` goes to Entity
8
+ // Studio; everything else keeps going to Vault, exactly as it always has.
9
+ //
10
+ // The second question is the one that decides whether pairing feels like
11
+ // magic or like a broken promise. Opening a browser on a machine nobody is
12
+ // sitting at does nothing; opening one over SSH opens it on the wrong
13
+ // computer entirely. Both are common, so both are checked, and when the
14
+ // answer is no the code gets printed like it always did.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.pairDestination = pairDestination;
17
+ exports.canOpenBrowser = canOpenBrowser;
18
+ const config_1 = require("./config");
19
+ const destination = (product, url) => ({
20
+ product,
21
+ url,
22
+ link: (code) => `${url}/${encodeURIComponent(code)}`,
23
+ });
24
+ /**
25
+ * @param argv process.argv.slice(2)
26
+ * @param env process.env
27
+ */
28
+ function pairDestination(argv, env = process.env) {
29
+ const entities = argv[0] === 'entities';
30
+ // A dev override has to move BOTH products or a local Studio ends up
31
+ // pairing against production Vault, which is a confusing way to lose an
32
+ // afternoon.
33
+ const base = env.AINODE_PAIR_BASE?.replace(/\/+$/, '');
34
+ if (base) {
35
+ return entities
36
+ ? destination('+Ai Entities', `${base}/connect`)
37
+ : destination('+Ai Vault', `${base}/add/entity`);
38
+ }
39
+ return entities
40
+ ? destination('+Ai Entities', config_1.ENTITIES_PAIR_URL)
41
+ : destination('+Ai Vault', config_1.VAULT_PAIR_URL);
42
+ }
43
+ /** Would opening a browser here actually show the user anything? */
44
+ function canOpenBrowser({ isTTY, platform, env }) {
45
+ if (!isTTY)
46
+ return false;
47
+ if (env.AINODE_NO_BROWSER === '1')
48
+ return false;
49
+ // Over SSH the browser would open on the machine being administered, where
50
+ // nobody is looking at it — and the terminal would sit there claiming it
51
+ // had done something helpful.
52
+ if (env.SSH_TTY || env.SSH_CONNECTION)
53
+ return false;
54
+ // A Linux box with no display server has no browser to open. macOS and
55
+ // Windows always have one.
56
+ if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY)
57
+ return false;
58
+ return true;
59
+ }
package/dist/pairing.d.ts CHANGED
@@ -14,7 +14,9 @@ export declare function requestPairCode(): Promise<string>;
14
14
  */
15
15
  export declare function awaitPairing(code: string): Promise<FinalizeResult>;
16
16
  /**
17
- * Print the human-facing instructions for a fresh pairing.
17
+ * Run the whole first-run flow: ask, open, wait, save.
18
+ *
19
+ * @param argv process.argv.slice(2) — decides which product to join
18
20
  */
19
- export declare function printPairingInstructions(code: string): void;
21
+ export declare function runPairing(argv: string[]): Promise<FinalizeResult>;
20
22
  export {};
package/dist/pairing.js CHANGED
@@ -1,7 +1,15 @@
1
1
  "use strict";
2
- // First-run pairing flow. Called when state.json doesn't exist. Walks the
3
- // user through obtaining a 5-letter code and entering it at
4
- // vault.add.ai/add/entity. Once finalized, persists the daemon_token.
2
+ // First-run pairing flow. Called when state.json doesn't exist.
3
+ //
4
+ // The machine now does the walking. It asks one question, opens the user's
5
+ // browser at the confirm page carrying its own code, and waits — so the
6
+ // person types nothing and never has to know a code existed. Where that
7
+ // cannot work (no terminal, over SSH, headless Linux) it prints the code and
8
+ // the link exactly as it always did, because those machines are real and are
9
+ // often the ones that most need a node on them.
10
+ //
11
+ // Which product the browser lands on comes from the command the user ran:
12
+ // `ainode entities` goes to Entity Studio, everything else to Vault.
5
13
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
14
  if (k2 === undefined) k2 = k;
7
15
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -38,10 +46,13 @@ var __importStar = (this && this.__importStar) || (function () {
38
46
  Object.defineProperty(exports, "__esModule", { value: true });
39
47
  exports.requestPairCode = requestPairCode;
40
48
  exports.awaitPairing = awaitPairing;
41
- exports.printPairingInstructions = printPairingInstructions;
49
+ exports.runPairing = runPairing;
42
50
  const os = __importStar(require("os"));
43
51
  const supabase_client_1 = require("./supabase-client");
44
52
  const store_1 = require("./store");
53
+ const pair_destination_1 = require("./pair-destination");
54
+ const open_browser_1 = require("./open-browser");
55
+ const pair_1 = require("./tui/pair");
45
56
  const config_1 = require("./config");
46
57
  function platform() {
47
58
  const p = process.platform;
@@ -113,17 +124,54 @@ async function awaitPairing(code) {
113
124
  throw new Error('pair_timeout');
114
125
  }
115
126
  /**
116
- * Print the human-facing instructions for a fresh pairing.
127
+ * Run the whole first-run flow: ask, open, wait, save.
128
+ *
129
+ * @param argv process.argv.slice(2) — decides which product to join
117
130
  */
118
- function printPairingInstructions(code) {
119
- console.log('');
120
- console.log(' ── Setup needed ──');
121
- console.log('');
122
- console.log(` 1. Open ${config_1.VAULT_PAIR_URL}`);
123
- console.log(` 2. Enter this code: ${code}`);
124
- console.log('');
125
- console.log(` (or click: ${config_1.VAULT_PAIR_URL}/${code})`);
126
- console.log('');
127
- console.log(' Waiting for pairing… (expires in 15 minutes)');
128
- console.log('');
131
+ async function runPairing(argv) {
132
+ const dest = (0, pair_destination_1.pairDestination)(argv);
133
+ const hostname = os.hostname();
134
+ const interactive = (0, pair_destination_1.canOpenBrowser)({
135
+ isTTY: Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY),
136
+ platform: process.platform,
137
+ env: process.env,
138
+ });
139
+ // Ask BEFORE minting a code. A code starts a 15-minute clock the moment it
140
+ // exists, and someone who walks away at the question should not come back
141
+ // to an expired one.
142
+ const choice = interactive ? await (0, pair_1.promptConnect)({ product: dest.product, hostname }) : 'manual';
143
+ const code = await requestPairCode();
144
+ const link = dest.link(code);
145
+ const opened = choice === 'open' ? (0, open_browser_1.openBrowser)(link) : false;
146
+ let waiting = null;
147
+ if (interactive) {
148
+ waiting = (0, pair_1.renderWaiting)({ product: dest.product, link, code, opened });
149
+ }
150
+ else {
151
+ // Headless: plain lines, no spinner, no cursor games. This output ends up
152
+ // in a log file as often as on a screen.
153
+ console.log('');
154
+ console.log(` Connect this machine to ${dest.product}`);
155
+ console.log('');
156
+ console.log(` 1. Open ${dest.url}`);
157
+ console.log(` 2. Enter this code: ${code}`);
158
+ console.log('');
159
+ console.log(` (or open directly: ${link})`);
160
+ console.log('');
161
+ console.log(' Waiting for pairing… (expires in 15 minutes)');
162
+ console.log('');
163
+ }
164
+ try {
165
+ const result = await awaitPairing(code);
166
+ waiting?.stop();
167
+ if (interactive)
168
+ (0, pair_1.renderConnected)(hostname);
169
+ else
170
+ console.log(` ✓ Connected as ${hostname}`);
171
+ return result;
172
+ }
173
+ catch (err) {
174
+ waiting?.stop();
175
+ throw err;
176
+ }
129
177
  }
@@ -1,4 +1,5 @@
1
1
  export declare function inflightCount(): number;
2
+ export declare function activeRequestIdList(): string[];
2
3
  export declare function start(): void;
3
4
  export declare function stop(): void;
4
5
  /**
@@ -5,6 +5,7 @@
5
5
  // arrives.
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.inflightCount = inflightCount;
8
+ exports.activeRequestIdList = activeRequestIdList;
8
9
  exports.start = start;
9
10
  exports.stop = stop;
10
11
  exports.drain = drain;
@@ -59,6 +60,14 @@ let lastHealthBlockAt = 0;
59
60
  // Exported so /stats can surface "pump stuck at N inflight" without
60
61
  // having to guess.
61
62
  function inflightCount() { return inflight; }
63
+ // The runs this daemon is holding a child process for, right now. This is the
64
+ // only honest answer to "is that run alive?" — the server used to infer it from
65
+ // how recently the agent emitted an event, which measures how chatty the agent
66
+ // is, not whether it is alive. A grok turn routinely goes ten minutes between
67
+ // its last streamed token and turn_complete, and the server was failing those
68
+ // runs mid-flight and apologising to the user for a reply that had already
69
+ // arrived. Reported every 30s by the heartbeat.
70
+ function activeRequestIdList() { return [...activeRequestIds]; }
62
71
  async function tick() {
63
72
  if (stopped)
64
73
  return;
@@ -22,6 +22,8 @@ export interface DashboardState {
22
22
  inflight: number;
23
23
  paired: boolean;
24
24
  viewerMode: boolean;
25
+ /** First press of 'd' asks; the second one does it. */
26
+ disconnectArmed?: boolean;
25
27
  offline: boolean;
26
28
  now: number;
27
29
  version: string;
@@ -19,6 +19,8 @@ exports.createDashboardScreen = createDashboardScreen;
19
19
  const render_1 = require("./render");
20
20
  const request_row_1 = require("./request-row");
21
21
  const app_1 = require("./app");
22
+ const unpair_1 = require("../unpair");
23
+ const store_1 = require("../store");
22
24
  const autostart_1 = require("../autostart");
23
25
  /** The landing screen's menu. */
24
26
  exports.MENU = [
@@ -71,7 +73,7 @@ function headerLines(st) {
71
73
  if (!st.paired) {
72
74
  return [
73
75
  (0, render_1.yellow)('Not paired'),
74
- (0, render_1.dim)('run `ainode` and follow the prompt to vault.add.ai/entity/connect/<CODE>'),
76
+ (0, render_1.dim)('run `ainode` to pair again it opens your browser to finish up'),
75
77
  ];
76
78
  }
77
79
  const self = st.self;
@@ -174,7 +176,13 @@ function nowLines(st, width, rows) {
174
176
  function renderDashboard(st, width, height) {
175
177
  const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`@addai/node v${st.version}`)}`, headerLines(st), width);
176
178
  if (!st.paired) {
177
- return [...head, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
179
+ // The note has to survive the transition into this screen. Disconnecting
180
+ // flips `paired` to false, and without this the sentence saying how it
181
+ // went — including "+Ai could not be told" — was rendered for a frame and
182
+ // then replaced by a screen that had no room for it. The outcome of the
183
+ // one destructive action in here must not be the thing you cannot read.
184
+ const note = st.autostartNote ? ['', ` ${st.autostartNote}`] : [];
185
+ return [...head, ...note, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
178
186
  }
179
187
  const stats = statsLines(st).map(l => ` ${l}`);
180
188
  // The shortcut letters line up in a column of their own, so the eye can
@@ -198,6 +206,10 @@ function renderDashboard(st, width, height) {
198
206
  { keys: '?', label: 'keys' },
199
207
  { keys: 'q', label: 'quit' },
200
208
  ]);
209
+ // 'd' is deliberately absent from this row. The footer is the set of things
210
+ // it is safe to press to find out what they do, and disconnect is not one
211
+ // of them; it lives in the `?` list, where you go when you are looking for
212
+ // something specific.
201
213
  // Everything but the NOW band is fixed height, so the band takes what is
202
214
  // left: heading + header row + rows + the "+N more" line.
203
215
  const fixed = head.length + 1 + stats.length + 1 + 3 + 1 + menu.length + 1 + 1;
@@ -277,6 +289,40 @@ function createDashboardScreen(deps) {
277
289
  // line — a note that never clears becomes furniture.
278
290
  setTimeout(() => { st.autostartNote = null; deps.host.redraw(); }, 8000).unref?.();
279
291
  };
292
+ /* ── disconnect ────────────────────────────────────────────────────────
293
+ The one key in here that takes work away from people, so it is armed
294
+ rather than instant: the first press asks, naming what it will cancel,
295
+ and the second does it. Any other key disarms, which means a stray 'd'
296
+ costs nothing.
297
+
298
+ Afterwards the daemon has no token and cannot do anything useful, so the
299
+ console closes with it — except in viewer mode, where the daemon is a
300
+ different process this console does not own and can only report on. */
301
+ const disconnect = async () => {
302
+ if (!st.disconnectArmed) {
303
+ st.disconnectArmed = true;
304
+ st.autostartNote = `Disconnect this node? ${(0, unpair_1.describeDisconnect)(st.inflight)} Press d again to confirm.`;
305
+ deps.host.redraw();
306
+ return;
307
+ }
308
+ st.disconnectArmed = false;
309
+ st.autostartNote = 'Disconnecting…';
310
+ deps.host.redraw();
311
+ await new Promise(r => setTimeout(r, 0));
312
+ const r = await (0, unpair_1.disconnectNode)();
313
+ st.paired = (0, store_1.isPaired)();
314
+ st.autostartNote = (0, unpair_1.summariseDisconnect)(r);
315
+ deps.host.redraw();
316
+ if (!r.ok)
317
+ return;
318
+ if (st.viewerMode) {
319
+ st.autostartNote = `${(0, unpair_1.summariseDisconnect)(r)} The daemon (pid ${st.pid ?? '?'}) is still running — stop it to finish.`;
320
+ deps.host.redraw();
321
+ return;
322
+ }
323
+ // Let the sentence land before the screen goes away.
324
+ setTimeout(() => deps.host.quit(), 1600).unref?.();
325
+ };
280
326
  /**
281
327
  * One cursor over two stacked lists: the NOW band sits above the menu, so
282
328
  * ↑ off the top of the menu lands on the LAST live row (the one nearest the
@@ -337,6 +383,7 @@ function createDashboardScreen(deps) {
337
383
  { keys: 'l', label: 'logs — daemon output' },
338
384
  { keys: 's', label: 'start this node at login (on / off)' },
339
385
  { keys: 'r', label: 'refresh now' },
386
+ { keys: 'd', label: 'disconnect this node from +Ai' },
340
387
  ],
341
388
  async onKey(key) {
342
389
  if (key.name === 'up' || key.name === 'k') {
@@ -369,6 +416,17 @@ function createDashboardScreen(deps) {
369
416
  await refresh();
370
417
  return;
371
418
  }
419
+ if (key.name === 'd') {
420
+ await disconnect();
421
+ return;
422
+ }
423
+ // Anything else disarms a pending disconnect: an armed destructive
424
+ // action must not survive the user walking away and coming back.
425
+ if (st.disconnectArmed) {
426
+ st.disconnectArmed = false;
427
+ st.autostartNote = null;
428
+ deps.host.redraw();
429
+ }
372
430
  if (key.name === 'return') {
373
431
  // A selected run wins over the menu: the cursor is visibly on it.
374
432
  const live = selectableNow(st)[nowIndex(st)];
@@ -0,0 +1,33 @@
1
+ export type PairChoice = 'open' | 'manual';
2
+ interface PromptOpts {
3
+ /** '+Ai Entities' — what the user thinks they are joining. */
4
+ product: string;
5
+ hostname: string;
6
+ }
7
+ /**
8
+ * Ask whether to open the browser. Resolves the user's answer; never rejects.
9
+ *
10
+ * Ctrl-C here means "I did not want to do this at all", so it exits rather
11
+ * than falling through to a code the user never asked for.
12
+ */
13
+ export declare function promptConnect({ product, hostname }: PromptOpts): Promise<PairChoice>;
14
+ export interface WaitingHandle {
15
+ /** Stop the spinner and leave the block on screen. */
16
+ stop(): void;
17
+ }
18
+ /**
19
+ * The block that stays up while the daemon waits to be claimed.
20
+ *
21
+ * It shows the code and the link even when the browser opened fine. That is
22
+ * not clutter — a browser that opened on the wrong profile, or behind another
23
+ * window, is invisible from here, and the only recovery is the thing this
24
+ * block is holding.
25
+ */
26
+ export declare function renderWaiting(opts: {
27
+ product: string;
28
+ link: string;
29
+ code: string;
30
+ opened: boolean;
31
+ }): WaitingHandle;
32
+ export declare function renderConnected(name: string): void;
33
+ export {};
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ // The first thing anyone sees: one question, then the browser opens.
3
+ //
4
+ // This is not a screen on the console stack, and deliberately so. That stack
5
+ // owns the alt screen and exits the process when it quits — both wrong here.
6
+ // Pairing has to hand back to a daemon that then runs for months, and the code
7
+ // and link have to stay in scrollback afterwards, because the most likely
8
+ // reason someone is still reading this block is that the browser didn't open.
9
+ //
10
+ // So: no alt screen, no takeover. A question that repaints in place, then a
11
+ // block that stays on the terminal and a spinner line that updates until the
12
+ // machine is through.
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.promptConnect = promptConnect;
48
+ exports.renderWaiting = renderWaiting;
49
+ exports.renderConnected = renderConnected;
50
+ const readline = __importStar(require("readline"));
51
+ const render_1 = require("./render");
52
+ const E = '\x1b[';
53
+ const write = (s) => { process.stdout.write(s); };
54
+ /** Move up n lines and clear from there down — how a block repaints in place. */
55
+ const rewind = (n) => { if (n > 0)
56
+ write(`${E}${n}A${E}0J`); };
57
+ const CHOICES = [
58
+ { id: 'open', label: 'Yes, open my browser' },
59
+ { id: 'manual', label: 'No, show me a code instead' },
60
+ ];
61
+ /**
62
+ * Ask whether to open the browser. Resolves the user's answer; never rejects.
63
+ *
64
+ * Ctrl-C here means "I did not want to do this at all", so it exits rather
65
+ * than falling through to a code the user never asked for.
66
+ */
67
+ function promptConnect({ product, hostname }) {
68
+ return new Promise(resolve => {
69
+ let sel = 0;
70
+ let painted = 0;
71
+ const frame = () => [
72
+ '',
73
+ ` ${(0, render_1.bold)((0, render_1.cyan)('✦'))} ${(0, render_1.bold)('+Ai Node')}`,
74
+ '',
75
+ ` Connect ${(0, render_1.bold)(hostname)} to ${(0, render_1.bold)(product)}?`,
76
+ '',
77
+ ...CHOICES.map((c, i) => i === sel ? ` ${(0, render_1.green)('▸')} ${c.label}` : ` ${(0, render_1.grey)(c.label)}`),
78
+ '',
79
+ ` ${(0, render_1.dim)('↑↓ to choose · ⏎ to continue · ^C to cancel')}`,
80
+ '',
81
+ ];
82
+ const paint = () => {
83
+ rewind(painted);
84
+ const lines = frame();
85
+ write(lines.join('\n') + '\n');
86
+ painted = lines.length;
87
+ };
88
+ const done = (choice) => {
89
+ rewind(painted);
90
+ process.stdin.off('keypress', onKey);
91
+ if (process.stdin.isTTY)
92
+ process.stdin.setRawMode(false);
93
+ process.stdin.pause();
94
+ resolve(choice);
95
+ };
96
+ function onKey(_s, key) {
97
+ if (!key)
98
+ return;
99
+ if (key.ctrl && key.name === 'c') {
100
+ rewind(painted);
101
+ write(` ${(0, render_1.dim)('Cancelled. Run the command again when you are ready.')}\n\n`);
102
+ process.exit(130);
103
+ }
104
+ if (key.name === 'up' || key.name === 'k') {
105
+ sel = (sel + CHOICES.length - 1) % CHOICES.length;
106
+ paint();
107
+ }
108
+ else if (key.name === 'down' || key.name === 'j') {
109
+ sel = (sel + 1) % CHOICES.length;
110
+ paint();
111
+ }
112
+ else if (key.name === 'y')
113
+ done('open');
114
+ else if (key.name === 'n')
115
+ done('manual');
116
+ else if (key.name === 'return' || key.name === 'space')
117
+ done(CHOICES[sel].id);
118
+ }
119
+ readline.emitKeypressEvents(process.stdin);
120
+ if (process.stdin.isTTY)
121
+ process.stdin.setRawMode(true);
122
+ process.stdin.resume();
123
+ process.stdin.on('keypress', onKey);
124
+ paint();
125
+ });
126
+ }
127
+ /**
128
+ * The block that stays up while the daemon waits to be claimed.
129
+ *
130
+ * It shows the code and the link even when the browser opened fine. That is
131
+ * not clutter — a browser that opened on the wrong profile, or behind another
132
+ * window, is invisible from here, and the only recovery is the thing this
133
+ * block is holding.
134
+ */
135
+ function renderWaiting(opts) {
136
+ const { product, link, code, opened } = opts;
137
+ write('\n');
138
+ if (opened) {
139
+ write(` ${(0, render_1.green)('✦')} ${(0, render_1.bold)(`Waiting in your browser…`)}\n`);
140
+ write('\n');
141
+ write(` ${(0, render_1.dim)('Nothing opened? Go to')} ${(0, render_1.cyan)(link)}\n`);
142
+ }
143
+ else {
144
+ write(` ${(0, render_1.bold)(`Connect this machine to ${product}`)}\n`);
145
+ write('\n');
146
+ write(` ${(0, render_1.dim)('1.')} Open ${(0, render_1.cyan)(link.replace(`/${code}`, ''))}\n`);
147
+ write(` ${(0, render_1.dim)('2.')} Enter this code: ${(0, render_1.bold)((0, render_1.green)(code))}\n`);
148
+ write('\n');
149
+ write(` ${(0, render_1.dim)('or open this directly:')} ${(0, render_1.cyan)(link)}\n`);
150
+ }
151
+ if (opened)
152
+ write(` ${(0, render_1.dim)('or enter this code:')} ${(0, render_1.bold)((0, render_1.green)(code))}\n`);
153
+ write('\n');
154
+ let n = 0;
155
+ let painted = false;
156
+ const line = () => ` ${(0, render_1.green)(render_1.SPIN[n % render_1.SPIN.length])} ${(0, render_1.dim)('waiting for you to confirm · expires in 15 minutes')}`;
157
+ const tick = () => {
158
+ if (painted)
159
+ rewind(1);
160
+ write(line() + '\n');
161
+ painted = true;
162
+ n++;
163
+ };
164
+ tick();
165
+ const timer = setInterval(tick, 120);
166
+ // A spinner is not a reason to keep the process alive.
167
+ timer.unref?.();
168
+ return {
169
+ stop() {
170
+ clearInterval(timer);
171
+ if (painted)
172
+ rewind(1);
173
+ },
174
+ };
175
+ }
176
+ function renderConnected(name) {
177
+ write(` ${(0, render_1.green)('✓')} ${(0, render_1.bold)('Connected')} ${(0, render_1.dim)('as')} ${name}\n\n`);
178
+ }
@@ -0,0 +1,20 @@
1
+ export interface DisconnectResult {
2
+ ok: boolean;
3
+ cancelled: number;
4
+ /** The local pairing was cleared but +Ai was not told. */
5
+ serverFailed?: boolean;
6
+ error?: string;
7
+ }
8
+ /** The question to ask before doing it. */
9
+ export declare function describeDisconnect(inflight: number): string;
10
+ /** What to say once it is done. */
11
+ export declare function summariseDisconnect(r: DisconnectResult): string;
12
+ /**
13
+ * Tell +Ai, then forget the pairing locally.
14
+ *
15
+ * Local cleanup happens even when the server call fails. A node whose token
16
+ * the user has decided to abandon should not keep using it because the network
17
+ * was down at the wrong moment — and the alternative, refusing to disconnect,
18
+ * strands them on a machine they may be trying to hand back.
19
+ */
20
+ export declare function disconnectNode(): Promise<DisconnectResult>;