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 +5 -4
- package/install.ps1 +29 -6
- package/package.json +1 -1
- package/src/bridge.mjs +30 -7
- package/src/cli.mjs +72 -19
- package/src/notice.mjs +6 -4
- package/src/pair.mjs +14 -2
- package/src/policy.mjs +15 -0
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
|
|
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
|
|
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
|
-
$
|
|
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 =
|
|
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
|
-
|
|
52
|
-
|
|
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) {
|
|
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.
|
|
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:
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
+
}
|