bridgerator 1.1.5 → 1.1.7

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 CHANGED
@@ -19,6 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "bridgerator",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "bridgerator",
9
- "version": "1.1.5",
9
+ "version": "1.1.7",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "^1.29.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgerator",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
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
@@ -1,5 +1,5 @@
1
1
  // Bridgerator bridge: runs on the user's PC.
2
- // - starts the bundled open-source Desktop Commander MCP server as a child over stdio;
2
+ // - starts the bundled MCP engine as a child over stdio;
3
3
  // - pushes this PC's limits (allowedDirectories, blockedCommands) into it at startup;
4
4
  // - opens ONE outbound WebSocket to the relay (no inbound port is opened on this PC);
5
5
  // - forwards each relay `call` through the policy (excluded / read-only folders, disabled tools)
@@ -9,7 +9,7 @@ import { createRequire } from 'node:module';
9
9
  import WebSocket from 'ws';
10
10
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
11
11
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
12
- import { createPolicy, debrand, debrandResult, TERMINAL_TOOLS, ALWAYS_HIDDEN } from './policy.mjs';
12
+ import { createPolicy, debrand, debrandResult, ourUi, engineUi, TERMINAL_TOOLS, ALWAYS_HIDDEN } from './policy.mjs';
13
13
  import { showNotice } from './notice.mjs';
14
14
  import { usage } from './telemetry.mjs';
15
15
  import { createGate, askOnPc, CONFIRM_TOOL_DEFS } from './confirm.mjs';
@@ -78,7 +78,7 @@ export async function localDrivesAsync() {
78
78
  }
79
79
  const DRIVES_MS = 60000;
80
80
 
81
- export async function startDesktopCommander(cfg, log = console.log, { ask: askArg, recycle: recycleArg, drives: drivesArg, delegate: delegateOpts } = {}) {
81
+ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle: recycleArg, drives: drivesArg, delegate: delegateOpts } = {}) {
82
82
  const ask0 = askArg || askOnPc;
83
83
  const recycle = recycleArg || recycleOnPc;
84
84
  // Drive list: refreshed in the background once a minute (new / removed drives are seen); only the
@@ -115,7 +115,7 @@ export async function startDesktopCommander(cfg, log = console.log, { ask: askAr
115
115
  const absPath = (p) => path.resolve(engineDir, String(p).replace(/^~(?=[\\/]|$)/, os.homedir()));
116
116
  const transport = new StdioClientTransport({
117
117
  command: process.execPath,
118
- // Own Desktop Commander config (dc-isolate.mjs): a DC the user runs elsewhere keeps its limits.
118
+ // The engine's own config folder (dc-isolate.mjs): a copy of the engine the user runs elsewhere keeps its limits.
119
119
  args: ['--import', ISOLATE, DC_ENTRY],
120
120
  // No calls home: telemetry kill-switch, and remote feature flags (onboarding page, promo text
121
121
  // injected into tool results, A/B tests) replaced by an empty local set.
@@ -133,7 +133,7 @@ export async function startDesktopCommander(cfg, log = console.log, { ask: askAr
133
133
  // Engine gone (crash, killed): nothing works without it — exit, the autostart starts us again.
134
134
  let closing = false;
135
135
  client.onclose = () => { if (closing) return; log(`[bridgerator] engine stopped — exiting (autostart restarts the bridge)${engineErr()}`); process.exit(1); };
136
- // The PC owner's limits go into Desktop Commander's own config; the model can't change them
136
+ // The PC owner's limits go into the engine's own config; the model can't change them
137
137
  // (set_config_value is never exposed to the relay, see policy.ALWAYS_HIDDEN).
138
138
  const set = (key, value) => client.callTool({ name: 'set_config_value', arguments: { key, value } });
139
139
  await set('allowedDirectories', cfg.allowedDirectories || []);
@@ -152,7 +152,7 @@ export async function startDesktopCommander(cfg, log = console.log, { ask: askAr
152
152
  // Snapshots + Undo (on by default) in %LOCALAPPDATA%\Bridgerator\snapshots (given by the CLI).
153
153
  const snapCfg = () => ({ ...SNAP_DEFAULTS, ...(cfg.snapshots || {}) });
154
154
  const snapshots = cfg.snapshotsDir ? createSnapshots(cfg.snapshotsDir, { limits: snapCfg, log }) : null;
155
- const visible = [...tools.filter(listed).map(debrand), ...CONFIRM_TOOL_DEFS, SETUP_TOOL_DEF, ...(cfg.configFile ? [SETTINGS_TOOL_DEF] : []), ...(cfg.configDir ? QUEUE_TOOL_DEFS : []), ...(snapshots ? [UNDO_TOOL_DEF] : []), ...extra.defs.filter(policy.visible)].map(shorten);
155
+ const visible = [...tools.filter(listed).map(debrand), ...CONFIRM_TOOL_DEFS, SETUP_TOOL_DEF, ...(cfg.configFile ? [SETTINGS_TOOL_DEF] : []), ...(cfg.configDir ? QUEUE_TOOL_DEFS : []), ...(snapshots ? [UNDO_TOOL_DEF] : []), ...extra.defs.filter(policy.visible)].map(shorten).map(ourUi);
156
156
  const virtual = new Set(CONFIRM_TOOL_DEFS.map((t) => t.name));
157
157
  // namePending: a computer name given on this PC not yet accepted by the relay — kept in config.json too,
158
158
  // so a restart before the next link does not lose it; cleared by nameSent() (welcome / live mode message).
@@ -197,7 +197,7 @@ export async function startDesktopCommander(cfg, log = console.log, { ask: askAr
197
197
  return gate.accept(s ? { enabled: s.enabled } : {});
198
198
  };
199
199
  // MCP Apps widgets (file preview, config view) — served to ChatGPT through the relay as-is.
200
- const resources = ((await client.listResources().catch(() => ({ resources: [] }))).resources || []).map(debrand);
200
+ const resources = ourUi(((await client.listResources().catch(() => ({ resources: [] }))).resources || []).map(debrand));
201
201
  const resourceUris = new Set(resources.map((r) => r.uri));
202
202
  log(`[bridgerator] engine up: ${visible.length} tools; allowed=${JSON.stringify(cfg.allowedDirectories || [])} excluded=${JSON.stringify(cfg.deniedDirectories || [])} read-only=${JSON.stringify(cfg.readOnlyDirectories || [])} confirm=${gate.mode()}`);
203
203
  if (!(cfg.allowedDirectories || []).length) log('[bridgerator] folders: all (no list), except the always-blocked ones');
@@ -492,8 +492,9 @@ export async function startDesktopCommander(cfg, log = console.log, { ask: askAr
492
492
  return { ...rest, confirm: { enabled: confirm.enabled, ...confirm.ask, timeoutSeconds: confirm.timeoutSec }, telemetryEnabled: telemetry };
493
493
  };
494
494
  async function readResource(uri) {
495
- if (!resourceUris.has(uri)) return { error: 'unknown resource' };
496
- try { return { result: debrand(await client.readResource({ uri })) }; } catch (e) { return { error: String(e?.message || e) }; }
495
+ // The old widget URI still works: ChatGPT and the relay's offline list may have cached it.
496
+ if (!resourceUris.has(ourUi(String(uri)))) return { error: 'unknown resource' };
497
+ try { return { result: ourUi(debrand(await client.readResource({ uri: engineUi(uri) }))) }; } catch (e) { return { error: String(e?.message || e) }; }
497
498
  }
498
499
  // Requests of the Bridgerator window (tray helper). Made by the person at the PC: no extra prompt.
499
500
  // Theme for our pop-ups = the window's theme (Settings → Appearance); sound / style from Notifications.
package/src/cli.mjs CHANGED
@@ -10,7 +10,7 @@ import crypto from 'node:crypto';
10
10
  import { spawn, execFileSync } from 'node:child_process';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { isSea } from 'node:sea';
13
- import { startDesktopCommander, connect, checkRelay } from './bridge.mjs';
13
+ import { startEngine, connect, checkRelay } from './bridge.mjs';
14
14
  import { pair, siteOf, checkKey, machineFp } from './pair.mjs';
15
15
  import { ACCOUNT_URL } from './notice.mjs';
16
16
  import { send, VERSION } from './telemetry.mjs';
@@ -393,7 +393,7 @@ async function main() {
393
393
  protectSensitive: prev.protectSensitive === true,
394
394
  // Readable, never changed by the file tools (the terminal is not bound by folders).
395
395
  readOnlyDirectories: o['read-only'] ? abs(o['read-only']) : prev.readOnlyDirectories || [],
396
- // blockedCommands: absent = Desktop Commander's own default list (format, diskpart, reg, net, sc, runas, …).
396
+ // blockedCommands: absent = the engine's own default list (format, diskpart, reg, net, sc, runas, …).
397
397
  ...(prev.blockedCommands ? { blockedCommands: prev.blockedCommands } : {}),
398
398
  disabledTools: prev.disabledTools || [],
399
399
  installId: prev.installId || crypto.randomUUID(),
@@ -471,7 +471,7 @@ async function main() {
471
471
  plan: b.kind === 'none' ? null : { label: b.action === 'renew' ? 'Renew Pro…' : 'Upgrade to Pro…', url: b.url, badge: b.tone },
472
472
  });
473
473
  };
474
- dc = await startDesktopCommander({ ...cfg, dcConfigDir: path.join(DIR, 'dc'), configDir: DIR, confirmIni: INI, configFile: CONFIG, snapshotsDir: SNAP_DIR, scriptsDir: SCRIPTS_DIR }, log, { ask: tray ? (q) => tray.ask(q) : undefined });
474
+ dc = await startEngine({ ...cfg, dcConfigDir: path.join(DIR, 'dc'), configDir: DIR, confirmIni: INI, configFile: CONFIG, snapshotsDir: SNAP_DIR, scriptsDir: SCRIPTS_DIR }, log, { ask: tray ? (q) => tray.ask(q) : undefined });
475
475
  dc.cfgName = () => readConfig()?.deviceName;
476
476
  dc.openWindow = (page) => tray?.open(page);
477
477
  dc.onPlan = pushTray;
@@ -25,7 +25,7 @@ const PATCHES = [
25
25
  { file: /[\\/]dist[\\/]terminal-manager\.js$/, from: "args: ['-Command', command],", to: "args: ['-Command', '[Console]::OutputEncoding=[Text.Encoding]::UTF8;' + command]," },
26
26
  { file: /[\\/]dist[\\/]index\.js$/, from: ' ensureChromeAvailable();\n', to: ' // bridgerator: no Chrome pre-download\n' },
27
27
  ];
28
- const ENGINE = /[\\/]bridgerator-engine[\\/]dist[\\/]/; // the bundled engine (Desktop Commander, MIT) installed under our own name (npm alias, owner 26.10)
28
+ const ENGINE = /[\\/]bridgerator-engine[\\/]dist[\\/]/; // the bundled engine, installed under our own name (owner 26.10)
29
29
 
30
30
  export async function load(url, context, nextLoad) {
31
31
  const r = await nextLoad(url, context);
@@ -1,4 +1,4 @@
1
- // Preload for the Desktop Commander child: registers dc-config-hooks.mjs (see there).
1
+ // Preload for the engine child: registers dc-config-hooks.mjs (see there).
2
2
  import { register } from 'node:module';
3
3
 
4
4
  if (!process.env.BRIDGERATOR_DC_CONFIG_DIR) throw new Error('bridgerator: BRIDGERATOR_DC_CONFIG_DIR not set');
package/src/policy.mjs CHANGED
@@ -1,5 +1,5 @@
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,
1
+ // Bridge-side access policy, applied BEFORE a call reaches the engine and to its results.
2
+ // The engine itself only knows allowedDirectories; it has no "except these folders" rule,
3
3
  // so deniedDirectories is enforced here: input paths are resolved (symlinks/junctions included)
4
4
  // and refused when they fall inside a denied folder; result lines that mention a denied path are
5
5
  // dropped (search hits, recursive listings).
@@ -53,7 +53,7 @@ const expandHome = (p) => (p === '~' || p.startsWith('~/') || p.startsWith('~\\'
53
53
  // base: the folder a relative path is resolved against (default: this process's cwd).
54
54
  export const norm = (p, base) => path.resolve(base || '.', expandHome(String(p))).replace(/[\\/]+$/, '').toLowerCase();
55
55
 
56
- // Resolve symlinks/junctions on the deepest existing ancestor, like Desktop Commander does.
56
+ // Resolve symlinks/junctions on the deepest existing ancestor, like the engine does.
57
57
  export function realish(p, base) {
58
58
  const start = path.resolve(base || '.', expandHome(String(p)));
59
59
  let cur = start;
@@ -157,6 +157,8 @@ const SELF_CHANGE = [
157
157
  /\b(?:taskkill|tskill|pskill|kill|stop-process|wmic|schtasks|(?:start|stop|disable|enable|unregister|set)-scheduledtask)\b[^|;&\n]*\bbridgerator/i,
158
158
  /\bbridgerator[^;&\n]*\|\s*(?:stop-process|kill|(?:stop|disable|unregister|set)-scheduledtask)\b/i,
159
159
  ];
160
+ // Tools whose result is the text of files that passed the folder check (shown whole, never filtered).
161
+ const FILE_TEXT = new Set(['read_file', 'read_multiple_files']);
160
162
  export function createPolicy(cfg = {}) {
161
163
  const base = cfg.cwd || process.cwd();
162
164
  // protectSensitive absent = on (callers that don't say); the bridge passes the person's setting.
@@ -249,8 +251,10 @@ export function createPolicy(cfg = {}) {
249
251
  },
250
252
  // Drop result lines that reveal anything inside a denied folder. `args` lets a listing of a
251
253
  // parent folder hide the excluded child, whose entries are printed relative to that parent.
254
+ // Not for the text of a file that was allowed to be read: a line that merely NAMES an excluded folder is the
255
+ // person's own content (E2E 26.10: .md notes lost lines, a later full rewrite would delete them from disk).
252
256
  filter(result, name, args) {
253
- if (!denied.length || !result || !Array.isArray(result.content)) return result;
257
+ if (!denied.length || !result || !Array.isArray(result.content) || FILE_TEXT.has(name)) return result;
254
258
  const rel = [];
255
259
  if (name === 'list_directory' && typeof args?.path === 'string') {
256
260
  const at = norm(realish(args.path, base));
@@ -274,6 +278,11 @@ export function createPolicy(cfg = {}) {
274
278
  // The bundled engine's own name and links are not shown to the user: tool titles/descriptions and
275
279
  // widget HTML say Bridgerator. Tool RESULTS are never rewritten (they are the user's own data).
276
280
  const BRAND = [[/"DC: \.\.\." or /g, ''], [/Desktop ?Commander/g, 'Bridgerator'], [/https?:\/\/(www\.)?desktopcommander\.app[^\s"'\)<]*/g, 'https://bridgerator.recondeck.com']];
281
+ // The engine's widget URIs and their in-page state key carry its name: ChatGPT sees them in tool _meta and resources.
282
+ // Shown as ui://bridgerator/…, mapped back when a resource is read (owner 26.10: "как будто наш код").
283
+ const UI_ENGINE = 'ui://desktop-commander/', UI_OURS = 'ui://bridgerator/';
284
+ export const ourUi = (v) => JSON.parse(JSON.stringify(v).replaceAll(UI_ENGINE, UI_OURS).replaceAll('desktop-commander:widget-state', 'bridgerator:widget-state'));
285
+ export const engineUi = (uri) => String(uri).replace(UI_OURS, UI_ENGINE);
277
286
  export function debrand(v) {
278
287
  if (typeof v === 'string') return BRAND.reduce((s, [re, to]) => s.replace(re, to), v);
279
288
  if (Array.isArray(v)) return v.map(debrand);
@@ -287,7 +296,7 @@ export function debrand(v) {
287
296
  }
288
297
 
289
298
  // Two results are the engine's own text, not user data: the prompt catalog (get_prompts says
290
- // "Desktop Commander Prompt Categories") and get_config, whose `version` is the engine's and is
299
+ // its own brand in the heading) and get_config, whose `version` is the engine's and is
291
300
  // shown as "Bridgerator version" — replaced by the bridge's own version.
292
301
  export function debrandResult(name, result, version) {
293
302
  if (!result || typeof result !== 'object') return result;
package/ui/app.js CHANGED
@@ -172,7 +172,7 @@ function awakeLine() {
172
172
  return `<div class="awake-line ${a.on ? 'on' : ''}"><span class="dot ${a.on ? 'online' : 'offline'}"></span>${esc(t(k))}</div>`;
173
173
  }
174
174
  // The Bridgerator app's "Permission" page in ChatGPT (App ID of the store app). Owner 26.10: on first use ChatGPT
175
- // asks before most actions ("Allow low-risk"); one switch to "Allow all" stops that, like Desktop Commander.
175
+ // asks before most actions ("Allow low-risk"); one switch to "Allow all" stops that.
176
176
  const CHATGPT_PERMISSIONS = 'https://chatgpt.com/settings/plugins-settings/plugin_asdk_app_6ad78d4a3c408191a9215fd304394c4a/permissions';
177
177
  P.home = () => {
178
178
  const st = state.state || 'offline';