bridgerator 1.0.0 → 1.0.6

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/README.md CHANGED
@@ -10,26 +10,39 @@ ChatGPT ──OAuth 2.1 + MCP (HTTPS)──► relay (mcp.bridgerator.recondeck.
10
10
 
11
11
  This repository is the part that runs on your PC. It is small on purpose so you (or an AI agent
12
12
  installing it for you) can read all of it: `src/bridge.mjs` (forwarder), `src/policy.mjs` (access
13
- rules), `src/dc-isolate.mjs` (separate engine config), `src/cli.mjs` (setup/autostart), `install.ps1` (installer).
13
+ rules), `src/dc-isolate.mjs` (separate engine config), `src/cli.mjs` (setup/autostart), `src/pair.mjs` (browser pairing), `install.ps1` (installer).
14
+
15
+ ## What people use it for
16
+
17
+ - "Sort my Downloads folder by type and month" — ChatGPT lists, plans and moves the files on your PC.
18
+ - "Find every invoice PDF in Documents and rename it by date" — search and rename in plain words.
19
+ - "Why is drive C almost full?" — disk usage, big folders, what is safe to delete.
20
+ - "Check the event log for errors since yesterday" / "Read the end of this log" — diagnostics.
21
+ - Work on code: "explain how D:\\Projects\\app is built and run its tests".
22
+
23
+ Everything runs as your Windows user and only inside the folders you allow.
14
24
 
15
25
  ## Plans
16
26
 
17
- Free: 15,000 tool calls per month per account, any number of PCs. Pro: $15/month, unlimited.
27
+ Free: 10,000 tool calls per month per account, any number of PCs. Pro: $10/month, unlimited.
18
28
  Details: https://bridgerator.recondeck.com/pricing
19
29
 
20
30
  ## Install (Windows)
21
31
 
22
- 1. Create an account and a device key at https://bridgerator.recondeck.com.
23
- 2. In PowerShell (no admin rights needed):
32
+ 1. In ChatGPT, add the Bridgerator app and sign in with Google (an account is created for you), or sign up at https://bridgerator.recondeck.com.
33
+ 2. On the PC, in PowerShell (no admin rights needed):
24
34
  ```powershell
25
- & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1))) -Token <device key>
35
+ & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1)))
26
36
  ```
27
- It installs Node.js LTS if missing (winget), installs this package and runs `bridgerator setup`.
28
- Prefer to read first? Download `install.ps1`, read it, run it with `-Token`.
29
- 3. In ChatGPT: Settings → Apps & Connectors → Advanced → Developer mode → Create → URL
30
- `https://mcp.bridgerator.recondeck.com/mcp`, authentication OAuth, sign in with your account.
37
+ It downloads portable Node.js LTS if missing (checksum-verified, no admin), installs this package and runs `bridgerator setup`.
38
+ 3. A browser tab opens with a code: check that it matches the one in PowerShell and click **Connect this computer**. Done — no key to copy.
39
+
40
+ Update later with the same line (the key and folder settings are kept). Prefer to read first? Download `install.ps1`, read it, then run it.
41
+ Servers or PCs without a browser: create a device key in your account (Add a computer → Advanced) and add `-Token <device key>` to the line.
42
+
43
+ Manual install (PowerShell): `$env:PUPPETEER_SKIP_DOWNLOAD=1; npm install -g bridgerator`, then `bridgerator setup` (or `bridgerator setup --token <key>`). The variable skips a ~700 MB Chrome download that only PDF export needs; PDF export fetches a browser itself on first use.
31
44
 
32
- Manual install: `npm install -g github:s-lexer/bridgerator` then `bridgerator setup --token <key>`.
45
+ Other MCP clients: server URL `https://mcp.bridgerator.recondeck.com/mcp`, OAuth sign-in.
33
46
 
34
47
  ## Access rules — what the assistant can and cannot touch
35
48
 
@@ -63,7 +76,7 @@ Bridgerator gives its bundled engine its **own** config folder (`%APPDATA%\Bridg
63
76
  ## Commands
64
77
 
65
78
  ```
66
- bridgerator setup --token <key> [--allow <dir>]... [--deny <dir>]... # config + autostart + start
79
+ bridgerator setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... # pair in the browser (no key yet / --pair) + config + autostart + start
67
80
  bridgerator run # foreground (what autostart runs)
68
81
  bridgerator status # config (token masked) + log tail
69
82
  bridgerator uninstall # stop + remove autostart
@@ -90,7 +103,7 @@ MIT. The bundled open-source MCP engine is MIT-licensed as well; its copyright n
90
103
 
91
104
  ### По-русски коротко
92
105
  Bridgerator подключает ваш ПК к ChatGPT: файлы, поиск, терминал. Мост только исходящий, входящих
93
- портов нет. Установка — одна строка PowerShell с ключом устройства из кабинета (см. выше).
106
+ портов нет. Установка — одна строка PowerShell, затем подтверждение компьютера во вкладке браузера (см. выше).
94
107
  Разрешённые папки — `allowedDirectories`, исключения внутри них — `deniedDirectories`
95
108
  (исключения обеспечивает сам Bridgerator). Терминал папками не
96
109
  ограничивается — для строгих границ выключите его через `disabledTools`.
package/install.ps1 CHANGED
@@ -1,17 +1,21 @@
1
- # Bridgerator installer for Windows (PowerShell 5.1+). Source: https://github.com/s-lexer/bridgerator
1
+ # Bridgerator installer for Windows (PowerShell 5.1+). Source: npm package "bridgerator" (https://www.npmjs.com/package/bridgerator)
2
2
  # Usage (one line, from your Bridgerator account page):
3
- # & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1))) -Token <device key>
3
+ # & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1)))
4
+ # A browser tab opens: sign in and confirm this computer — no key to paste. Run again to update
5
+ # (keeps the key and folders). Servers / headless PCs: add -Token <device key> from the account page.
4
6
  # What it does: installs Node.js LTS if missing (portable zip, no admin), installs the bridgerator npm package,
5
- # then runs `bridgerator setup` (config + hidden autostart at logon for THIS user, no admin rights),
7
+ # then runs `bridgerator setup` (pairing in the browser if there is no key yet, config + hidden autostart
8
+ # at logon for THIS user, no admin rights),
6
9
  # then opens the welcome page in the default browser (skip with -NoBrowser).
7
10
  param(
8
- [Parameter(Mandatory = $true)][string]$Token,
11
+ [string]$Token,
9
12
  [string]$Package = 'https://bridgerator.recondeck.com/bridgerator.tgz',
10
13
  [string[]]$Allow,
11
14
  [string[]]$Deny,
12
15
  [switch]$NoBrowser
13
16
  )
14
17
  $ErrorActionPreference = 'Stop'
18
+ $paired = $Token -or (Test-Path (Join-Path $env:APPDATA 'Bridgerator\config.json'))
15
19
 
16
20
  function Refresh-Path {
17
21
  $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
@@ -61,12 +65,14 @@ Write-Host "Installing bridgerator from $Package ..."
61
65
  if ($LASTEXITCODE -ne 0) { throw 'npm install failed' }
62
66
  Refresh-Path
63
67
 
64
- $setupArgs = @('setup', '--token', $Token)
68
+ $setupArgs = @('setup')
69
+ if ($Token) { $setupArgs += @('--token', $Token) }
65
70
  foreach ($a in $Allow) { $setupArgs += @('--allow', $a) }
66
71
  foreach ($d in $Deny) { $setupArgs += @('--deny', $d) }
72
+ if (-not $paired) { Write-Host 'Next: a browser tab opens - sign in to Bridgerator and confirm this computer.' }
67
73
  & bridgerator @setupArgs
68
74
  if ($LASTEXITCODE -ne 0) { throw 'bridgerator setup failed' }
69
- Write-Host 'Done. Check with: bridgerator status'
75
+ Write-Host 'Done. This computer is connected. Check with: bridgerator status'
70
76
 
71
77
  # Next steps (connect ChatGPT, example prompts) on the welcome page; skipped for -NoBrowser and
72
78
  # non-interactive sessions (scheduled tasks, remote shells). Never fails the install.
package/package.json CHANGED
@@ -1,33 +1,49 @@
1
- {
2
- "name": "bridgerator",
3
- "version": "1.0.0",
4
- "description": "Connect your Windows PC to ChatGPT: files, search and terminal through an outbound-only bridge. Bundles Desktop Commander and adds folder exclusions.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "bridgerator": "src/cli.mjs"
9
- },
10
- "files": [
11
- "src",
12
- "install.ps1",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "engines": {
17
- "node": ">=20"
18
- },
19
- "scripts": {
20
- "start": "node src/cli.mjs run",
21
- "test": "node --test"
22
- },
23
- "dependencies": {
24
- "@modelcontextprotocol/sdk": "^1.29.0",
25
- "@wonderwhy-er/desktop-commander": "0.2.52",
26
- "ws": "^8.20.0"
27
- },
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/s-lexer/bridgerator.git"
31
- },
32
- "homepage": "https://bridgerator.recondeck.com"
33
- }
1
+ {
2
+ "name": "bridgerator",
3
+ "version": "1.0.6",
4
+ "description": "Connect ChatGPT to your Windows PC: files, search and terminal through an outbound-only bridge with folder limits. No inbound ports, no admin rights.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "bridgerator": "src/cli.mjs"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "install.ps1",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "scripts": {
20
+ "start": "node src/cli.mjs run",
21
+ "test": "node --test"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
25
+ "@wonderwhy-er/desktop-commander": "0.2.52",
26
+ "ws": "^8.20.0"
27
+ },
28
+ "homepage": "https://bridgerator.recondeck.com",
29
+ "keywords": [
30
+ "chatgpt",
31
+ "mcp",
32
+ "mcp-server",
33
+ "model-context-protocol",
34
+ "windows",
35
+ "chatgpt-connector",
36
+ "ai-assistant",
37
+ "remote-files",
38
+ "powershell",
39
+ "openai",
40
+ "apps-sdk",
41
+ "desktop-automation",
42
+ "local-files",
43
+ "bridge"
44
+ ],
45
+ "bugs": {
46
+ "url": "https://bridgerator.recondeck.com/support"
47
+ },
48
+ "author": "Bridgerator <info@recondeck.com>"
49
+ }
package/src/bridge.mjs CHANGED
@@ -9,6 +9,7 @@ import WebSocket from 'ws';
9
9
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
10
10
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
11
11
  import { createPolicy, debrand } from './policy.mjs';
12
+ import { showNotice } from './notice.mjs';
12
13
 
13
14
  const RECONNECT_MS = 5000;
14
15
  const DC_ENTRY = createRequire(import.meta.url).resolve('@wonderwhy-er/desktop-commander/dist/index.js');
@@ -70,6 +71,7 @@ export function connect(cfg, dc, log = console.log) {
70
71
  ws.on('message', async (raw) => {
71
72
  let msg; try { msg = JSON.parse(raw); } catch { return; }
72
73
  if (msg.type === 'welcome') { log(`[bridgerator] registered as "${msg.device}"`); return; }
74
+ if (msg.type === 'notice') { showNotice(msg, log); return; }
73
75
  if (msg.type === 'read_resource' && msg.callId) {
74
76
  const r = await dc.readResource(msg.uri);
75
77
  if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'result', callId: msg.callId, ...r }));
package/src/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // bridgerator setup --token <t> [--relay <wss url>] [--allow <dir>]... [--deny <dir>]... → config + autostart + start
2
+ // bridgerator setup [--token <t> | --pair] [--relay <wss url>] [--allow <dir>]... [--deny <dir>]... → config + autostart + start
3
+ // (no key yet: pairs this PC through the browser — the signed-in owner confirms a code)
3
4
  // bridgerator run | status | uninstall
4
5
  import fs from 'node:fs';
5
6
  import os from 'node:os';
@@ -7,6 +8,7 @@ import path from 'node:path';
7
8
  import { spawn, execFileSync } from 'node:child_process';
8
9
  import { fileURLToPath } from 'node:url';
9
10
  import { startDesktopCommander, connect } from './bridge.mjs';
11
+ import { pair, siteOf } from './pair.mjs';
10
12
 
11
13
  const DEFAULT_RELAY = 'wss://mcp.bridgerator.recondeck.com/link';
12
14
  const TASK = 'Bridgerator';
@@ -82,10 +84,14 @@ async function main() {
82
84
  const cmd = o._[0];
83
85
  if (cmd === 'setup') {
84
86
  const prev = readConfig() || {};
85
- const token = o.token || prev.token;
86
- if (!token || token === true) throw new Error('setup needs --token <device key from your Bridgerator account>');
87
+ if (o.token === true) throw new Error('--token needs the device key from your Bridgerator account');
88
+ const relay = o.relay || prev.relay || DEFAULT_RELAY;
89
+ // --pair: get a new key through the browser even if one is configured (computer removed in the account).
90
+ const paired = !o.token && (o.pair || !prev.token);
91
+ const token = o.token || (paired ? (await pair(siteOf(relay), os.hostname())).token : prev.token);
92
+ if (paired) console.log('Confirmed — this computer is linked to your account.');
87
93
  const cfg = {
88
- relay: o.relay || prev.relay || DEFAULT_RELAY,
94
+ relay,
89
95
  token,
90
96
  allowedDirectories: o.allow || prev.allowedDirectories || [home],
91
97
  deniedDirectories: o.deny || prev.deniedDirectories || [path.join(home, '.ssh'), path.join(home, 'AppData')],
@@ -102,12 +108,12 @@ async function main() {
102
108
  stopRunning();
103
109
  startHidden();
104
110
  console.log(`started in background — log: ${LOG}`);
105
- console.log('Next: in ChatGPT add the connector https://mcp.bridgerator.recondeck.com/mcp and sign in.');
111
+ console.log('Next: in ChatGPT add the Bridgerator app and sign in with the same account.');
106
112
  return;
107
113
  }
108
114
  if (cmd === 'run') {
109
115
  const cfg = readConfig();
110
- if (!cfg) throw new Error(`no config at ${CONFIG} — run: bridgerator setup --token <device key>`);
116
+ if (!cfg) throw new Error(`no config at ${CONFIG} — run: bridgerator setup`);
111
117
  const dc = await startDesktopCommander({ ...cfg, dcConfigDir: path.join(DIR, 'dc') }, log);
112
118
  connect(cfg, dc, log);
113
119
  return;
@@ -125,7 +131,7 @@ async function main() {
125
131
  console.log(`stopped and autostart removed. Config kept at ${CONFIG}. Remove the package: npm rm -g bridgerator`);
126
132
  return;
127
133
  }
128
- console.log('usage: bridgerator setup --token <key> [--allow <dir>]... [--deny <dir>]... | run | status | uninstall');
134
+ console.log('usage: bridgerator setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... | run | status | uninstall');
129
135
  }
130
136
 
131
137
  main().catch((e) => { console.error(`[bridgerator] ${e?.message || e}`); process.exit(1); });
@@ -8,11 +8,14 @@
8
8
  // open-browser.js — no-ops: the bridge runs hidden and never opens a browser (the engine opens
9
9
  // its vendor's welcome page on first run).
10
10
  // index.js — no Chrome pre-download at start (~400 MB); PDF export still fetches it on demand.
11
+ // terminal-manager — PowerShell (the default shell) writes UTF-8: the engine decodes output as UTF-8,
12
+ // so on Russian & co. Windows (console code page 866/1251) text came out as garbage.
11
13
  const DIR_EXPR = "(process.env.BRIDGERATOR_DC_CONFIG_DIR || path.join(USER_HOME, '.claude-server-commander'))";
12
14
  const PATCHES = [
13
15
  { file: /[\\/]dist[\\/]config\.js$/, from: "const CONFIG_DIR = path.join(USER_HOME, '.claude-server-commander');", to: `const CONFIG_DIR = ${DIR_EXPR};` },
14
16
  { file: /[\\/]dist[\\/]utils[\\/]toolHistory\.js$/, from: "const historyDir = path.join(os.homedir(), '.claude-server-commander');", to: "const historyDir = process.env.BRIDGERATOR_DC_CONFIG_DIR || path.join(os.homedir(), '.claude-server-commander');" },
15
17
  { file: /[\\/]dist[\\/]utils[\\/]open-browser\.js$/, from: 'export async function openWelcomePage', replaceAll: 'export async function openBrowser() {}\nexport async function openWelcomePage() {}\n' },
18
+ { file: /[\\/]dist[\\/]terminal-manager\.js$/, from: "args: ['-Command', command],", to: "args: ['-Command', '[Console]::OutputEncoding=[Text.Encoding]::UTF8;' + command]," },
16
19
  { file: /[\\/]dist[\\/]index\.js$/, from: ' ensureChromeAvailable();\n', to: ' // bridgerator: no Chrome pre-download\n' },
17
20
  ];
18
21
  const ENGINE = /[\\/]@wonderwhy-er[\\/]desktop-commander[\\/]dist[\\/]/;
package/src/notice.mjs ADDED
@@ -0,0 +1,39 @@
1
+ // Plan notices from the relay (limit reached, Pro ending/ended) shown as a Windows notification with
2
+ // an "Open account" button. They never go into the ChatGPT chat (app rules: no upgrade links there).
3
+ // The link is fixed here — the relay only says which notice, not where to send the user.
4
+ import { execFile } from 'node:child_process';
5
+
6
+ export const ACCOUNT_URL = 'https://bridgerator.recondeck.com/app';
7
+ const day = (iso) => String(iso || '').slice(0, 10);
8
+ const xml = (s) => String(s).replace(/[<>&"']/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&apos;' }[c]));
9
+
10
+ // ponytail: English only for now (owner, 20.10); other languages later.
11
+ export function noticeText(n) {
12
+ const t = {
13
+ limit: ['Bridgerator: monthly limit reached', `${n.calls} of ${n.limit} tool calls used. ChatGPT can't use this PC until the 1st. Turn on Pro in your account.`],
14
+ expired: ['Bridgerator Pro has ended', "You're on Free now (10,000 tool calls a month). Renew Pro in your account."],
15
+ expiring: [`Bridgerator Pro ends on ${day(n.until)}`, 'Pay for 30 more days in your account to keep unlimited calls.'],
16
+ }[n.kind];
17
+ return t && { title: t[0], body: t[1], button: 'Open account' };
18
+ }
19
+
20
+ export function toastXml({ title, body, button }) {
21
+ return `<toast activationType="protocol" launch="${ACCOUNT_URL}"><visual><binding template="ToastGeneric"><text>${xml(title)}</text><text>${xml(body)}</text></binding></visual>`
22
+ + `<actions><action content="${xml(button)}" activationType="protocol" arguments="${ACCOUNT_URL}"/></actions></toast>`;
23
+ }
24
+
25
+ export function showNotice(n, log = console.log) {
26
+ const t = noticeText(n);
27
+ if (!t) return;
28
+ log(`[bridgerator] notice: ${t.title} — ${t.body} ${ACCOUNT_URL}`);
29
+ if (process.platform !== 'win32') return;
30
+ // Own app id under HKCU (no admin): the notification header reads "Bridgerator", not "Windows PowerShell".
31
+ const ps = `$k = 'HKCU:\\Software\\Classes\\AppUserModelId\\Bridgerator'
32
+ if (-not (Test-Path $k)) { New-Item $k -Force | Out-Null; New-ItemProperty $k -Name DisplayName -Value 'Bridgerator' -Force | Out-Null }
33
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
34
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null
35
+ $x = New-Object Windows.Data.Xml.Dom.XmlDocument
36
+ $x.LoadXml('${toastXml(t).replace(/'/g, "''")}')
37
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Bridgerator').Show([Windows.UI.Notifications.ToastNotification]::new($x))`;
38
+ execFile('powershell', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(ps, 'utf16le').toString('base64')], { windowsHide: true }, (e) => { if (e) log(`[bridgerator] notification failed: ${e.message}`); });
39
+ }
package/src/pair.mjs ADDED
@@ -0,0 +1,43 @@
1
+ // Key-less setup: ask the relay for a pairing code, open the confirmation page in the browser, wait
2
+ // until the signed-in owner confirms this computer there, and get this PC's device key back.
3
+ import { spawn } from 'node:child_process';
4
+
5
+ // wss://mcp.bridgerator.recondeck.com/link → https://bridgerator.recondeck.com (the site host, where the account pages live).
6
+ export const siteOf = (relay) => { const u = new URL(relay); return `${u.protocol === 'ws:' ? 'http' : 'https'}://${u.host.replace(/^mcp\./, '')}`; };
7
+
8
+ // Opens the URL in the default browser. Only a URL we checked ourselves reaches cmd's `start`.
9
+ export function openBrowser(url) {
10
+ if (!/^https?:\/\/[A-Za-z0-9.:-]+\/pair\?code=[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(url)) return;
11
+ try {
12
+ const p = process.platform === 'win32'
13
+ ? spawn('cmd', ['/c', `start "" "${url}"`], { windowsHide: true, windowsVerbatimArguments: true, stdio: 'ignore', detached: true })
14
+ : spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { stdio: 'ignore', detached: true });
15
+ p.on('error', () => {});
16
+ p.unref();
17
+ } catch { /* no browser: the URL is printed */ }
18
+ }
19
+
20
+ /** → { token, name } once the owner confirmed; throws when the code expires. */
21
+ export async function pair(site, name, { fetch: f = globalThis.fetch, open = openBrowser, log = console.log, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), everyMs = 3000 } = {}) {
22
+ const post = (path, body) => f(`${site}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(20000) });
23
+ const r = await post('/pair/start', { name });
24
+ if (!r.ok) throw new Error(`could not start pairing (HTTP ${r.status}) — try again in a minute`);
25
+ const s = await r.json();
26
+ const url = `${site}/pair?code=${s.code}`;
27
+ if (!/^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(String(s.code)) || typeof s.poll !== 'string') throw new Error('unexpected answer from the relay');
28
+ log('');
29
+ log(` Confirm this computer in your browser: ${url}`);
30
+ log(` Code: ${s.code} (check that the page shows the same code)`);
31
+ log('');
32
+ open(url);
33
+ log('Waiting for the confirmation (up to 15 minutes)...');
34
+ const until = Date.now() + (Number(s.expiresIn) || 900) * 1000;
35
+ while (Date.now() < until) {
36
+ await sleep(everyMs);
37
+ let p;
38
+ try { p = await post('/pair/poll', { poll: s.poll }); } catch { continue; } // network blip: try again
39
+ if (p.status === 200) { const j = await p.json(); if (j.token) return { token: j.token, name: j.name || name }; }
40
+ if (p.status === 410) break;
41
+ }
42
+ throw new Error('the code expired before it was confirmed — run the command again');
43
+ }