bridgerator 1.0.7 → 1.0.8

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
@@ -40,7 +40,7 @@ Details: https://bridgerator.recondeck.com/pricing
40
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
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
42
 
43
- Manual install (PowerShell): `$env:PUPPETEER_SKIP_DOWNLOAD=1; npm install -g bridgerator`, then `bridgerator.cmd setup` (or `bridgerator.cmd 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.
43
+ Manual install (PowerShell): `$env:PUPPETEER_SKIP_DOWNLOAD=1; npm.cmd install -g bridgerator`, then `bridgerator.cmd setup` (or `bridgerator.cmd 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.
44
44
 
45
45
  Other MCP clients: server URL `https://mcp.bridgerator.recondeck.com/mcp`, OAuth sign-in.
46
46
 
@@ -76,12 +76,13 @@ Bridgerator gives its bundled engine its **own** config folder (`%APPDATA%\Bridg
76
76
  ## Commands
77
77
 
78
78
  ```
79
- bridgerator.cmd setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... # pair in the browser (no key yet / --pair) + config + autostart + start
79
+ bridgerator.cmd setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... # pair in the browser (no key yet, key rejected, or --pair) + config + autostart + start
80
80
  bridgerator.cmd run # foreground (what autostart runs)
81
81
  bridgerator.cmd status # config (token masked) + log tail
82
- bridgerator.cmd uninstall # stop + remove autostart
82
+ bridgerator.cmd uninstall --all # THE uninstall: stop, remove autostart, settings + key, the package and the Node.js the installer added
83
+ bridgerator.cmd uninstall [--keep-config] # stop, remove autostart and %APPDATA%\Bridgerator (kept with --keep-config); program files stay
83
84
  ```
84
- Log: `%APPDATA%\Bridgerator\bridge.log`. Stopping the bridge cuts access immediately.
85
+ Log: `%APPDATA%\Bridgerator\bridge.log`. Stopping the bridge cuts access immediately. After uninstalling, also remove the computer on the account page: https://bridgerator.recondeck.com/app
85
86
 
86
87
  ## Privacy
87
88
 
package/install.ps1 CHANGED
@@ -4,7 +4,7 @@
4
4
  # A browser tab opens: sign in and confirm this computer — no key to paste. Run again to update
5
5
  # (keeps the key and folders). Servers / headless PCs: add -Token <device key> from the account page.
6
6
  # What it does: installs Node.js LTS if missing (portable zip, no admin), installs the bridgerator npm package,
7
- # then runs `bridgerator setup` (pairing in the browser if there is no key yet, config + hidden autostart
7
+ # then runs `bridgerator.cmd setup` (pairing in the browser if there is no key yet, config + hidden autostart
8
8
  # at logon for THIS user, no admin rights),
9
9
  # then opens the welcome page in the default browser (skip with -NoBrowser).
10
10
  param(
@@ -17,17 +17,24 @@ param(
17
17
  $ErrorActionPreference = 'Stop'
18
18
  $paired = $Token -or (Test-Path (Join-Path $env:APPDATA 'Bridgerator\config.json'))
19
19
 
20
+ # Private Node.js (portable zip) used when there is none or it is older than 20; it goes FIRST on
21
+ # the PATH so an old system Node does not shadow it.
22
+ $NodeDir = Join-Path $env:LOCALAPPDATA 'Bridgerator\node'
20
23
  function Refresh-Path {
21
- $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
24
+ $first = if (Test-Path (Join-Path $NodeDir 'node.exe')) { $NodeDir }
25
+ $env:Path = (@($first, [Environment]::GetEnvironmentVariable('Path', 'Machine'), [Environment]::GetEnvironmentVariable('Path', 'User')) | Where-Object { $_ }) -join ';'
22
26
  }
23
27
 
28
+ $admin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
29
+ if ($admin) { Write-Warning "This window runs as Administrator. Bridgerator is installed for THIS Windows user only ($env:USERNAME), with this user's rights. To use it as another user, run the install command in that user's normal (not Administrator) PowerShell." }
30
+
24
31
  [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
25
32
  $ProgressPreference = 'SilentlyContinue'
26
33
 
27
34
  # No winget on many clean Windows installs: take the official Node.js LTS zip into the user's
28
35
  # profile (no admin rights), checked against nodejs.org SHASUMS256, and put it on the user PATH.
29
36
  function Install-PortableNode {
30
- $dir = Join-Path $env:LOCALAPPDATA 'Bridgerator\node'
37
+ $dir = $NodeDir
31
38
  $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
32
39
  $rel = (Invoke-RestMethod 'https://nodejs.org/dist/index.json') | Where-Object { $_.lts -and ($_.files -contains "win-$arch-zip") } | Select-Object -First 1
33
40
  if (-not $rel) { throw 'Could not find a Node.js LTS build. Install Node.js 20+ from https://nodejs.org and run this again.' }
@@ -48,14 +55,30 @@ function Install-PortableNode {
48
55
  New-Item -ItemType Directory -Path (Split-Path $dir) -Force | Out-Null
49
56
  Move-Item (Join-Path $tmp $name) $dir
50
57
  } finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
51
- $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
52
- if (($userPath -split ';') -notcontains $dir) { [Environment]::SetEnvironmentVariable('Path', (@($dir, $userPath) | Where-Object { $_ }) -join ';', 'User') }
58
+ # User PATH through the registry: keeps it REG_EXPAND_SZ with %VARS% unexpanded.
59
+ $k = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
60
+ $userPath = [string]$k.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
61
+ if (($userPath -split ';') -notcontains $dir) { $k.SetValue('Path', ((@($dir) + ($userPath -split ';')) | Where-Object { $_ }) -join ';', [Microsoft.Win32.RegistryValueKind]::ExpandString) }
62
+ $k.Close()
63
+ # Tell Explorer the environment changed (new windows see the new PATH).
64
+ [Environment]::SetEnvironmentVariable('BRIDGERATOR_PATH_REFRESH', '1', 'User'); [Environment]::SetEnvironmentVariable('BRIDGERATOR_PATH_REFRESH', $null, 'User')
53
65
  Refresh-Path
54
66
  }
55
67
 
68
+ # A running bridge (and its engine / ripgrep children, the hidden autostart loop) locks the package
69
+ # and the private Node.js files: stop them before updating.
70
+ Get-CimInstance Win32_Process -Filter "Name='wscript.exe' OR Name='node.exe' OR Name='rg.exe'" -ErrorAction SilentlyContinue |
71
+ Where-Object { $s = "$($_.CommandLine) $($_.ExecutablePath)".Replace('/', '\'); $s -like '*\node_modules\bridgerator\*' -or $s -like '*\Bridgerator\run-hidden.vbs*' } |
72
+ Sort-Object { $_.Name -ne 'wscript.exe' } |
73
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
74
+
75
+ if (Test-Path (Join-Path $NodeDir 'node.exe')) { $env:Path = "$NodeDir;$env:Path" }
56
76
  if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Install-PortableNode }
57
77
  $major = [int]((& node -v).TrimStart('v').Split('.')[0])
58
- if ($major -lt 20) { throw "Node.js 20+ required, found $(& node -v). Update from https://nodejs.org" }
78
+ if ($major -lt 20) {
79
+ Write-Host "Node.js $(& node -v) is too old (20+ needed) - Bridgerator gets its own copy, your Node.js stays as is."
80
+ Install-PortableNode
81
+ }
59
82
 
60
83
  # The engine's PDF export brings puppeteer, whose postinstall downloads Chrome (~700 MB);
61
84
  # skipped here, PDF export fetches a browser itself the first time it is used.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgerator",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
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
5
  "license": "MIT",
6
6
  "type": "module",
package/src/bridge.mjs CHANGED
@@ -8,10 +8,14 @@ import { createRequire } from 'node:module';
8
8
  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
- import { createPolicy, debrand } from './policy.mjs';
11
+ import { createPolicy, debrand, debrandResult } from './policy.mjs';
12
12
  import { showNotice } from './notice.mjs';
13
13
 
14
14
  const RECONNECT_MS = 5000;
15
+ // The relay pings every 30 s: silence for this long means a dead link (sleep/Wi-Fi switch) that
16
+ // TCP alone may not notice for hours — drop it and reconnect.
17
+ const SILENT_MS = 75000;
18
+ const VERSION = createRequire(import.meta.url)('../package.json').version;
15
19
  const DC_ENTRY = createRequire(import.meta.url).resolve('@wonderwhy-er/desktop-commander/dist/index.js');
16
20
  const ISOLATE = new URL('./dc-isolate.mjs', import.meta.url).href;
17
21
 
@@ -26,8 +30,11 @@ export async function startDesktopCommander(cfg, log = console.log) {
26
30
  env: { ...process.env, DC_TELEMETRY_ENABLED: 'false', DESKTOP_COMMANDER_DISABLE_TELEMETRY: '1', DC_FLAG_URL: 'data:application/json,{"flags":{}}', BRIDGERATOR_DC_CONFIG_DIR: cfg.dcConfigDir },
27
31
  stderr: 'ignore',
28
32
  });
29
- const client = new Client({ name: 'bridgerator', version: '1.0.0' }, { capabilities: {} });
33
+ const client = new Client({ name: 'bridgerator', version: VERSION }, { capabilities: {} });
30
34
  await client.connect(transport);
35
+ // Engine gone (crash, killed): nothing works without it — exit, the autostart starts us again.
36
+ let closing = false;
37
+ client.onclose = () => { if (closing) return; log('[bridgerator] engine stopped — exiting (autostart restarts the bridge)'); process.exit(1); };
31
38
  // The PC owner's limits go into Desktop Commander's own config; the model can't change them
32
39
  // (set_config_value is never exposed to the relay, see policy.ALWAYS_HIDDEN).
33
40
  const set = (key, value) => client.callTool({ name: 'set_config_value', arguments: { key, value } });
@@ -47,8 +54,9 @@ export async function startDesktopCommander(cfg, log = console.log) {
47
54
  const refusal = policy.check(name, args);
48
55
  if (refusal) return { content: [{ type: 'text', text: refusal }], isError: true };
49
56
  try {
50
- // Results are NOT debranded: they carry the user's own file contents verbatim.
51
- return policy.filter(await client.callTool({ name, arguments: args || {} }), name, args);
57
+ // Results are NOT debranded (the user's own file contents), except the engine's own
58
+ // get_prompts / get_config texts (debrandResult).
59
+ return policy.filter(debrandResult(name, await client.callTool({ name, arguments: args || {} }), VERSION), name, args);
52
60
  } catch (e) {
53
61
  return { content: [{ type: 'text', text: `bridge error: ${e?.message || e}` }], isError: true };
54
62
  }
@@ -57,18 +65,24 @@ export async function startDesktopCommander(cfg, log = console.log) {
57
65
  if (!resourceUris.has(uri)) return { error: 'unknown resource' };
58
66
  try { return { result: debrand(await client.readResource({ uri })) }; } catch (e) { return { error: String(e?.message || e) }; }
59
67
  }
60
- return { client, tools: visible, resources, call, readResource, close: () => client.close() };
68
+ return { client, tools: visible, resources, call, readResource, close: () => { closing = true; return client.close(); } };
61
69
  }
62
70
 
63
71
  export function connect(cfg, dc, log = console.log) {
64
72
  const ws = new WebSocket(cfg.relay, { handshakeTimeout: 15000 });
65
- let alive = false;
73
+ let alive = false, seen = Date.now();
74
+ const touch = () => { seen = Date.now(); };
75
+ const watch = setInterval(() => { if (Date.now() - seen > SILENT_MS) { log('[bridgerator] relay silent for 75s'); ws.terminate(); } }, 15000);
76
+ ws.on('ping', touch);
77
+ ws.on('pong', touch);
66
78
  ws.on('open', () => {
67
79
  alive = true;
80
+ touch();
68
81
  ws.send(JSON.stringify({ type: 'hello', token: cfg.token, tools: dc.tools, resources: dc.resources }));
69
82
  log('[bridgerator] linked to relay, waiting for calls');
70
83
  });
71
84
  ws.on('message', async (raw) => {
85
+ touch();
72
86
  let msg; try { msg = JSON.parse(raw); } catch { return; }
73
87
  if (msg.type === 'welcome') { log(`[bridgerator] registered as "${msg.device}"`); return; }
74
88
  if (msg.type === 'notice') { showNotice(msg, log); return; }
@@ -84,11 +98,20 @@ export function connect(cfg, dc, log = console.log) {
84
98
  const die = (why) => {
85
99
  if (ws._down) return;
86
100
  ws._down = true;
101
+ clearInterval(watch);
87
102
  log(`[bridgerator] disconnected (${why}); reconnecting in ${RECONNECT_MS / 1000}s`);
88
103
  setTimeout(() => connect(cfg, dc, log), RECONNECT_MS);
89
104
  };
90
105
  ws.on('close', (code) => {
91
- if (code === 4003) { log('[bridgerator] relay rejected the device token — run the install command from your account page again (or `bridgerator.cmd setup --pair`)'); process.exit(2); }
106
+ clearInterval(watch);
107
+ // Exit 0 = done on purpose: the autostart loop (run-hidden.vbs) does not restart it.
108
+ if (code === 4003) {
109
+ log('[bridgerator] This computer is no longer linked to your Bridgerator account. Run the install command from https://bridgerator.recondeck.com/app again.');
110
+ ws._down = true;
111
+ showNotice({ kind: 'unlinked' }, log, () => process.exit(0));
112
+ return;
113
+ }
114
+ if (code === 4004) { log('[bridgerator] another Bridgerator on this computer took over the link — this one stops'); process.exit(0); }
92
115
  die(`close ${code}`);
93
116
  });
94
117
  ws.on('error', (e) => { if (!alive) die(`connect: ${e.message}`); });
package/src/cli.mjs CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
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)
4
- // bridgerator run | status | uninstall
2
+ // bridgerator.cmd setup [--token <t> | --pair] [--relay <wss url>] [--allow <dir>]... [--deny <dir>]... → config + autostart + start
3
+ // (no key yet, or the stored key was rejected: pairs this PC through the browser — the signed-in owner confirms a code)
4
+ // bridgerator.cmd run | status | uninstall [--all | --keep-config]
5
5
  import fs from 'node:fs';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { spawn, execFileSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { startDesktopCommander, connect } from './bridge.mjs';
11
- import { pair, siteOf } from './pair.mjs';
11
+ import { pair, siteOf, checkKey } from './pair.mjs';
12
+ import { ACCOUNT_URL } from './notice.mjs';
12
13
 
13
14
  const DEFAULT_RELAY = 'wss://mcp.bridgerator.recondeck.com/link';
14
15
  const TASK = 'Bridgerator';
@@ -17,6 +18,9 @@ const DIR = process.env.BRIDGERATOR_HOME || path.join(process.env.APPDATA || pat
17
18
  const CONFIG = path.join(DIR, 'config.json');
18
19
  const LOG = path.join(DIR, 'bridge.log');
19
20
  const SELF = fileURLToPath(import.meta.url);
21
+ const PKG_DIR = path.dirname(path.dirname(SELF)); // …\node_modules\bridgerator
22
+ // Private Node.js the installer puts in when there is none (or < 20) — see install.ps1.
23
+ const NODE_DIR = path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Bridgerator', 'node');
20
24
 
21
25
  function parse(argv) {
22
26
  const o = { _: [] };
@@ -48,8 +52,7 @@ function log(line) {
48
52
  function installAutostart() {
49
53
  if (process.platform !== 'win32') return 'autostart: not Windows — start `bridgerator run` yourself';
50
54
  const vbs = path.join(DIR, 'run-hidden.vbs');
51
- const cmd = `"${process.execPath}" "${SELF}" run`;
52
- fs.writeFileSync(vbs, `CreateObject("WScript.Shell").Run "${cmd.replace(/"/g, '""')}", 0, False\r\n`);
55
+ fs.writeFileSync(vbs, '\ufeff' + hiddenVbs(`"${process.execPath}" "${SELF}" run`), 'utf16le');
53
56
  // Registered through PowerShell's ScheduledTasks module: `schtasks /Create … wscript.exe …` spawned
54
57
  // from node is refused (EPERM) on clean Windows 11. No 72 h execution limit (the task default
55
58
  // would silently stop the bridge after three days); restart on failure.
@@ -65,11 +68,35 @@ function installAutostart() {
65
68
  return vbs;
66
69
  }
67
70
 
71
+ // Hidden, waits for the bridge and restarts it after a crash (exit code ≠ 0); exit 0 = stopped on
72
+ // purpose (computer unlinked, replaced). Saved as UTF-16 with BOM: WSH reads a BOM-less file as
73
+ // ANSI, so a Cyrillic & co. profile path (C:\Users\Иван) would break the autostart.
74
+ export function hiddenVbs(cmd) {
75
+ return [
76
+ 'Set sh = CreateObject("WScript.Shell")',
77
+ 'Do',
78
+ ` rc = sh.Run("${cmd.replace(/"/g, '""')}", 0, True)`,
79
+ ' If rc = 0 Then Exit Do',
80
+ ' WScript.Sleep 30000',
81
+ 'Loop',
82
+ 'WScript.Quit rc',
83
+ '',
84
+ ].join('\r\n');
85
+ }
86
+
68
87
  function stopRunning() {
69
88
  if (process.platform !== 'win32') return;
70
- // Only our own bridge processes: node.exe whose command line runs this cli with `run`.
71
- const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*bridgerator*cli.mjs*run*' -and $_.ProcessId -ne ${process.pid} } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }`;
72
- try { execFileSync('powershell', ['-NoProfile', '-Command', ps], { stdio: 'pipe' }); } catch { /* none running */ }
89
+ // Only our own processes: the autostart loop (wscript, first — or it restarts the bridge), the
90
+ // bridge, its engine child and the engine's ripgrep — all run from this package / config folder.
91
+ const q = (s) => `'${s.toLowerCase().replace(/'/g, "''")}'`;
92
+ const ps = `Get-CimInstance Win32_Process -Filter "Name='wscript.exe' OR Name='node.exe' OR Name='rg.exe'" | Where-Object { $s = ("$($_.CommandLine) $($_.ExecutablePath)").ToLower().Replace('/', '\\'); $_.ProcessId -ne ${process.pid} -and ($s.Contains(${q(PKG_DIR)}) -or $s.Contains(${q(path.join(DIR, 'run-hidden.vbs'))})) } | Sort-Object { $_.Name -ne 'wscript.exe' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`;
93
+ try { execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(ps, 'utf16le').toString('base64')], { stdio: 'pipe' }); } catch { /* none running */ }
94
+ }
95
+
96
+ // Removes `dir` from the user PATH, keeping its registry type (REG_EXPAND_SZ stays expandable).
97
+ function removeFromUserPath(dir) {
98
+ const ps = `$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true); if ($k -and $null -ne $k.GetValue('Path')) { $kind = $k.GetValueKind('Path'); $old = $k.GetValue('Path', '', 'DoNotExpandEnvironmentNames'); $new = ($old -split ';' | Where-Object { $_ -and $_.TrimEnd('\\') -ne ${`'${dir.replace(/'/g, "''")}'`} }) -join ';'; if ($new -ne $old) { $k.SetValue('Path', $new, $kind) } }`;
99
+ try { execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(ps, 'utf16le').toString('base64')], { stdio: 'pipe' }); } catch { /* PATH left as is */ }
73
100
  }
74
101
 
75
102
  // Start through the scheduled task itself — same as at logon, and not a child of this console
@@ -83,13 +110,20 @@ async function main() {
83
110
  const o = parse(process.argv.slice(2));
84
111
  const cmd = o._[0];
85
112
  if (cmd === 'setup') {
86
- const prev = readConfig() || {};
87
113
  if (o.token === true) throw new Error('--token needs the device key from your Bridgerator account');
114
+ stopRunning(); // the old bridge must not hold the engine files / the link while we reconfigure
115
+ const prev = readConfig() || {};
88
116
  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.');
117
+ // --pair: get a new key through the browser even if one is configured. Without it, a stored key
118
+ // the relay rejects (computer removed in the account) is replaced the same way; offline = keep it.
119
+ let paired = !o.token && (o.pair || !prev.token);
120
+ if (!o.token && !paired && (await checkKey(siteOf(relay), prev.token)) === 'rejected') {
121
+ console.log("This computer was removed from your account — let's connect it again.");
122
+ paired = true;
123
+ }
124
+ const linked = paired ? await pair(siteOf(relay), os.hostname()) : null;
125
+ const token = o.token || (linked ? linked.token : prev.token);
126
+ if (linked) console.log(`Confirmed — this computer is linked to ${linked.email ? `the account ${linked.email}` : 'your account'}.`);
93
127
  const cfg = {
94
128
  relay,
95
129
  token,
@@ -105,7 +139,6 @@ async function main() {
105
139
  console.log(`allowed: ${cfg.allowedDirectories.join(', ')}`);
106
140
  console.log(`excluded: ${cfg.deniedDirectories.join(', ') || '-'}`);
107
141
  console.log(installAutostart() ? `autostart: scheduled task "${TASK}" (at logon, hidden)` : '');
108
- stopRunning();
109
142
  startHidden();
110
143
  console.log(`started in background — log: ${LOG}`);
111
144
  console.log('Next: in ChatGPT add the Bridgerator app and sign in with the same account.');
@@ -113,7 +146,7 @@ async function main() {
113
146
  }
114
147
  if (cmd === 'run') {
115
148
  const cfg = readConfig();
116
- if (!cfg) throw new Error(`no config at ${CONFIG} — run: bridgerator setup`);
149
+ if (!cfg) throw new Error(`no config at ${CONFIG} — run the install command from ${ACCOUNT_URL}`);
117
150
  const dc = await startDesktopCommander({ ...cfg, dcConfigDir: path.join(DIR, 'dc') }, log);
118
151
  connect(cfg, dc, log);
119
152
  return;
@@ -126,12 +159,32 @@ async function main() {
126
159
  return;
127
160
  }
128
161
  if (cmd === 'uninstall') {
162
+ // uninstall --all: everything (THE uninstall for customers); --keep-config: keep key + folder settings.
163
+ if (process.platform === 'win32') try { execFileSync('schtasks', ['/Delete', '/F', '/TN', TASK], { stdio: 'pipe' }); } catch { /* not installed */ }
129
164
  stopRunning();
130
- try { execFileSync('schtasks', ['/Delete', '/F', '/TN', TASK], { stdio: 'pipe' }); } catch { /* not installed */ }
131
- console.log(`stopped and autostart removed. Config kept at ${CONFIG}. Remove the package: npm rm -g bridgerator`);
165
+ console.log('Bridgerator stopped, autostart removed.');
166
+ if (!o['keep-config']) { fs.rmSync(DIR, { recursive: true, force: true, maxRetries: 5 }); console.log(`Removed ${DIR} (device key, settings, history).`); }
167
+ const ownNode = fs.existsSync(path.join(NODE_DIR, 'node.exe'));
168
+ if (o.all) {
169
+ console.log('Removing the program files...');
170
+ // The npm next to the node.exe running us is the one whose global folder holds this package.
171
+ const npm = path.join(path.dirname(process.execPath), 'npm.cmd');
172
+ try { execFileSync('cmd', ['/d', '/c', fs.existsSync(npm) ? npm : 'npm.cmd', 'rm', '-g', 'bridgerator', '--loglevel=error'], { stdio: 'inherit' }); } catch { console.log('Could not remove the npm package — run: npm.cmd rm -g bridgerator'); }
173
+ if (ownNode) {
174
+ removeFromUserPath(NODE_DIR);
175
+ // This very process may run on that node.exe: delete the folder a few seconds after we exit.
176
+ spawn('cmd', ['/d', '/c', `ping -n 4 127.0.0.1 >nul & rmdir /s /q "${NODE_DIR}" & rmdir "${path.dirname(NODE_DIR)}"`], { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true }).unref();
177
+ console.log(`Removed the Node.js that Bridgerator installed (${NODE_DIR}).`);
178
+ }
179
+ } else {
180
+ console.log('To remove the program files: npm.cmd rm -g bridgerator');
181
+ if (ownNode) console.log(`and the Node.js that Bridgerator installed: Remove-Item -Recurse -Force '${NODE_DIR}'`);
182
+ console.log('(or remove everything at once: bridgerator.cmd uninstall --all)');
183
+ }
184
+ console.log(`Also remove this computer in your account: ${ACCOUNT_URL}`);
132
185
  return;
133
186
  }
134
- console.log('usage: bridgerator setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... | run | status | uninstall');
187
+ console.log('usage: bridgerator.cmd setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... | run | status | uninstall [--all | --keep-config]');
135
188
  }
136
189
 
137
190
  main().catch((e) => { console.error(`[bridgerator] ${e?.message || e}`); process.exit(1); });
package/src/notice.mjs CHANGED
@@ -13,6 +13,7 @@ export function noticeText(n) {
13
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
14
  expired: ['Bridgerator Pro has ended', "You're on Free now (10,000 tool calls a month). Renew Pro in your account."],
15
15
  expiring: [`Bridgerator Pro ends on ${day(n.until)}`, 'Pay for 30 more days in your account to keep unlimited calls.'],
16
+ unlinked: ['Bridgerator: this computer is no longer linked', 'ChatGPT can no longer reach this PC. Run the install command from your account page again.'],
16
17
  }[n.kind];
17
18
  return t && { title: t[0], body: t[1], button: 'Open account' };
18
19
  }
@@ -22,11 +23,12 @@ export function toastXml({ title, body, button }) {
22
23
  + `<actions><action content="${xml(button)}" activationType="protocol" arguments="${ACCOUNT_URL}"/></actions></toast>`;
23
24
  }
24
25
 
25
- export function showNotice(n, log = console.log) {
26
+ // `done` runs once the notification is shown (or could not be): lets the caller exit after it.
27
+ export function showNotice(n, log = console.log, done = () => {}) {
26
28
  const t = noticeText(n);
27
- if (!t) return;
29
+ if (!t) return done();
28
30
  log(`[bridgerator] notice: ${t.title} — ${t.body} ${ACCOUNT_URL}`);
29
- if (process.platform !== 'win32') return;
31
+ if (process.platform !== 'win32') return done();
30
32
  // Own app id under HKCU (no admin): the notification header reads "Bridgerator", not "Windows PowerShell".
31
33
  const ps = `$k = 'HKCU:\\Software\\Classes\\AppUserModelId\\Bridgerator'
32
34
  if (-not (Test-Path $k)) { New-Item $k -Force | Out-Null; New-ItemProperty $k -Name DisplayName -Value 'Bridgerator' -Force | Out-Null }
@@ -35,5 +37,5 @@ if (-not (Test-Path $k)) { New-Item $k -Force | Out-Null; New-ItemProperty $k -N
35
37
  $x = New-Object Windows.Data.Xml.Dom.XmlDocument
36
38
  $x.LoadXml('${toastXml(t).replace(/'/g, "''")}')
37
39
  [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}`); });
40
+ execFile('powershell', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(ps, 'utf16le').toString('base64')], { windowsHide: true }, (e) => { if (e) log(`[bridgerator] notification failed: ${e.message}`); done(); });
39
41
  }
package/src/pair.mjs CHANGED
@@ -17,7 +17,7 @@ export function openBrowser(url) {
17
17
  } catch { /* no browser: the URL is printed */ }
18
18
  }
19
19
 
20
- /** → { token, name } once the owner confirmed; throws when the code expires. */
20
+ /** → { token, name, email? } once the owner confirmed; throws when the code expires. */
21
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
22
  const post = (path, body) => f(`${site}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(20000) });
23
23
  const r = await post('/pair/start', { name });
@@ -36,8 +36,20 @@ export async function pair(site, name, { fetch: f = globalThis.fetch, open = ope
36
36
  await sleep(everyMs);
37
37
  let p;
38
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 }; }
39
+ if (p.status === 200) { const j = await p.json(); if (j.token) return { token: j.token, name: j.name || name, ...(typeof j.email === 'string' ? { email: j.email } : {}) }; }
40
40
  if (p.status === 410) break;
41
+ if (p.status === 404) throw new Error('this pairing code is no longer valid — run the install command again');
42
+ if (p.status === 429) throw new Error('too many attempts — wait a few minutes, then run the install command again');
41
43
  }
42
44
  throw new Error('the code expired before it was confirmed — run the command again');
43
45
  }
46
+
47
+ /** Is the stored device key still accepted? → 'ok' | 'rejected' (computer removed in the account) | 'unknown' (network). */
48
+ export async function checkKey(site, token, { fetch: f = globalThis.fetch } = {}) {
49
+ try {
50
+ const r = await f(`${site}/pair/check`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token }), signal: AbortSignal.timeout(15000) });
51
+ if (r.status === 401) return 'rejected';
52
+ if (r.status === 200 && (await r.json().catch(() => ({}))).ok === true) return 'ok';
53
+ } catch { /* offline: keep the key */ }
54
+ return 'unknown';
55
+ }
package/src/policy.mjs CHANGED
@@ -107,3 +107,18 @@ export function debrand(v) {
107
107
  }
108
108
  return v;
109
109
  }
110
+
111
+ // Two results are the engine's own text, not user data: the prompt catalog (get_prompts says
112
+ // "Desktop Commander Prompt Categories") and get_config, whose `version` is the engine's and is
113
+ // shown as "Bridgerator version" — replaced by the bridge's own version.
114
+ export function debrandResult(name, result, version) {
115
+ if (!result || typeof result !== 'object') return result;
116
+ if (name === 'get_prompts') return debrand(result);
117
+ const sc = result.structuredContent, engine = sc?.config?.version;
118
+ if (name !== 'get_config' || typeof engine !== 'string') return result;
119
+ const label = `"version": ${JSON.stringify(engine)}`;
120
+ const content = Array.isArray(result.content)
121
+ ? result.content.map((c) => (c?.type === 'text' && typeof c.text === 'string' ? { ...c, text: c.text.replace(label, `"version": ${JSON.stringify(version)}`) } : c))
122
+ : result.content;
123
+ return { ...result, content, structuredContent: { ...sc, config: { ...sc.config, version } } };
124
+ }