bridgerator 1.1.11 → 1.1.12

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "bridgerator",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "bridgerator",
9
- "version": "1.1.11",
9
+ "version": "1.1.12",
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.11",
3
+ "version": "1.1.12",
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
@@ -252,7 +252,8 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
252
252
  // bridgerator_setup: applied at once; after Apply on the PC only with askSettings on (setup-tool.mjs).
253
253
  const say = (t, isError) => ({ content: [{ type: 'text', text: t }], ...(isError ? { isError: true } : {}) });
254
254
  // The person sees every settings change not made in the window (notify.settingsChanged, on by default; owner 26.10).
255
- const toldSettings = (by, lines) => { if (lines.length && cfg.notify?.settingsChanged !== false) showNotice({ kind: 'settings_changed', by, what: lines }, log); };
255
+ // `on` is read before the change: the call that turns this notice off is still announced (review 26.10).
256
+ const toldSettings = (by, lines, on = cfg.notify?.settingsChanged !== false) => { const what = lines.filter((l) => l && l !== 'No changes' && !/: unchanged$/.test(l)).sort((a, b) => /settingsChanged/.test(b) - /settingsChanged/.test(a)); if (what.length && on) showNotice({ kind: 'settings_changed', by, what }, log); };
256
257
  async function setup(args) {
257
258
  const v = validateSetup(args);
258
259
  if (v.error) return say(`Not applied: ${v.error}`, true);
@@ -263,7 +264,7 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
263
264
  delete v.value.preset;
264
265
  }
265
266
  // Owner 21.10: applied at once; Apply on the PC only when the person turned on "Ask before AI changes settings".
266
- if (cfg.askSettings !== true) { const d = diffSetup(cfg, cfg.confirmIni, v.value).short; await applyValues(v.value, 'bridgerator_setup'); toldSettings('ai', d ? [d] : []); return say('Applied on the PC.'); }
267
+ if (cfg.askSettings !== true) { const d = diffSetup(cfg, cfg.confirmIni, v.value).short, on = cfg.notify?.settingsChanged !== false; try { await applyValues(v.value, 'bridgerator_setup'); } finally { toldSettings('ai', [d], on); } return say('Applied on the PC.'); }
267
268
  // A diff that fits the notification gets Apply there; a longer one is reviewed in the window.
268
269
  const diff = diffSetup(cfg, cfg.confirmIni, v.value);
269
270
  let r;
@@ -293,15 +294,18 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
293
294
  // Owner 21.10: everything applies at once unless "Ask before AI changes settings" is on (then only lowering protection asks).
294
295
  const ask1 = (c) => cfg.askSettings === true && widens(cur, c);
295
296
  const risky = v.changes.filter(ask1), safe = v.changes.filter((c) => !ask1(c));
296
- for (const c of safe) await rpc('setSetting', { ...c, via: 'bridgerator_settings' });
297
+ const on = cfg.notify?.settingsChanged !== false;
298
+ // Announced: what really changed (not a value sent again), also when a later change fails.
299
+ const real = (c) => JSON.stringify(c.key.split('.').reduce((o, k) => o?.[k], cur)) !== JSON.stringify(c.value);
300
+ const apply = async (list) => { const ok = []; try { for (const c of list) { await rpc('setSetting', { ...c, via: 'bridgerator_settings' }); if (real(c)) ok.push(describe(cur, c)); } } finally { toldSettings('ai', ok, on); } };
301
+ await apply(safe);
297
302
  const done = safe.map((c) => describe(cur, c));
298
- toldSettings('ai', done);
299
303
  if (!risky.length) return say(`Applied: ${done.join('; ')}`);
300
304
  const lines = risky.map((c) => describe(cur, c)), short = lines.join(' · ');
301
305
  const r = short.length <= 180
302
306
  ? await gate.askPerson('settings', { head: 'ChatGPT wants to change Bridgerator settings', text: short, buttons: [['allow', 'Apply', ''], ['deny', 'Cancel', 'deny']], secs: 120, auto: 'auto-cancel', ...popup() })
303
307
  : await gate.askPerson('settings', { review: true, lines });
304
- if (r === 'allow') { for (const c of risky) await rpc('setSetting', { ...c, via: 'bridgerator_settings' }); toldSettings('ai', lines); return say(`Applied${done.length ? `: ${done.join('; ')};` : ':'} after the person's OK: ${lines.join('; ')}`); }
308
+ if (r === 'allow') { await apply(risky); return say(`Applied${done.length ? `: ${done.join('; ')};` : ':'} after the person's OK: ${lines.join('; ')}`); }
305
309
  return { ...say(`${done.length ? `Applied: ${done.join('; ')}. ` : ''}Not applied (the person at the PC ${r === 'cooldown' ? 'declined a few minutes ago' : 'cancelled or didn\'t answer'}): ${lines.join('; ')}`), notConfirmed: true };
306
310
  }
307
311
 
@@ -660,20 +664,22 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
660
664
  // Owner 26.10: a file edit applies at once like an AI change; Apply on the PC only with askSettings on.
661
665
  const ask1 = (c) => cfg.askSettings === true && widens(cur, c);
662
666
  const risky = changes.filter(ask1), safe = changes.filter((c) => !ask1(c));
663
- const denAsk = cfg.askSettings === true && denRemoved.length > 0;
667
+ // Excluded folders are never AI-editable (bridgerator_settings / setup can't touch them): removing one from the file always asks.
668
+ const denAsk = denRemoved.length > 0;
669
+ const on = cfg.notify?.settingsChanged !== false;
664
670
  const back = {};
665
671
  const top = (key) => (key === 'terminal' ? 'disabledTools' : key.split('.')[0]);
666
672
  for (const c of safe) { try { await rpc('setSetting', { ...c, via: 'settings file' }); } catch (e) { log(`[bridgerator] settings file: ${c.key} not applied (${e.message})`); back[top(c.key)] = cfg[top(c.key)]; } }
667
673
  if (!denAsk) { cfg.deniedDirectories = den; policy = makePolicy(); }
668
674
  const denLine = denRemoved.length ? [`Excluded folders: ${denRemoved.map((x) => `−${x}`).join(', ')}`] : [];
669
- toldSettings('file', [...safe.filter((c) => !(top(c.key) in back)).map((c) => describe(cur, c)), ...(denAsk ? [] : denLine)]);
675
+ toldSettings('file', safe.filter((c) => !(top(c.key) in back)).map((c) => describe(cur, c)), on);
670
676
  const lines = [...risky.map((c) => describe(cur, c)), ...(denAsk ? denLine : [])];
671
677
  if (lines.length) {
672
678
  const r = await gate.askPerson('file', { head: 'Bridgerator settings file changed', text: clip(`Apply: ${lines.join(' · ')}?`, '', 180), buttons: [['allow', 'Apply', ''], ['deny', 'Keep', 'deny']], secs: 120, auto: 'auto-keep', ...popup() });
673
679
  if (r === 'allow') {
674
680
  for (const c of risky) { try { await rpc('setSetting', { ...c, via: 'settings file' }); } catch (e) { log(`[bridgerator] settings file: ${c.key} not applied (${e.message})`); back[top(c.key)] = cfg[top(c.key)]; } }
675
681
  if (denAsk) { cfg.deniedDirectories = den; policy = makePolicy(); }
676
- toldSettings('file', lines);
682
+ toldSettings('file', [...risky.filter((c) => !(top(c.key) in back)).map((c) => describe(cur, c)), ...(denRemoved.length ? denLine : [])], on);
677
683
  log(`[bridgerator] settings file change applied on the PC: ${lines.join('; ')}`);
678
684
  } else {
679
685
  for (const c of risky) back[top(c.key)] = cfg[top(c.key)];
@@ -786,6 +792,7 @@ export function connect(cfg, dc, log = console.log) {
786
792
  return;
787
793
  }
788
794
  if (msg.type === 'notice') {
795
+ if (msg.kind === 'settings_changed') return; // the bridge's own notice, never from the relay
789
796
  if (['limit', 'expiring', 'expired'].includes(msg.kind) && cfg.notify?.limit === false) return;
790
797
  showNotice({ ...msg, renewUrl: msg.renewUrl ?? dc.plan?.renewUrl }, log);
791
798
  return;
package/src/cli.mjs CHANGED
@@ -338,9 +338,10 @@ const defaultDenied = () => [];
338
338
  export const excludedLine = (cfg) => [...(cfg.deniedDirectories || []), `built-in: Bridgerator's own folders${cfg.protectSensitive ? ', AppData, credentials (.ssh, .aws, …), Startup, PowerShell profiles' : ''}`].join(', ');
339
339
  /** 'ru' when the Windows display language or regional format is Russian, else 'en'. */
340
340
  // Node's Intl gives only the regional format: the display language comes from Windows, as in install.ps1.
341
- const uiCulture = () => { try { return process.platform === 'win32' ? ps('[Globalization.CultureInfo]::CurrentUICulture.Name').trim() : ''; } catch { return ''; } };
342
- export function osLanguage(locales = [Intl.DateTimeFormat().resolvedOptions().locale, process.env.LANG, uiCulture()]) {
343
- return locales.some((l) => /^ru([-_.]|$)/i.test(String(l || ''))) ? 'ru' : 'en';
341
+ const uiCulture = () => { try { return process.platform === 'win32' ? execFileSync(POWERSHELL, psArgs('[Globalization.CultureInfo]::CurrentUICulture.Name'), { stdio: 'pipe', timeout: 15000 }).toString().trim() : ''; } catch { return ''; } };
342
+ export function osLanguage(locales = [Intl.DateTimeFormat().resolvedOptions().locale, process.env.LANG], askWindows = arguments.length === 0) {
343
+ const ru = (l) => /^ru([-_.]|$)/i.test(String(l || ''));
344
+ return locales.some(ru) || (askWindows && ru(uiCulture())) ? 'ru' : 'en';
344
345
  }
345
346
  const keptDenied = (list) => (Array.isArray(list) && !(list.length === 1 && process.env.OneDrive && list[0].toLowerCase() === process.env.OneDrive.toLowerCase()) ? list : []);
346
347
 
package/src/notice.mjs CHANGED
@@ -39,8 +39,11 @@ export function noticeText(n) {
39
39
  // The bridge's own change lines (describe / diffSetup): paths and labels, no relay text.
40
40
  const what = Array.isArray(n.what) ? n.what.map((w) => String(w).replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').trim()).filter(Boolean) : [];
41
41
  if (!what.length || !['ai', 'file'].includes(n.by)) return undefined;
42
- const all = what.join(' · ');
43
- t = ['Bridgerator settings changed', `${n.by === 'ai' ? 'By the AI' : 'In the settings file'}: ${all.length > 200 ? `${all.slice(0, 199)}…` : all}. Turn this notice off in the Bridgerator window.`];
42
+ // Whole lines up to ~200 chars, then "+N more" (the window has the full list): nothing is cut silently.
43
+ let all = what[0].slice(0, 200), k = 1;
44
+ while (k < what.length && all.length + what[k].length + 3 <= 200) all += ` · ${what[k++]}`;
45
+ if (k < what.length) all += ` · +${what.length - k} more`;
46
+ t = ['Bridgerator settings changed', `${n.by === 'ai' ? 'By the AI' : 'In the settings file'}: ${all}. Turn this notice off in the Bridgerator window.`];
44
47
  } else if (kind === 'protection_off') {
45
48
  // Internal list only (the bridge's own labels), never relay text.
46
49
  const ok = new Set(['confirmations', 'your own patterns', ...Object.values(LABEL).map((l) => `confirmations for ${l}`)]);
package/src/queue.mjs CHANGED
@@ -199,7 +199,7 @@ export function queueTool(q, name, args = {}) {
199
199
  for (const x of want) { const d = args.allowDuplicate === true ? undefined : open.get(key(x?.title)); if (d) skipped.push({ title: x.title, duplicate_of: d }); else keep.push(x); }
200
200
  const why = skipped.length ? { skipped, message: 'Already in the queue (open), not added again: work on those items (queue_list / queue_next), or pass allowDuplicate: true to add them anyway.' } : {};
201
201
  if (!keep.length && skipped.length) return ok({ added: 0, ...why });
202
- if (args.items === undefined) return ok({ item: q.add(args) });
202
+ if (args.items == null) return ok({ item: q.add(args) });
203
203
  return ok({ items: q.addMany(Array.isArray(args.items) ? keep : args.items), ...why });
204
204
  }
205
205
  case 'queue_clear': { if (args.status === 'all') throw new Error('clearing every item needs the person\'s OK'); const gone = q.clear({ status: args.status || 'finished' }); return ok({ removed: gone.length, counts: q.counts() }); }
@@ -114,6 +114,7 @@ export function widens(cur, { key, value }) {
114
114
  if (key === 'fuse.enabled') return !value && fuse.enabled;
115
115
  if (key === 'fuse.limit') return fuse.enabled && value > fuse.limit;
116
116
  if (key === 'askSettings') return !value && cur.askSettings === true;
117
+ if (key === 'notify.settingsChanged') return !value && cur.notify?.settingsChanged !== false; // the person's view of AI changes
117
118
  if (key === 'keepAwake' || key === 'preset') return false; // preset: judged by its parts (presetChanges)
118
119
  if (key === 'autostart') return value !== cur.autostart;
119
120
  if (key === 'telemetryEnabled') return value && !cur.telemetry;