bridgerator 1.1.10 → 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.
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/bridge.mjs +15 -8
- package/src/cli.mjs +4 -3
- package/src/notice.mjs +5 -2
- package/src/queue.mjs +13 -2
- package/src/settings-tool.mjs +1 -0
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bridgerator",
|
|
3
|
-
"version": "1.1.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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') {
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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' ?
|
|
342
|
-
export function osLanguage(locales = [Intl.DateTimeFormat().resolvedOptions().locale, process.env.LANG,
|
|
343
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
@@ -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':
|
|
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 == null) 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) });
|
package/src/settings-tool.mjs
CHANGED
|
@@ -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;
|