bridgerator 1.1.9 → 1.1.11

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.9",
3
+ "version": "1.1.11",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "bridgerator",
9
- "version": "1.1.9",
9
+ "version": "1.1.11",
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.9",
3
+ "version": "1.1.11",
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
@@ -251,6 +251,8 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
251
251
 
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
+ // 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); };
254
256
  async function setup(args) {
255
257
  const v = validateSetup(args);
256
258
  if (v.error) return say(`Not applied: ${v.error}`, true);
@@ -261,7 +263,7 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
261
263
  delete v.value.preset;
262
264
  }
263
265
  // Owner 21.10: applied at once; Apply on the PC only when the person turned on "Ask before AI changes settings".
264
- if (cfg.askSettings !== true) { await applyValues(v.value, 'bridgerator_setup'); return say('Applied on the PC.'); }
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.'); }
265
267
  // A diff that fits the notification gets Apply there; a longer one is reviewed in the window.
266
268
  const diff = diffSetup(cfg, cfg.confirmIni, v.value);
267
269
  let r;
@@ -293,12 +295,13 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
293
295
  const risky = v.changes.filter(ask1), safe = v.changes.filter((c) => !ask1(c));
294
296
  for (const c of safe) await rpc('setSetting', { ...c, via: 'bridgerator_settings' });
295
297
  const done = safe.map((c) => describe(cur, c));
298
+ toldSettings('ai', done);
296
299
  if (!risky.length) return say(`Applied: ${done.join('; ')}`);
297
300
  const lines = risky.map((c) => describe(cur, c)), short = lines.join(' · ');
298
301
  const r = short.length <= 180
299
302
  ? 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() })
300
303
  : await gate.askPerson('settings', { review: true, lines });
301
- if (r === 'allow') { for (const c of risky) await rpc('setSetting', { ...c, via: 'bridgerator_settings' }); return say(`Applied${done.length ? `: ${done.join('; ')};` : ':'} after the person's OK: ${lines.join('; ')}`); }
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('; ')}`); }
302
305
  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 };
303
306
  }
304
307
 
@@ -504,7 +507,7 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
504
507
  const bannerNow = () => { const b = planBanner(out.plan); return { ...b, visible: bannerVisible(b, cfg.planBannerDismissed) }; };
505
508
  out.banner = bannerNow;
506
509
  out.onPlanMsg = (m) => { out.plan = readPlan(m); out.onPlan?.(); };
507
- const BOOL_NOTIFY = ['sound', 'offline', 'queueDone', 'queueStalled', 'limit'];
510
+ const BOOL_NOTIFY = ['sound', 'offline', 'queueDone', 'queueStalled', 'settingsChanged', 'limit'];
508
511
 
509
512
  // Requests of the Bridgerator window (tray helper). Made by the person at the PC: no extra prompt.
510
513
  async function rpc(method, p = {}, paths = []) {
@@ -654,21 +657,27 @@ export async function startEngine(cfg, log = console.log, { ask: askArg, recycle
654
657
  const den = list(j.deniedDirectories), denNow = cfg.deniedDirectories || [];
655
658
  const denRemoved = denNow.filter((x) => !den.some((y) => y.toLowerCase() === x.toLowerCase()));
656
659
  if (!changes.length && JSON.stringify(den) === JSON.stringify(denNow)) return;
657
- const risky = changes.filter((c) => widens(cur, c)), safe = changes.filter((c) => !widens(cur, c));
660
+ // Owner 26.10: a file edit applies at once like an AI change; Apply on the PC only with askSettings on.
661
+ const ask1 = (c) => cfg.askSettings === true && widens(cur, c);
662
+ const risky = changes.filter(ask1), safe = changes.filter((c) => !ask1(c));
663
+ const denAsk = cfg.askSettings === true && denRemoved.length > 0;
658
664
  const back = {};
659
665
  const top = (key) => (key === 'terminal' ? 'disabledTools' : key.split('.')[0]);
660
666
  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)]; } }
661
- if (!denRemoved.length) { cfg.deniedDirectories = den; policy = makePolicy(); }
662
- const lines = [...risky.map((c) => describe(cur, c)), ...(denRemoved.length ? [`Excluded folders: ${denRemoved.map((x) => `−${x}`).join(', ')}`] : [])];
667
+ if (!denAsk) { cfg.deniedDirectories = den; policy = makePolicy(); }
668
+ 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)]);
670
+ const lines = [...risky.map((c) => describe(cur, c)), ...(denAsk ? denLine : [])];
663
671
  if (lines.length) {
664
672
  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() });
665
673
  if (r === 'allow') {
666
674
  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)]; } }
667
- if (denRemoved.length) { cfg.deniedDirectories = den; policy = makePolicy(); }
675
+ if (denAsk) { cfg.deniedDirectories = den; policy = makePolicy(); }
676
+ toldSettings('file', lines);
668
677
  log(`[bridgerator] settings file change applied on the PC: ${lines.join('; ')}`);
669
678
  } else {
670
679
  for (const c of risky) back[top(c.key)] = cfg[top(c.key)];
671
- if (denRemoved.length) back.deniedDirectories = denNow;
680
+ if (denAsk) back.deniedDirectories = denNow;
672
681
  log(`[bridgerator] settings file change kept out (${r}): ${lines.join('; ')}`);
673
682
  }
674
683
  }
package/src/cli.mjs CHANGED
@@ -337,7 +337,9 @@ const defaultDenied = () => [];
337
337
  /** The setup summary's "excluded" line: only what really applies (sensitive folders only when that switch is on). */
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
- export function osLanguage(locales = [Intl.DateTimeFormat().resolvedOptions().locale, process.env.LANG]) {
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()]) {
341
343
  return locales.some((l) => /^ru([-_.]|$)/i.test(String(l || ''))) ? 'ru' : 'en';
342
344
  }
343
345
  const keptDenied = (list) => (Array.isArray(list) && !(list.length === 1 && process.env.OneDrive && list[0].toLowerCase() === process.env.OneDrive.toLowerCase()) ? list : []);
package/src/notice.mjs CHANGED
@@ -35,6 +35,12 @@ export function noticeText(n) {
35
35
  if (!title) return undefined;
36
36
  const more = int(n.more) || 0;
37
37
  t = ['ChatGPT may be waiting for you', `No activity on the queue task "${title}"${more ? ` and ${more} more` : ''} for 10 minutes. Open the ChatGPT chat: it may be asking you to allow an action, or show an error such as "model is at capacity" — press Retry there or pick another model.`];
38
+ } else if (kind === 'settings_changed') {
39
+ // The bridge's own change lines (describe / diffSetup): paths and labels, no relay text.
40
+ const what = Array.isArray(n.what) ? n.what.map((w) => String(w).replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').trim()).filter(Boolean) : [];
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.`];
38
44
  } else if (kind === 'protection_off') {
39
45
  // Internal list only (the bridge's own labels), never relay text.
40
46
  const ok = new Set(['confirmations', 'your own patterns', ...Object.values(LABEL).map((l) => `confirmations for ${l}`)]);
package/src/queue.mjs CHANGED
@@ -24,7 +24,7 @@ export const QUEUE_TOOL_DEFS = [
24
24
  { name: 'queue_list', description: `List the task queue the person keeps on this PC (a checklist of things to do): counts and items (id, title, status, note, worker, updatedAt, from, resultPath), optionally only one status.`, inputSchema: { type: 'object', additionalProperties: false, properties: { status: { type: 'string', enum: STATUSES } } } },
25
25
  { name: 'queue_next', description: `Take the next item of the task queue: returns the first "todo" item and marks it "doing" (nothing when the queue is empty). An item left "doing" for 30 minutes without an update (another chat stopped) is returned first, with resumed: true. Several chats can work through one queue at once; each call gets a different item. Pass worker = a short name for this chat (e.g. its title) so the person sees who is on it. ${RULE}`, inputSchema: { type: 'object', additionalProperties: false, properties: { worker: STR(80), id: { type: 'string', description: 'take this "todo" item instead of the first one' } } } },
26
26
  { name: 'queue_update', description: `Set an item's status (doing / done / failed / skipped / todo) with a short result note.`, inputSchema: { type: 'object', additionalProperties: false, required: ['id', 'status'], properties: { id: ID, status: { type: 'string', enum: STATUSES }, note: STR(MAX_NOTE) } } },
27
- { name: 'queue_add', description: `Add steps to the task queue (e.g. when planning a bigger job with the person): one { title, details?, paths?, from?, resultPath? } or items: [...] (1–100). Another AI handing work to ChatGPT sets from (its name) and resultPath (a file for the result), then reads the outcome with queue_list.`, inputSchema: { type: 'object', additionalProperties: false, properties: { title: STR(MAX_TITLE), details: STR(MAX_DETAILS), paths: PATHS, ...HANDOFF, items: { type: 'array', minItems: 1, maxItems: 100, items: { type: 'object', additionalProperties: false, required: ['title'], properties: { title: STR(MAX_TITLE), details: STR(MAX_DETAILS), paths: PATHS, ...HANDOFF } } } } } },
27
+ { name: 'queue_add', description: `Add steps to the task queue (e.g. when planning a bigger job with the person): one { title, details?, paths?, from?, resultPath? } or items: [...] (1–100). Another AI handing work to ChatGPT sets from (its name) and resultPath (a file for the result), then reads the outcome with queue_list. A step whose title matches an open (todo / doing) item is not added again — the reply names that item; allowDuplicate: true adds it anyway.`, inputSchema: { type: 'object', additionalProperties: false, properties: { title: STR(MAX_TITLE), details: STR(MAX_DETAILS), paths: PATHS, ...HANDOFF, allowDuplicate: { type: 'boolean' }, items: { type: 'array', minItems: 1, maxItems: 100, items: { type: 'object', additionalProperties: false, required: ['title'], properties: { title: STR(MAX_TITLE), details: STR(MAX_DETAILS), paths: PATHS, ...HANDOFF } } } } } },
28
28
  { name: 'queue_clear', description: 'Remove finished items from the task queue: status done, failed or skipped. status "all" removes every item and needs the person\'s OK on the PC.', inputSchema: { type: 'object', additionalProperties: false, properties: { status: { type: 'string', enum: ['done', 'failed', 'skipped', 'all'] } } } },
29
29
  { name: 'queue_edit', description: 'Change the title, details, paths or result file of a queue item.', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: ID, title: STR(MAX_TITLE), details: STR(MAX_DETAILS), paths: PATHS, resultPath: HANDOFF.resultPath } } },
30
30
  { name: 'queue_reorder', description: 'Put queue items in a new order: ids first, in this order; the rest keep their order after them.', inputSchema: { type: 'object', additionalProperties: false, required: ['ids'], properties: { ids: { type: 'array', maxItems: MAX_ITEMS, items: ID } } } },
@@ -190,7 +190,18 @@ export function queueTool(q, name, args = {}) {
190
190
  case 'queue_list': return ok({ counts: q.counts(), items: q.list(args.status).map(({ id, title, status, note, worker, updatedAt, details, paths, from, resultPath }) => ({ id, title, status, ...(note ? { note } : {}), ...(worker ? { worker } : {}), updatedAt, ...(details ? { details } : {}), ...(paths ? { paths } : {}), ...(from ? { from } : {}), ...(resultPath ? { resultPath } : {}) })) });
191
191
  case 'queue_next': { const it = q.next(args); return ok(it ? { item: it } : { item: null, message: 'The queue is empty — tell the person and stop.' }); }
192
192
  case 'queue_update': return ok({ item: q.update(args) });
193
- case 'queue_add': return args.items ? ok({ items: q.addMany(args.items) }) : ok({ item: q.add(args) });
193
+ case 'queue_add': {
194
+ // A second chat given the same request re-added the whole set (E2E 26.10): an open item with the same
195
+ // title is skipped and named. Only the AI's tool; the window adds what the person types.
196
+ const key = (t) => (typeof t === 'string' ? t.trim().toLowerCase() : null);
197
+ const open = new Map(q.list().filter((x) => x.status === 'todo' || x.status === 'doing').map((x) => [key(x.title), x.id]));
198
+ const want = Array.isArray(args.items) ? args.items : [args], keep = [], skipped = [];
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
+ 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
+ if (!keep.length && skipped.length) return ok({ added: 0, ...why });
202
+ if (args.items === undefined) return ok({ item: q.add(args) });
203
+ return ok({ items: q.addMany(Array.isArray(args.items) ? keep : args.items), ...why });
204
+ }
194
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() }); }
195
206
  case 'queue_edit': return ok({ item: q.edit(args) });
196
207
  case 'queue_reorder': return ok({ items: q.reorder(args) });
@@ -21,11 +21,11 @@ export const DEFAULTS = {
21
21
  preset: 'full', terminal: true, allowedDirectories: [], readOnlyDirectories: [], protectSensitive: false,
22
22
  confirm: { enabled: false, ...readSettings('').ask, timeoutSeconds: 120 }, // no confirm.ini = the defaults per category
23
23
  recycleBin: true, snapshots: { ...SNAP_DEFAULTS }, fuse: { ...FUSE_DEFAULTS }, keepAwake: true, askSettings: false,
24
- theme: 'system', language: 'en', popupStyle: 'toast', notify: { sound: true, offline: true, queueDone: true, queueStalled: true, limit: true },
24
+ theme: 'system', language: 'en', popupStyle: 'toast', notify: { sound: true, offline: true, queueDone: true, queueStalled: true, settingsChanged: true, limit: true },
25
25
  deviceName: '', autostart: true, localHistory: false, telemetryEnabled: true,
26
26
  ...EXTRA.defaults(),
27
27
  };
28
- const NOTIFY = ['sound', 'offline', 'queueDone', 'queueStalled', 'limit'];
28
+ const NOTIFY = ['sound', 'offline', 'queueDone', 'queueStalled', 'settingsChanged', 'limit'];
29
29
  const dirs = { type: 'array', maxItems: 20, items: { type: 'string' } };
30
30
  export const SETTINGS_TOOL_DEF = {
31
31
  name: 'bridgerator_settings',
package/ui/app.js CHANGED
@@ -334,7 +334,7 @@ P.notifications = () => {
334
334
  return `<main class="notifications">${bannerHtml()}
335
335
  <div class="head"><h1>${esc(t('notify.title'))}</h1><div class="sub">${esc(t('notify.sub'))}</div></div>
336
336
  <div class="styles" role="radiogroup" aria-label="${esc(t('aria.style'))}">${style('toast')}${style('popup')}</div>
337
- <div class="nsplit"><section class="card" aria-label="${esc(t('aria.options'))}">${sw('sound', 'notify.sound')}${sw('offline', 'notify.offline')}${sw('queueDone', 'notify.queueDone')}${sw('queueStalled', 'notify.queueStalled')}${sw('limit', 'notify.limit')}</section>
337
+ <div class="nsplit"><section class="card" aria-label="${esc(t('aria.options'))}">${sw('sound', 'notify.sound')}${sw('offline', 'notify.offline')}${sw('queueDone', 'notify.queueDone')}${sw('queueStalled', 'notify.queueStalled')}${sw('settingsChanged', 'notify.settingsChanged')}${sw('limit', 'notify.limit')}</section>
338
338
  <section class="preview" aria-label="${esc(t('notify.preview'))}"><div class="bar-top"><span class="b">${esc(t('notify.preview'))}</span><div class="seg" role="radiogroup" aria-label="${esc(t('aria.previewTheme'))}">${pv('light')}${pv('dark')}</div></div>
339
339
  <div class="toast ${previewTheme}"><div class="app">${LOGO16}<span class="grow">Bridgerator</span><span>⋯</span></div><div class="h">${esc(t('notify.pHead'))}</div><div class="c">${esc(t('notify.pCmd'))}</div><div class="m">${esc(t('notify.pMeta', { name: state.name || 'PC' }))}</div>
340
340
  <div class="btns"><span>${esc(t('notify.allow'))}</span><span>${esc(t('notify.allow10'))}</span><span>${esc(t('notify.deny'))}</span></div></div>
package/ui/i18n/en.json CHANGED
@@ -276,6 +276,7 @@
276
276
  "sound": "Play a sound",
277
277
  "offline": "When the PC goes offline",
278
278
  "queueDone": "When a queue task is done",
279
+ "settingsChanged": "When the AI or the settings file changes Bridgerator settings",
279
280
  "queueStalled": "When a queue task stalls (ChatGPT may be waiting for your OK or show an error in the chat)",
280
281
  "limit": "Monthly limit warnings",
281
282
  "preview": "Preview",
package/ui/i18n/ru.json CHANGED
@@ -276,6 +276,7 @@
276
276
  "sound": "Звук",
277
277
  "offline": "Когда ПК уходит из сети",
278
278
  "queueDone": "Когда задача из очереди готова",
279
+ "settingsChanged": "Когда ИИ или файл настроек меняет настройки Bridgerator",
279
280
  "queueStalled": "Когда задача из очереди зависла (ChatGPT может ждать вашего «Разрешить» или показать ошибку в чате)",
280
281
  "limit": "Предупреждения о лимите",
281
282
  "preview": "Пример",