@agentdeck/bridge 1.0.14 → 1.0.17

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.
Files changed (40) hide show
  1. package/dist/auth.d.ts +30 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +162 -29
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +99 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/daemon-server.d.ts.map +1 -1
  9. package/dist/daemon-server.js +190 -27
  10. package/dist/daemon-server.js.map +1 -1
  11. package/dist/daemon.js +1 -1
  12. package/dist/esp32-serial.d.ts +23 -1
  13. package/dist/esp32-serial.d.ts.map +1 -1
  14. package/dist/esp32-serial.js +50 -1
  15. package/dist/esp32-serial.js.map +1 -1
  16. package/dist/http-auth-gate.d.ts +22 -5
  17. package/dist/http-auth-gate.d.ts.map +1 -1
  18. package/dist/http-auth-gate.js +14 -2
  19. package/dist/http-auth-gate.js.map +1 -1
  20. package/dist/pairing-window.d.ts +95 -0
  21. package/dist/pairing-window.d.ts.map +1 -0
  22. package/dist/pairing-window.js +268 -0
  23. package/dist/pairing-window.js.map +1 -0
  24. package/dist/passive-observer.d.ts +1 -0
  25. package/dist/passive-observer.d.ts.map +1 -1
  26. package/dist/passive-observer.js +13 -4
  27. package/dist/passive-observer.js.map +1 -1
  28. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  29. package/dist/pixoo/pixoo-renderer.js +17 -9
  30. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  31. package/dist/session-registry.d.ts +1 -0
  32. package/dist/session-registry.d.ts.map +1 -1
  33. package/dist/usage-event.d.ts.map +1 -1
  34. package/dist/usage-event.js +23 -4
  35. package/dist/usage-event.js.map +1 -1
  36. package/dist/ws-server.d.ts +36 -0
  37. package/dist/ws-server.d.ts.map +1 -1
  38. package/dist/ws-server.js +105 -2
  39. package/dist/ws-server.js.map +1 -1
  40. package/package.json +13 -13
package/dist/auth.d.ts CHANGED
@@ -1,5 +1,30 @@
1
1
  /** Read existing token or generate a new one. */
2
2
  export declare function getOrCreateToken(): string;
3
+ /** Tokens accepted for authentication but never handed out. */
4
+ export declare function getAcceptedTokens(): string[];
5
+ /**
6
+ * Adopt the pairing token of a daemon that is already serving this machine.
7
+ *
8
+ * A machine has ONE fleet of paired devices but can be served by either the
9
+ * Node CLI daemon or the macOS app's in-process Swift daemon, and the two keep
10
+ * their token in different files — the sandboxed app cannot read
11
+ * `~/.agentdeck/auth-token` and this process must not poke at the app's
12
+ * container. So the credential travels over the one channel both can always
13
+ * use: the incumbent's loopback `/health`, which carries `pairingToken` to
14
+ * same-machine callers only. Whoever starts second adopts what the incumbent
15
+ * is already serving, so a later handover in EITHER direction is
16
+ * credential-neutral and start order stops mattering.
17
+ *
18
+ * The token we were holding moves to the accepted list rather than being
19
+ * dropped: devices provisioned under it must keep working until they are
20
+ * re-armed.
21
+ *
22
+ * No new trust is granted — `probeDaemonHealth` dials 127.0.0.1, and
23
+ * same-machine peers are already fully trusted by this daemon's access model.
24
+ *
25
+ * Returns true when the served token actually changed.
26
+ */
27
+ export declare function adoptPeerToken(peerToken: unknown): boolean;
3
28
  /** Check if a connection originates from this machine (localhost or own LAN IPs). */
4
29
  export declare function isLocalConnection(ip: string): boolean;
5
30
  /**
@@ -10,7 +35,11 @@ export declare function isLocalConnection(ip: string): boolean;
10
35
  * retired without hand-editing the file.
11
36
  */
12
37
  export declare function rotateToken(): string;
13
- /** Validate a token string against the stored token. */
38
+ /**
39
+ * Validate a token against the one we serve, then against the ones we still
40
+ * accept (a token superseded by `adoptPeerToken`). Every candidate is compared
41
+ * — no early exit — so the answer costs the same whichever key matched.
42
+ */
14
43
  export declare function validateToken(token: string): boolean;
15
44
  /** Build a ws:// URL with auth token for QR code pairing. */
16
45
  export declare function getWsUrl(port: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAaA,iDAAiD;AACjD,wBAAgB,gBAAgB,IAAI,MAAM,CA4BzC;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAYrD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAOpC;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CASpD;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7C"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAiDA,iDAAiD;AACjD,wBAAgB,gBAAgB,IAAI,MAAM,CA8BzC;AAOD,+DAA+D;AAC/D,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAiB5C;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAmB1D;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAYrD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAWpC;AAYD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7C"}
package/dist/auth.js CHANGED
@@ -4,38 +4,157 @@ import { join } from 'path';
4
4
  import { homedir, networkInterfaces } from 'os';
5
5
  import { getLanIp } from '@agentdeck/shared';
6
6
  import { debug } from './logger.js';
7
- const AGENTDECK_DIR = join(homedir(), '.agentdeck');
8
- const TOKEN_FILE = join(AGENTDECK_DIR, 'auth-token');
7
+ const LEGACY_DIR = join(homedir(), '.agentdeck');
9
8
  const TOKEN_LENGTH = 32; // 32 hex chars = 16 bytes
9
+ /** Bounded so a long-lived machine cannot accumulate an unbounded key ring. */
10
+ const MAX_ACCEPTED_TOKENS = 4;
11
+ /**
12
+ * Every other module resolves its state through
13
+ * `AGENTDECK_DATA_DIR || ~/.agentdeck`; this one used to hardcode the home
14
+ * path, so a daemon started with a custom data dir kept its pairing token
15
+ * somewhere else than the rest of its state.
16
+ */
17
+ function dataDir() {
18
+ return process.env.AGENTDECK_DATA_DIR || LEGACY_DIR;
19
+ }
20
+ function tokenFile(dir = dataDir()) {
21
+ return join(dir, 'auth-token');
22
+ }
23
+ /**
24
+ * Tokens this daemon still ACCEPTS but no longer hands out — the credential it
25
+ * held before adopting a peer daemon's one (see `adoptPeerToken`). Without it,
26
+ * convergence itself would lock out every device that was provisioned a moment
27
+ * earlier, which is the failure this whole path exists to prevent.
28
+ */
29
+ function acceptedFile(dir = dataDir()) {
30
+ return join(dir, 'auth-token-accepted');
31
+ }
32
+ // Cached per resolved directory, so pointing AGENTDECK_DATA_DIR somewhere else
33
+ // re-reads instead of serving the previous directory's credential.
10
34
  let cachedToken = null;
35
+ let cachedAccepted = null;
36
+ function readTokenFile(path) {
37
+ try {
38
+ if (!existsSync(path))
39
+ return null;
40
+ const token = readFileSync(path, 'utf-8').trim();
41
+ return token.length >= TOKEN_LENGTH ? token : null;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
11
47
  /** Read existing token or generate a new one. */
12
48
  export function getOrCreateToken() {
13
- if (cachedToken)
14
- return cachedToken;
49
+ const dir = dataDir();
50
+ if (cachedToken?.dir === dir)
51
+ return cachedToken.token;
52
+ let token = readTokenFile(tokenFile(dir));
53
+ // A custom data dir that has no token yet inherits the legacy one rather than
54
+ // minting a fresh one: generating here would silently un-pair every device
55
+ // the machine already has.
56
+ const inherited = token === null && dir !== LEGACY_DIR
57
+ ? readTokenFile(tokenFile(LEGACY_DIR))
58
+ : null;
59
+ if (inherited)
60
+ token = inherited;
61
+ const generated = token === null;
62
+ if (token === null)
63
+ token = randomBytes(TOKEN_LENGTH / 2).toString('hex');
64
+ if (generated || inherited) {
65
+ try {
66
+ mkdirSync(dir, { recursive: true });
67
+ writeFileSync(tokenFile(dir), token + '\n', { mode: 0o600 });
68
+ debug('auth', generated
69
+ ? `Generated new auth token → ${tokenFile(dir)}`
70
+ : `Carried the existing auth token into ${tokenFile(dir)}`);
71
+ }
72
+ catch (err) {
73
+ debug('auth', `Failed to write token file: ${err}`);
74
+ }
75
+ }
76
+ cachedToken = { dir, token };
77
+ return token;
78
+ }
79
+ /** Shape check for a token handed to us by someone else. */
80
+ function isWellFormedToken(token) {
81
+ return typeof token === 'string' && token.trim().length >= TOKEN_LENGTH;
82
+ }
83
+ /** Tokens accepted for authentication but never handed out. */
84
+ export function getAcceptedTokens() {
85
+ const dir = dataDir();
86
+ if (cachedAccepted?.dir === dir)
87
+ return cachedAccepted.tokens;
88
+ let tokens = [];
15
89
  try {
16
- if (existsSync(TOKEN_FILE)) {
17
- const token = readFileSync(TOKEN_FILE, 'utf-8').trim();
18
- if (token.length >= TOKEN_LENGTH) {
19
- cachedToken = token;
20
- return token;
21
- }
90
+ if (existsSync(acceptedFile(dir))) {
91
+ tokens = readFileSync(acceptedFile(dir), 'utf-8')
92
+ .split('\n')
93
+ .map(line => line.trim())
94
+ .filter(isWellFormedToken)
95
+ .slice(0, MAX_ACCEPTED_TOKENS);
22
96
  }
23
97
  }
24
98
  catch {
25
- // Fall through to generate
99
+ tokens = [];
26
100
  }
27
- // Generate new token
28
- const token = randomBytes(TOKEN_LENGTH / 2).toString('hex');
101
+ cachedAccepted = { dir, tokens };
102
+ return tokens;
103
+ }
104
+ function writeAcceptedTokens(tokens) {
105
+ const dir = dataDir();
106
+ const bounded = tokens.slice(0, MAX_ACCEPTED_TOKENS);
107
+ cachedAccepted = { dir, tokens: bounded };
29
108
  try {
30
- mkdirSync(AGENTDECK_DIR, { recursive: true });
31
- writeFileSync(TOKEN_FILE, token + '\n', { mode: 0o600 });
32
- debug('auth', `Generated new auth token → ${TOKEN_FILE}`);
109
+ mkdirSync(dir, { recursive: true });
110
+ writeFileSync(acceptedFile(dir), bounded.map(t => t + '\n').join(''), { mode: 0o600 });
33
111
  }
34
112
  catch (err) {
35
- debug('auth', `Failed to write token file: ${err}`);
113
+ debug('auth', `Failed to write accepted-token file: ${err}`);
36
114
  }
37
- cachedToken = token;
38
- return token;
115
+ }
116
+ /**
117
+ * Adopt the pairing token of a daemon that is already serving this machine.
118
+ *
119
+ * A machine has ONE fleet of paired devices but can be served by either the
120
+ * Node CLI daemon or the macOS app's in-process Swift daemon, and the two keep
121
+ * their token in different files — the sandboxed app cannot read
122
+ * `~/.agentdeck/auth-token` and this process must not poke at the app's
123
+ * container. So the credential travels over the one channel both can always
124
+ * use: the incumbent's loopback `/health`, which carries `pairingToken` to
125
+ * same-machine callers only. Whoever starts second adopts what the incumbent
126
+ * is already serving, so a later handover in EITHER direction is
127
+ * credential-neutral and start order stops mattering.
128
+ *
129
+ * The token we were holding moves to the accepted list rather than being
130
+ * dropped: devices provisioned under it must keep working until they are
131
+ * re-armed.
132
+ *
133
+ * No new trust is granted — `probeDaemonHealth` dials 127.0.0.1, and
134
+ * same-machine peers are already fully trusted by this daemon's access model.
135
+ *
136
+ * Returns true when the served token actually changed.
137
+ */
138
+ export function adoptPeerToken(peerToken) {
139
+ if (!isWellFormedToken(peerToken))
140
+ return false;
141
+ const adopted = peerToken.trim();
142
+ const current = getOrCreateToken();
143
+ if (adopted === current)
144
+ return false;
145
+ const accepted = [current, ...getAcceptedTokens().filter(t => t !== current && t !== adopted)];
146
+ writeAcceptedTokens(accepted);
147
+ const dir = dataDir();
148
+ try {
149
+ mkdirSync(dir, { recursive: true });
150
+ writeFileSync(tokenFile(dir), adopted + '\n', { mode: 0o600 });
151
+ }
152
+ catch (err) {
153
+ debug('auth', `Failed to persist adopted token: ${err}`);
154
+ }
155
+ cachedToken = { dir, token: adopted };
156
+ debug('auth', 'Adopted the incumbent daemon\'s pairing token; previous one still accepted');
157
+ return true;
39
158
  }
40
159
  /** Check if a connection originates from this machine (localhost or own LAN IPs). */
41
160
  export function isLocalConnection(ip) {
@@ -62,24 +181,38 @@ export function isLocalConnection(ip) {
62
181
  */
63
182
  export function rotateToken() {
64
183
  const token = randomBytes(TOKEN_LENGTH / 2).toString('hex');
65
- mkdirSync(AGENTDECK_DIR, { recursive: true });
66
- writeFileSync(TOKEN_FILE, token + '\n', { mode: 0o600 });
67
- cachedToken = token;
68
- debug('auth', `Rotated auth token → ${TOKEN_FILE}`);
184
+ const dir = dataDir();
185
+ mkdirSync(dir, { recursive: true });
186
+ writeFileSync(tokenFile(dir), token + '\n', { mode: 0o600 });
187
+ cachedToken = { dir, token };
188
+ // Retiring a leaked token has to retire the key ring with it, or the leaked
189
+ // one survives in the accepted list and rotation buys nothing.
190
+ writeAcceptedTokens([]);
191
+ debug('auth', `Rotated auth token → ${tokenFile(dir)}`);
69
192
  return token;
70
193
  }
71
- /** Validate a token string against the stored token. */
72
- export function validateToken(token) {
73
- const stored = getOrCreateToken();
194
+ function matchesToken(candidate, stored) {
74
195
  // Constant-time comparison to prevent timing attacks
75
- if (token.length !== stored.length)
196
+ if (candidate.length !== stored.length)
76
197
  return false;
77
198
  let result = 0;
78
- for (let i = 0; i < token.length; i++) {
79
- result |= token.charCodeAt(i) ^ stored.charCodeAt(i);
199
+ for (let i = 0; i < candidate.length; i++) {
200
+ result |= candidate.charCodeAt(i) ^ stored.charCodeAt(i);
80
201
  }
81
202
  return result === 0;
82
203
  }
204
+ /**
205
+ * Validate a token against the one we serve, then against the ones we still
206
+ * accept (a token superseded by `adoptPeerToken`). Every candidate is compared
207
+ * — no early exit — so the answer costs the same whichever key matched.
208
+ */
209
+ export function validateToken(token) {
210
+ let ok = matchesToken(token, getOrCreateToken());
211
+ for (const accepted of getAcceptedTokens()) {
212
+ ok = matchesToken(token, accepted) || ok;
213
+ }
214
+ return ok;
215
+ }
83
216
  /** Build a ws:// URL with auth token for QR code pairing. */
84
217
  export function getWsUrl(port) {
85
218
  const ip = getLanIp();
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AAyP1D,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAM,CAAC;AAOtF;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE;AA+BD,gFAAgF;AAChF,QAAA,MAAM,cAAc,EAAE,MAAM,CAAC,aAAa,GAAG,WAAW,GAAG,UAAU,EAAE,MAAM,CAI5E,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAsBjE;AAKD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,EAAE,CAyBV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAIR;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,OAAO,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAER;AAID,QAAA,MAAM,OAAO,SAAgB,CAAC;AAqvE9B,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AAyP1D,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAM,CAAC;AAOtF;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE;AA+BD,gFAAgF;AAChF,QAAA,MAAM,cAAc,EAAE,MAAM,CAAC,aAAa,GAAG,WAAW,GAAG,UAAU,EAAE,MAAM,CAI5E,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAsBjE;AAKD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,EAAE,CAyBV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAIR;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,OAAO,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAER;AAID,QAAA,MAAM,OAAO,SAAgB,CAAC;AAs2E9B,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -590,6 +590,7 @@ daemon
590
590
  .option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
591
591
  .action(async (opts) => {
592
592
  const { findExistingDaemon, probeDaemonHealth, readDaemonInfo, removeDaemonInfo, removeDaemonSession, requestDaemonStandDown, requestDaemonShutdown, waitForDaemonExit, waitForPortBindable } = await import('./session-registry.js');
593
+ const { adoptPeerToken } = await import('./auth.js');
593
594
  // Reverse two-tier upgrade path: the macOS app may already own the canonical
594
595
  // port with its in-process Swift daemon (Tier 1 — limited: no ADB devices,
595
596
  // no subscription usage, …). A plain "already running → exit" would strand
@@ -601,6 +602,14 @@ daemon
601
602
  const targetPort = opts.port ? parseInt(String(opts.port), 10) : BRIDGE_WS_PORT;
602
603
  const incumbent = await probeDaemonHealth(targetPort);
603
604
  if (incumbent?.mode === 'daemon') {
605
+ // Take over the fleet's credential along with the port. The app's daemon
606
+ // keeps its token inside its sandbox container, unreadable from here, so
607
+ // this loopback answer is the only place we can learn what every paired
608
+ // board is currently holding. Adopting it BEFORE the stand-down means the
609
+ // handover costs no device its pairing.
610
+ if (adoptPeerToken(incumbent.pairingToken)) {
611
+ log(`Adopted the incumbent daemon's pairing token so paired devices survive the handover.`);
612
+ }
604
613
  if (incumbent.isSwift) {
605
614
  log(`AgentDeck app's in-process daemon holds port ${targetPort} — requesting stand-down to take over with the full CLI feature set…`);
606
615
  // Prefer /stand-down (clean demote: the app stays running as a client).
@@ -1349,6 +1358,96 @@ program
1349
1358
  // qrcode not available
1350
1359
  }
1351
1360
  });
1361
+ program
1362
+ .command('pair')
1363
+ .description('Pair a device with a one-time code (no camera or USB needed)')
1364
+ .option('-p, --port <port>', 'Daemon port (auto-detected)')
1365
+ .option('-t, --ttl <seconds>', 'How long the window stays open')
1366
+ .option('-n, --devices <count>', 'How many devices this window may pair', '1')
1367
+ .option('--adopt <ip...>', 'Re-arm ESP32 board(s) at these IPs over WiFi — no cable. Read the address off the daemon\'s "Rejected <ip> (esp32)" line')
1368
+ .action(async (opts) => {
1369
+ const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
1370
+ const { formatPairingCode, getLanIp, PAIRING_WINDOW_MS, PAIRING_MAX_FAILED_ATTEMPTS } = await import('@agentdeck/shared');
1371
+ const info = readDaemonInfo();
1372
+ const port = opts.port != null
1373
+ ? parseInt(opts.port, 10)
1374
+ : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
1375
+ const ttlMs = opts.ttl != null ? Math.round(parseFloat(opts.ttl) * 1000) : PAIRING_WINDOW_MS;
1376
+ const redemptions = parseInt(opts.devices, 10) || 1;
1377
+ const adoptEsp32Ips = Array.isArray(opts.adopt)
1378
+ ? opts.adopt
1379
+ : (typeof opts.adopt === 'string' ? [opts.adopt] : []);
1380
+ const daemon = `http://127.0.0.1:${port}`;
1381
+ let opened;
1382
+ try {
1383
+ const res = await fetch(`${daemon}/pair/open`, {
1384
+ method: 'POST',
1385
+ headers: { 'Content-Type': 'application/json' },
1386
+ body: JSON.stringify({ ttlMs, redemptions, adoptEsp32Ips }),
1387
+ signal: AbortSignal.timeout(3000),
1388
+ });
1389
+ if (!res.ok)
1390
+ throw new Error(`daemon answered ${res.status}`);
1391
+ opened = await res.json();
1392
+ }
1393
+ catch (err) {
1394
+ console.error(`Could not reach the daemon on port ${port}: ${err instanceof Error ? err.message : String(err)}`);
1395
+ console.error('Start it with "agentdeck daemon start", then try again.');
1396
+ process.exitCode = 1;
1397
+ return;
1398
+ }
1399
+ const lanIp = getLanIp();
1400
+ log('');
1401
+ log(` Pairing code: ${formatPairingCode(opened.code)}`);
1402
+ log('');
1403
+ log(` On the device: Settings → Connection → "Pair with code", enter it there.`);
1404
+ log(` The device finds this Mac itself (${lanIp}:${port}) — no cable, no QR scan.`);
1405
+ log('');
1406
+ log(` Valid for ${Math.round((opened.expiresAt - Date.now()) / 1000)}s, `
1407
+ + `${opened.redemptions} device(s), ${PAIRING_MAX_FAILED_ATTEMPTS} wrong tries.`);
1408
+ if (opened.adoptEsp32Ips?.length) {
1409
+ log('');
1410
+ log(` Also re-arming ESP32 board(s) at ${opened.adoptEsp32Ips.join(', ')} —`);
1411
+ log(' they need no code; the credential is pushed on their next connect.');
1412
+ }
1413
+ log('');
1414
+ // Watch the window until it closes, so the operator sees who paired — and
1415
+ // sees a wrong code as it happens, which is the point of a short window
1416
+ // somebody is standing in front of.
1417
+ const seenRedemptions = new Set();
1418
+ let seenFailures = 0;
1419
+ for (;;) {
1420
+ await new Promise((r) => setTimeout(r, 1000));
1421
+ let status;
1422
+ try {
1423
+ const res = await fetch(`${daemon}/pair/status`, { signal: AbortSignal.timeout(2000) });
1424
+ status = await res.json();
1425
+ }
1426
+ catch {
1427
+ console.error('Lost contact with the daemon — the pairing window is no longer being watched.');
1428
+ process.exitCode = 1;
1429
+ return;
1430
+ }
1431
+ for (const r of status.redemptions) {
1432
+ const key = `${r.at}:${r.ip}`;
1433
+ if (seenRedemptions.has(key))
1434
+ continue;
1435
+ seenRedemptions.add(key);
1436
+ log(` ✓ Paired ${r.name} (${r.kind}) at ${r.ip}`);
1437
+ }
1438
+ for (const f of status.failures.slice(seenFailures)) {
1439
+ log(` ✗ Wrong code from ${f.ip} — ${status.attemptsRemaining} attempt(s) left`);
1440
+ }
1441
+ seenFailures = status.failures.length;
1442
+ if (!status.open) {
1443
+ log('');
1444
+ log(seenRedemptions.size > 0
1445
+ ? `Pairing window closed. ${seenRedemptions.size} device(s) paired.`
1446
+ : 'Pairing window closed with nothing paired. Run "agentdeck pair" again to retry.');
1447
+ return;
1448
+ }
1449
+ }
1450
+ });
1352
1451
  program
1353
1452
  .command('token')
1354
1453
  .description('Show or rotate the pairing token (~/.agentdeck/auth-token)')