bridgerator 0.0.0-stage → 1.0.5

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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bridgerator contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Bundled dependency: Desktop Commander (https://github.com/wonderwhy-er/DesktopCommanderMCP),
24
+ MIT License, Copyright (c) 2024-2025 Eduard Ruzga and Desktop Commander Contributors.
package/README.md CHANGED
@@ -1,3 +1,107 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Bridgerator
2
+
3
+ Connect your Windows PC to ChatGPT. The assistant can read and edit files, search folders and run
4
+ PowerShell commands on **your** computer — through a bridge that only makes an **outbound**
5
+ connection. No inbound ports, no remote-desktop software.
6
+
7
+ ```
8
+ ChatGPT ──OAuth 2.1 + MCP (HTTPS)──► relay (mcp.bridgerator.recondeck.com) ──WebSocket──► bridgerator on your PC ──stdio──► bundled MCP engine
9
+ ```
10
+
11
+ This repository is the part that runs on your PC. It is small on purpose so you (or an AI agent
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).
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.
24
+
25
+ ## Plans
26
+
27
+ Free: 15,000 tool calls per month per account, any number of PCs. Pro: $10/month, unlimited.
28
+ Details: https://bridgerator.recondeck.com/pricing
29
+
30
+ ## Install (Windows)
31
+
32
+ 1. Create an account and a device key at https://bridgerator.recondeck.com.
33
+ 2. In PowerShell (no admin rights needed):
34
+ ```powershell
35
+ & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1))) -Token <device key>
36
+ ```
37
+ It downloads portable Node.js LTS if missing (checksum-verified, no admin), installs this package and runs `bridgerator setup`.
38
+ Update later with the same line without `-Token` (the key and folder settings are kept).
39
+ Prefer to read first? Download `install.ps1`, read it, run it with `-Token`.
40
+ 3. In ChatGPT: Settings → Apps & Connectors → Advanced → Developer mode → Create → URL
41
+ `https://mcp.bridgerator.recondeck.com/mcp`, authentication OAuth, sign in with your account.
42
+
43
+ Manual install (PowerShell): `$env:PUPPETEER_SKIP_DOWNLOAD=1; npm install -g bridgerator`, then `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.
44
+
45
+ ## Access rules — what the assistant can and cannot touch
46
+
47
+ Config: `%APPDATA%\Bridgerator\config.json` (written by `setup`, edit and restart to change).
48
+
49
+ | Key | Meaning | Default |
50
+ |---|---|---|
51
+ | `allowedDirectories` | File tools work only inside these folders (enforced by the engine). Empty = whole disk. | your user folder |
52
+ | `deniedDirectories` | Folders excluded **inside** the allowed ones (enforced by Bridgerator, see below). | `~\.ssh`, `~\AppData` |
53
+ | `blockedCommands` | Commands the engine refuses to run. Leave it out to keep the built-in list. | built-in list (`format`, `diskpart`, `reg`, `net`, `sc`, `runas`, `netsh`, `takeown`, …) |
54
+ | `disabledTools` | Tools never shown to the assistant, e.g. `["start_process","interact_with_process"]` to turn the terminal off. | none |
55
+
56
+ Full file, search and terminal toolset, including ChatGPT widgets (file preview,
57
+ config view). Hidden: `set_config_value` (would let the assistant change its own limits), feedback and
58
+ usage-stats tools.
59
+
60
+ How exclusions work (`src/policy.mjs`): every path argument is resolved (symlinks and junctions
61
+ included) and refused if it lies inside an excluded folder; result lines that mention an excluded
62
+ path (search hits, recursive listings) are removed; `set_config_value` is never exposed, so the
63
+ assistant cannot change these limits.
64
+
65
+ ⚠ **The terminal is not a folder sandbox.** A shell command runs with your Windows user's rights
66
+ and can read anything you can. Bridgerator refuses commands that literally name an excluded folder,
67
+ but that is a courtesy, not a boundary. If folder limits must be strict, add the terminal tools to
68
+ `disabledTools`.
69
+
70
+ The bridge runs as **your** Windows user (hidden scheduled task at logon), never as SYSTEM.
71
+
72
+ Bridgerator gives its bundled engine its **own** config folder (`%APPDATA%\Bridgerator\dc`, see `src/dc-isolate.mjs`): if you already run the same open-source MCP server for Claude or another app, its settings and folder limits are not touched. The engine keeps a local log of recent tool calls in that folder; it never leaves your PC.
73
+
74
+ ## Commands
75
+
76
+ ```
77
+ bridgerator setup --token <key> [--allow <dir>]... [--deny <dir>]... # config + autostart + start
78
+ bridgerator run # foreground (what autostart runs)
79
+ bridgerator status # config (token masked) + log tail
80
+ bridgerator uninstall # stop + remove autostart
81
+ ```
82
+ Log: `%APPDATA%\Bridgerator\bridge.log`. Stopping the bridge cuts access immediately.
83
+
84
+ ## Privacy
85
+
86
+ File contents and command output go to ChatGPT (OpenAI) as tool results; the relay forwards them
87
+ in memory and does not store them. Engine telemetry, remote feature flags and any browser pop-ups are turned off by the bridge.
88
+ Policy: https://bridgerator.recondeck.com/privacy
89
+
90
+ ## Development
91
+
92
+ `npm test` — policy unit tests plus an end-to-end run against the real bundled engine
93
+ (throw-away folders; asserts that an existing config of the same engine stays byte-identical).
94
+
95
+ ## License
96
+
97
+ MIT. The bundled open-source MCP engine is MIT-licensed as well; its copyright notice is kept in
98
+ `LICENSE` and the dependency is listed in `package.json`.
99
+
100
+ ---
101
+
102
+ ### По-русски коротко
103
+ Bridgerator подключает ваш ПК к ChatGPT: файлы, поиск, терминал. Мост только исходящий, входящих
104
+ портов нет. Установка — одна строка PowerShell с ключом устройства из кабинета (см. выше).
105
+ Разрешённые папки — `allowedDirectories`, исключения внутри них — `deniedDirectories`
106
+ (исключения обеспечивает сам Bridgerator). Терминал папками не
107
+ ограничивается — для строгих границ выключите его через `disabledTools`.
package/install.ps1 ADDED
@@ -0,0 +1,81 @@
1
+ # Bridgerator installer for Windows (PowerShell 5.1+). Source: npm package "bridgerator" (https://www.npmjs.com/package/bridgerator)
2
+ # Usage (one line, from your Bridgerator account page):
3
+ # & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1))) -Token <device key>
4
+ # Update an installed bridge (keeps its key and folders): the same line without -Token.
5
+ # What it does: installs Node.js LTS if missing (portable zip, no admin), installs the bridgerator npm package,
6
+ # then runs `bridgerator setup` (config + hidden autostart at logon for THIS user, no admin rights),
7
+ # then opens the welcome page in the default browser (skip with -NoBrowser).
8
+ param(
9
+ [string]$Token,
10
+ [string]$Package = 'https://bridgerator.recondeck.com/bridgerator.tgz',
11
+ [string[]]$Allow,
12
+ [string[]]$Deny,
13
+ [switch]$NoBrowser
14
+ )
15
+ $ErrorActionPreference = 'Stop'
16
+ if (-not $Token -and -not (Test-Path (Join-Path $env:APPDATA 'Bridgerator\config.json'))) { throw 'Add -Token <device key> from your Bridgerator account page (https://bridgerator.recondeck.com/app).' }
17
+
18
+ function Refresh-Path {
19
+ $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
20
+ }
21
+
22
+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
23
+ $ProgressPreference = 'SilentlyContinue'
24
+
25
+ # No winget on many clean Windows installs: take the official Node.js LTS zip into the user's
26
+ # profile (no admin rights), checked against nodejs.org SHASUMS256, and put it on the user PATH.
27
+ function Install-PortableNode {
28
+ $dir = Join-Path $env:LOCALAPPDATA 'Bridgerator\node'
29
+ $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
30
+ $rel = (Invoke-RestMethod 'https://nodejs.org/dist/index.json') | Where-Object { $_.lts -and ($_.files -contains "win-$arch-zip") } | Select-Object -First 1
31
+ if (-not $rel) { throw 'Could not find a Node.js LTS build. Install Node.js 20+ from https://nodejs.org and run this again.' }
32
+ $name = "node-$($rel.version)-win-$arch"
33
+ Write-Host "Node.js not found - downloading Node.js $($rel.version) into $dir ..."
34
+ $tmp = Join-Path $env:TEMP "bridgerator-$([guid]::NewGuid().ToString('N'))"
35
+ New-Item -ItemType Directory -Path $tmp | Out-Null
36
+ try {
37
+ $zip = Join-Path $tmp "$name.zip"
38
+ Invoke-WebRequest "https://nodejs.org/dist/$($rel.version)/$name.zip" -OutFile $zip -UseBasicParsing
39
+ $sums = (Invoke-WebRequest "https://nodejs.org/dist/$($rel.version)/SHASUMS256.txt" -UseBasicParsing).Content
40
+ $want = ($sums -split "`n" | Where-Object { $_ -match "\s$name\.zip$" }) -replace '\s.*$', ''
41
+ $got = (Get-FileHash $zip -Algorithm SHA256).Hash
42
+ if (-not $want -or $got -ne $want.Trim().ToUpper()) { throw 'Node.js download failed the checksum check.' }
43
+ Write-Host 'Node.js checksum OK (SHA256, nodejs.org)'
44
+ Expand-Archive $zip -DestinationPath $tmp -Force
45
+ if (Test-Path $dir) { Remove-Item $dir -Recurse -Force }
46
+ New-Item -ItemType Directory -Path (Split-Path $dir) -Force | Out-Null
47
+ Move-Item (Join-Path $tmp $name) $dir
48
+ } finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
49
+ $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
50
+ if (($userPath -split ';') -notcontains $dir) { [Environment]::SetEnvironmentVariable('Path', (@($dir, $userPath) | Where-Object { $_ }) -join ';', 'User') }
51
+ Refresh-Path
52
+ }
53
+
54
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Install-PortableNode }
55
+ $major = [int]((& node -v).TrimStart('v').Split('.')[0])
56
+ if ($major -lt 20) { throw "Node.js 20+ required, found $(& node -v). Update from https://nodejs.org" }
57
+
58
+ # The engine's PDF export brings puppeteer, whose postinstall downloads Chrome (~700 MB);
59
+ # skipped here, PDF export fetches a browser itself the first time it is used.
60
+ $env:PUPPETEER_SKIP_DOWNLOAD = '1'
61
+ Write-Host "Installing bridgerator from $Package ..."
62
+ & npm install -g $Package --no-audit --no-fund --loglevel=error
63
+ if ($LASTEXITCODE -ne 0) { throw 'npm install failed' }
64
+ Refresh-Path
65
+
66
+ $setupArgs = @('setup')
67
+ if ($Token) { $setupArgs += @('--token', $Token) }
68
+ foreach ($a in $Allow) { $setupArgs += @('--allow', $a) }
69
+ foreach ($d in $Deny) { $setupArgs += @('--deny', $d) }
70
+ & bridgerator @setupArgs
71
+ if ($LASTEXITCODE -ne 0) { throw 'bridgerator setup failed' }
72
+ Write-Host 'Done. Check with: bridgerator status'
73
+
74
+ # Next steps (connect ChatGPT, example prompts) on the welcome page; skipped for -NoBrowser and
75
+ # non-interactive sessions (scheduled tasks, remote shells). Never fails the install.
76
+ $welcome = 'https://bridgerator.recondeck.com/welcome/'
77
+ if (-not $NoBrowser -and [Environment]::UserInteractive) {
78
+ try { Start-Process $welcome } catch { Write-Host "Next steps: $welcome" }
79
+ } else {
80
+ Write-Host "Next steps: $welcome"
81
+ }
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
- {
2
- "name": "bridgerator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "bridgerator",
3
+ "version": "1.0.5",
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 ADDED
@@ -0,0 +1,96 @@
1
+ // Bridgerator bridge: runs on the user's PC.
2
+ // - starts the bundled open-source Desktop Commander MCP server as a child over stdio;
3
+ // - pushes this PC's limits (allowedDirectories, blockedCommands) into it at startup;
4
+ // - opens ONE outbound WebSocket to the relay (no inbound port is opened on this PC);
5
+ // - forwards each relay `call` through the policy (excluded folders, disabled tools) to Desktop
6
+ // Commander and sends the filtered result back.
7
+ import { createRequire } from 'node:module';
8
+ import WebSocket from 'ws';
9
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
10
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
11
+ import { createPolicy, debrand } from './policy.mjs';
12
+ import { showNotice } from './notice.mjs';
13
+
14
+ const RECONNECT_MS = 5000;
15
+ const DC_ENTRY = createRequire(import.meta.url).resolve('@wonderwhy-er/desktop-commander/dist/index.js');
16
+ const ISOLATE = new URL('./dc-isolate.mjs', import.meta.url).href;
17
+
18
+ export async function startDesktopCommander(cfg, log = console.log) {
19
+ if (!cfg.dcConfigDir) throw new Error('dcConfigDir missing');
20
+ const transport = new StdioClientTransport({
21
+ command: process.execPath,
22
+ // Own Desktop Commander config (dc-isolate.mjs): a DC the user runs elsewhere keeps its limits.
23
+ args: ['--import', ISOLATE, DC_ENTRY],
24
+ // No calls home: telemetry kill-switch, and remote feature flags (onboarding page, promo text
25
+ // injected into tool results, A/B tests) replaced by an empty local set.
26
+ 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
+ stderr: 'ignore',
28
+ });
29
+ const client = new Client({ name: 'bridgerator', version: '1.0.0' }, { capabilities: {} });
30
+ await client.connect(transport);
31
+ // The PC owner's limits go into Desktop Commander's own config; the model can't change them
32
+ // (set_config_value is never exposed to the relay, see policy.ALWAYS_HIDDEN).
33
+ const set = (key, value) => client.callTool({ name: 'set_config_value', arguments: { key, value } });
34
+ await set('allowedDirectories', cfg.allowedDirectories || []);
35
+ if (Array.isArray(cfg.blockedCommands)) await set('blockedCommands', cfg.blockedCommands);
36
+ await set('telemetryEnabled', false);
37
+ const policy = createPolicy(cfg);
38
+ const { tools } = await client.listTools();
39
+ const visible = tools.filter(policy.visible).map(debrand);
40
+ // MCP Apps widgets (file preview, config view) — served to ChatGPT through the relay as-is.
41
+ const resources = ((await client.listResources().catch(() => ({ resources: [] }))).resources || []).map(debrand);
42
+ const resourceUris = new Set(resources.map((r) => r.uri));
43
+ log(`[bridgerator] engine up: ${visible.length} tools; allowed=${JSON.stringify(cfg.allowedDirectories || [])} excluded=${JSON.stringify(cfg.deniedDirectories || [])}`);
44
+ if (!(cfg.allowedDirectories || []).length) log('[bridgerator] WARNING: allowedDirectories is empty — file tools can reach the whole disk');
45
+
46
+ async function call(name, args) {
47
+ const refusal = policy.check(name, args);
48
+ if (refusal) return { content: [{ type: 'text', text: refusal }], isError: true };
49
+ 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);
52
+ } catch (e) {
53
+ return { content: [{ type: 'text', text: `bridge error: ${e?.message || e}` }], isError: true };
54
+ }
55
+ }
56
+ async function readResource(uri) {
57
+ if (!resourceUris.has(uri)) return { error: 'unknown resource' };
58
+ try { return { result: debrand(await client.readResource({ uri })) }; } catch (e) { return { error: String(e?.message || e) }; }
59
+ }
60
+ return { client, tools: visible, resources, call, readResource, close: () => client.close() };
61
+ }
62
+
63
+ export function connect(cfg, dc, log = console.log) {
64
+ const ws = new WebSocket(cfg.relay, { handshakeTimeout: 15000 });
65
+ let alive = false;
66
+ ws.on('open', () => {
67
+ alive = true;
68
+ ws.send(JSON.stringify({ type: 'hello', token: cfg.token, tools: dc.tools, resources: dc.resources }));
69
+ log('[bridgerator] linked to relay, waiting for calls');
70
+ });
71
+ ws.on('message', async (raw) => {
72
+ let msg; try { msg = JSON.parse(raw); } catch { return; }
73
+ if (msg.type === 'welcome') { log(`[bridgerator] registered as "${msg.device}"`); return; }
74
+ if (msg.type === 'notice') { showNotice(msg, log); return; }
75
+ if (msg.type === 'read_resource' && msg.callId) {
76
+ const r = await dc.readResource(msg.uri);
77
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'result', callId: msg.callId, ...r }));
78
+ return;
79
+ }
80
+ if (msg.type !== 'call' || !msg.callId) return;
81
+ const result = await dc.call(msg.name, msg.args || {});
82
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'result', callId: msg.callId, result }));
83
+ });
84
+ const die = (why) => {
85
+ if (ws._down) return;
86
+ ws._down = true;
87
+ log(`[bridgerator] disconnected (${why}); reconnecting in ${RECONNECT_MS / 1000}s`);
88
+ setTimeout(() => connect(cfg, dc, log), RECONNECT_MS);
89
+ };
90
+ ws.on('close', (code) => {
91
+ if (code === 4003) { log('[bridgerator] relay rejected the device token — run `bridgerator setup --token <new token>`'); process.exit(2); }
92
+ die(`close ${code}`);
93
+ });
94
+ ws.on('error', (e) => { if (!alive) die(`connect: ${e.message}`); });
95
+ return ws;
96
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ // bridgerator setup --token <t> [--relay <wss url>] [--allow <dir>]... [--deny <dir>]... → config + autostart + start
3
+ // bridgerator run | status | uninstall
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { spawn, execFileSync } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { startDesktopCommander, connect } from './bridge.mjs';
10
+
11
+ const DEFAULT_RELAY = 'wss://mcp.bridgerator.recondeck.com/link';
12
+ const TASK = 'Bridgerator';
13
+ const home = os.homedir();
14
+ const DIR = process.env.BRIDGERATOR_HOME || path.join(process.env.APPDATA || path.join(home, '.config'), 'Bridgerator');
15
+ const CONFIG = path.join(DIR, 'config.json');
16
+ const LOG = path.join(DIR, 'bridge.log');
17
+ const SELF = fileURLToPath(import.meta.url);
18
+
19
+ function parse(argv) {
20
+ const o = { _: [] };
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const a = argv[i];
23
+ if (!a.startsWith('--')) { o._.push(a); continue; }
24
+ const k = a.slice(2), v = argv[i + 1];
25
+ if (v === undefined || v.startsWith('--')) { o[k] = true; continue; }
26
+ i++;
27
+ if (k === 'allow' || k === 'deny') (o[k] ||= []).push(v); else o[k] = v;
28
+ }
29
+ return o;
30
+ }
31
+
32
+ function readConfig() {
33
+ try { return JSON.parse(fs.readFileSync(CONFIG, 'utf8')); } catch { return null; }
34
+ }
35
+
36
+ function log(line) {
37
+ const s = `${new Date().toISOString()} ${line}`;
38
+ console.log(s);
39
+ try {
40
+ if (fs.existsSync(LOG) && fs.statSync(LOG).size > 5e6) fs.renameSync(LOG, `${LOG}.1`);
41
+ fs.appendFileSync(LOG, s + '\n');
42
+ } catch { /* log dir not writable — console only */ }
43
+ }
44
+
45
+ // Hidden autostart at logon of THIS user (not SYSTEM): the assistant gets the user's rights only.
46
+ function installAutostart() {
47
+ if (process.platform !== 'win32') return 'autostart: not Windows — start `bridgerator run` yourself';
48
+ const vbs = path.join(DIR, 'run-hidden.vbs');
49
+ const cmd = `"${process.execPath}" "${SELF}" run`;
50
+ fs.writeFileSync(vbs, `CreateObject("WScript.Shell").Run "${cmd.replace(/"/g, '""')}", 0, False\r\n`);
51
+ // Registered through PowerShell's ScheduledTasks module: `schtasks /Create … wscript.exe …` spawned
52
+ // from node is refused (EPERM) on clean Windows 11. No 72 h execution limit (the task default
53
+ // would silently stop the bridge after three days); restart on failure.
54
+ const q = (s) => `'${String(s).replace(/'/g, "''")}'`;
55
+ const ps = [
56
+ `$a = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument ${q(`"${vbs}"`)}`,
57
+ '$t = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\\$env:USERNAME"',
58
+ '$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew',
59
+ '$p = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\\$env:USERNAME" -LogonType Interactive -RunLevel Limited',
60
+ `Register-ScheduledTask -TaskName ${q(TASK)} -Action $a -Trigger $t -Settings $s -Principal $p -Force | Out-Null`,
61
+ ].join('; ');
62
+ execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(ps, 'utf16le').toString('base64')], { stdio: 'pipe' });
63
+ return vbs;
64
+ }
65
+
66
+ function stopRunning() {
67
+ if (process.platform !== 'win32') return;
68
+ // Only our own bridge processes: node.exe whose command line runs this cli with `run`.
69
+ 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 }`;
70
+ try { execFileSync('powershell', ['-NoProfile', '-Command', ps], { stdio: 'pipe' }); } catch { /* none running */ }
71
+ }
72
+
73
+ // Start through the scheduled task itself — same as at logon, and not a child of this console
74
+ // (closing the installer window or a job object around it must not kill the bridge).
75
+ function startHidden() {
76
+ if (process.platform === 'win32') execFileSync('schtasks', ['/Run', '/TN', TASK], { stdio: 'pipe' });
77
+ else spawn(process.execPath, [SELF, 'run'], { detached: true, stdio: 'ignore' }).unref();
78
+ }
79
+
80
+ async function main() {
81
+ const o = parse(process.argv.slice(2));
82
+ const cmd = o._[0];
83
+ if (cmd === 'setup') {
84
+ 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
+ const cfg = {
88
+ relay: o.relay || prev.relay || DEFAULT_RELAY,
89
+ token,
90
+ allowedDirectories: o.allow || prev.allowedDirectories || [home],
91
+ deniedDirectories: o.deny || prev.deniedDirectories || [path.join(home, '.ssh'), path.join(home, 'AppData')],
92
+ // blockedCommands: absent = Desktop Commander's own default list (format, diskpart, reg, net, sc, runas, …).
93
+ ...(prev.blockedCommands ? { blockedCommands: prev.blockedCommands } : {}),
94
+ disabledTools: prev.disabledTools || [],
95
+ };
96
+ fs.mkdirSync(DIR, { recursive: true });
97
+ fs.writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
98
+ console.log(`config: ${CONFIG}`);
99
+ console.log(`allowed: ${cfg.allowedDirectories.join(', ')}`);
100
+ console.log(`excluded: ${cfg.deniedDirectories.join(', ') || '-'}`);
101
+ console.log(installAutostart() ? `autostart: scheduled task "${TASK}" (at logon, hidden)` : '');
102
+ stopRunning();
103
+ startHidden();
104
+ 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.');
106
+ return;
107
+ }
108
+ if (cmd === 'run') {
109
+ const cfg = readConfig();
110
+ if (!cfg) throw new Error(`no config at ${CONFIG} — run: bridgerator setup --token <device key>`);
111
+ const dc = await startDesktopCommander({ ...cfg, dcConfigDir: path.join(DIR, 'dc') }, log);
112
+ connect(cfg, dc, log);
113
+ return;
114
+ }
115
+ if (cmd === 'status') {
116
+ const cfg = readConfig();
117
+ if (!cfg) { console.log(`not configured (${CONFIG})`); return; }
118
+ console.log(JSON.stringify({ ...cfg, token: `${String(cfg.token).slice(0, 4)}…` }, null, 2));
119
+ try { console.log('--- log tail ---\n' + fs.readFileSync(LOG, 'utf8').split('\n').slice(-8).join('\n')); } catch { console.log('no log yet'); }
120
+ return;
121
+ }
122
+ if (cmd === 'uninstall') {
123
+ stopRunning();
124
+ try { execFileSync('schtasks', ['/Delete', '/F', '/TN', TASK], { stdio: 'pipe' }); } catch { /* not installed */ }
125
+ console.log(`stopped and autostart removed. Config kept at ${CONFIG}. Remove the package: npm rm -g bridgerator`);
126
+ return;
127
+ }
128
+ console.log('usage: bridgerator setup --token <key> [--allow <dir>]... [--deny <dir>]... | run | status | uninstall');
129
+ }
130
+
131
+ main().catch((e) => { console.error(`[bridgerator] ${e?.message || e}`); process.exit(1); });
@@ -0,0 +1,32 @@
1
+ // Module-loader hooks (loaded into the bundled engine child via `node --import ./dc-isolate.mjs`).
2
+ // The engine is pinned (package.json); each patch replaces one exact snippet and FAILS CLOSED —
3
+ // if the snippet is missing (engine changed), the child refuses to start instead of silently
4
+ // sharing the user's own config, opening a browser or downloading a browser.
5
+ // config.js — config folder → BRIDGERATOR_DC_CONFIG_DIR (an engine the user runs for other
6
+ // apps keeps its limits; home-folder expansion and the shell env stay real).
7
+ // toolHistory.js — the local tool-call history goes to the same folder, not ~\.claude-server-commander.
8
+ // open-browser.js — no-ops: the bridge runs hidden and never opens a browser (the engine opens
9
+ // its vendor's welcome page on first run).
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.
13
+ const DIR_EXPR = "(process.env.BRIDGERATOR_DC_CONFIG_DIR || path.join(USER_HOME, '.claude-server-commander'))";
14
+ const PATCHES = [
15
+ { file: /[\\/]dist[\\/]config\.js$/, from: "const CONFIG_DIR = path.join(USER_HOME, '.claude-server-commander');", to: `const CONFIG_DIR = ${DIR_EXPR};` },
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');" },
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]," },
19
+ { file: /[\\/]dist[\\/]index\.js$/, from: ' ensureChromeAvailable();\n', to: ' // bridgerator: no Chrome pre-download\n' },
20
+ ];
21
+ const ENGINE = /[\\/]@wonderwhy-er[\\/]desktop-commander[\\/]dist[\\/]/;
22
+
23
+ export async function load(url, context, nextLoad) {
24
+ const r = await nextLoad(url, context);
25
+ const p = url.startsWith('file:') ? decodeURIComponent(new URL(url).pathname) : '';
26
+ if (!ENGINE.test(p)) return r;
27
+ const patch = PATCHES.find((x) => x.file.test(p));
28
+ if (!patch) return r;
29
+ const src = String(r.source).replace(/\r\n/g, '\n');
30
+ if (!src.includes(patch.from)) throw new Error(`bridgerator: unexpected engine file ${p.split(/[\\/]/).slice(-2).join('/')} — refusing to start`);
31
+ return { ...r, source: patch.replaceAll ?? src.replace(patch.from, patch.to) };
32
+ }
@@ -0,0 +1,5 @@
1
+ // Preload for the Desktop Commander child: registers dc-config-hooks.mjs (see there).
2
+ import { register } from 'node:module';
3
+
4
+ if (!process.env.BRIDGERATOR_DC_CONFIG_DIR) throw new Error('bridgerator: BRIDGERATOR_DC_CONFIG_DIR not set');
5
+ register('./dc-config-hooks.mjs', import.meta.url);
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 (15,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/policy.mjs ADDED
@@ -0,0 +1,109 @@
1
+ // Bridge-side access policy, applied BEFORE a call reaches Desktop Commander and to its results.
2
+ // Desktop Commander itself only knows allowedDirectories; it has no "except these folders" rule,
3
+ // so deniedDirectories is enforced here: input paths are resolved (symlinks/junctions included)
4
+ // and refused when they fall inside a denied folder; result lines that mention a denied path are
5
+ // dropped (search hits, recursive listings).
6
+ // The terminal is NOT covered by folder rules (a shell can read anything the user can); commands
7
+ // that name a denied folder are refused as a courtesy, the real control is disabledTools.
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+
12
+ // Tools the model must never get: they would let it rewrite its own limits or leak telemetry.
13
+ export const ALWAYS_HIDDEN = ['set_config_value', 'give_feedback_to_desktop_commander', 'get_usage_stats'];
14
+ // Argument names that carry filesystem paths in Desktop Commander's tools.
15
+ const PATH_KEYS = /^(path|paths|file_path|source|destination|inputPath|outputPath|directory)$/i;
16
+
17
+ const expandHome = (p) => (p === '~' || p.startsWith('~/') || p.startsWith('~\\') ? path.join(os.homedir(), p.slice(1)) : p);
18
+ export const norm = (p) => path.resolve(expandHome(String(p))).replace(/[\\/]+$/, '').toLowerCase();
19
+
20
+ // Resolve symlinks/junctions on the deepest existing ancestor, like Desktop Commander does.
21
+ export function realish(p) {
22
+ let cur = path.resolve(expandHome(String(p)));
23
+ const rest = [];
24
+ for (;;) {
25
+ try { return path.join(fs.realpathSync.native(cur), ...rest); } catch { /* walk up */ }
26
+ const up = path.dirname(cur);
27
+ if (up === cur) return path.resolve(expandHome(String(p)));
28
+ rest.unshift(path.basename(cur));
29
+ cur = up;
30
+ }
31
+ }
32
+
33
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34
+ // "c:\secret" mentioned as a path (not as the prefix of "c:\secretive"), any slash style / JSON escaping.
35
+ const flat = (s) => String(s).toLowerCase().replace(/[\\/]+/g, '\\');
36
+ const mentionRe = (d) => new RegExp(`${esc(d)}(?![a-z0-9_.\\-])`);
37
+
38
+ export function createPolicy(cfg = {}) {
39
+ const denied = (cfg.deniedDirectories || []).map((d) => norm(realish(d)));
40
+ const res = denied.map(mentionRe);
41
+ const mentionsDenied = (s) => { const f = flat(s); return res.some((r) => r.test(f)); };
42
+ const hidden = new Set([...ALWAYS_HIDDEN, ...(cfg.disabledTools || [])]);
43
+ const inDenied = (p) => {
44
+ const n = norm(p);
45
+ return denied.find((d) => n === d || n.startsWith(d + path.sep)) || null;
46
+ };
47
+ const deniedFor = (p) => inDenied(p) || inDenied(realish(p));
48
+
49
+ function pathsIn(args) {
50
+ const out = [];
51
+ for (const [k, v] of Object.entries(args || {})) {
52
+ if (!PATH_KEYS.test(k)) continue;
53
+ for (const x of Array.isArray(v) ? v : [v]) if (typeof x === 'string' && x) out.push(x);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ return {
59
+ denied,
60
+ visible: (tool) => !hidden.has(tool.name),
61
+ // → null when allowed, otherwise the refusal text returned to the model.
62
+ check(name, args) {
63
+ if (hidden.has(name)) return `Tool "${name}" is disabled on this computer.`;
64
+ for (const p of pathsIn(args)) {
65
+ if (deniedFor(p)) return `Access denied by Bridgerator: "${p}" is inside an excluded folder.`;
66
+ }
67
+ for (const k of ['command', 'input']) {
68
+ if (typeof args?.[k] === 'string' && mentionsDenied(args[k])) return `Access denied by Bridgerator: the ${k} refers to an excluded folder.`;
69
+ }
70
+ return null;
71
+ },
72
+ // Drop result lines that reveal anything inside a denied folder. `args` lets a listing of a
73
+ // parent folder hide the excluded child, whose entries are printed relative to that parent.
74
+ filter(result, name, args) {
75
+ if (!denied.length || !result || !Array.isArray(result.content)) return result;
76
+ const rel = [];
77
+ if (name === 'list_directory' && typeof args?.path === 'string') {
78
+ const base = norm(realish(args.path));
79
+ for (const d of denied) if (d.startsWith(base + path.sep)) rel.push(new RegExp(`^${esc(d.slice(base.length + 1))}(?![a-z0-9_.\\-])`));
80
+ }
81
+ const leaks = (line) => mentionsDenied(line) || rel.some((r) => r.test(flat(line).trim().replace(/^\[(dir|file)\]\s*/, '')));
82
+ let dropped = 0;
83
+ const content = result.content.map((c) => {
84
+ if (c?.type !== 'text' || typeof c.text !== 'string') return c;
85
+ const lines = c.text.split(/\r?\n/);
86
+ const kept = lines.filter((ln) => !leaks(ln));
87
+ dropped += lines.length - kept.length;
88
+ return { ...c, text: kept.join('\n') };
89
+ });
90
+ if (dropped) content.push({ type: 'text', text: `[Bridgerator: ${dropped} line(s) hidden — excluded folder]` });
91
+ return { ...result, content };
92
+ },
93
+ };
94
+ }
95
+
96
+ // The bundled engine's own name and links are not shown to the user: tool titles/descriptions and
97
+ // widget HTML say Bridgerator. Tool RESULTS are never rewritten (they are the user's own data).
98
+ const BRAND = [[/"DC: \.\.\." or /g, ''], [/Desktop ?Commander/g, 'Bridgerator'], [/https?:\/\/(www\.)?desktopcommander\.app[^\s"'\)<]*/g, 'https://bridgerator.recondeck.com']];
99
+ export function debrand(v) {
100
+ if (typeof v === 'string') return BRAND.reduce((s, [re, to]) => s.replace(re, to), v);
101
+ if (Array.isArray(v)) return v.map(debrand);
102
+ if (v && typeof v === 'object') {
103
+ const o = {};
104
+ // _meta keys (ui:// template URIs) are identifiers, not user-facing text — keep them as is.
105
+ for (const [k, x] of Object.entries(v)) o[k] = k === '_meta' || k === 'uri' ? x : debrand(x);
106
+ return o;
107
+ }
108
+ return v;
109
+ }