bridgerator 1.1.1 → 1.1.2
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 +13 -2
- package/src/notice.mjs +5 -0
- package/src/queue.mjs +13 -5
- package/src/settings-tool.mjs +2 -2
- package/ui/app.js +1 -1
- package/ui/i18n/en.json +3 -1
- package/ui/i18n/ru.json +3 -1
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bridgerator",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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.2",
|
|
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.2",
|
|
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
|
@@ -19,7 +19,7 @@ import { shorten } from './tool-text.mjs';
|
|
|
19
19
|
import { parseDelete, recycle as recycleOnPc, purgeFromBin } from './recycle.mjs';
|
|
20
20
|
import { createSnapshots, changedPaths, undoGroups, SNAP_DEFAULTS, UNDO_TOOL_DEF } from './undo.mjs';
|
|
21
21
|
import { createFuse, wantAwake, FUSE_DEFAULTS } from './guard.mjs';
|
|
22
|
-
import { createQueue, queueTool, QUEUE_TOOL_DEFS, QUEUE_TOOL_NAMES } from './queue.mjs';
|
|
22
|
+
import { createQueue, queueTool, stalledItems, QUEUE_TOOL_DEFS, QUEUE_TOOL_NAMES } from './queue.mjs';
|
|
23
23
|
import { createRecent, outcomeOf } from './recent.mjs';
|
|
24
24
|
import { readSettingsStrict, setIni, writeAtomic, CATEGORIES, findMatch, clip } from './confirm.mjs';
|
|
25
25
|
import { readPlan, planBanner, bannerVisible, dismissFor, safeUrl, forecast, setSite, accountUrl } from './plan.mjs';
|
|
@@ -374,6 +374,14 @@ export async function startDesktopCommander(cfg, log = console.log, { ask: askAr
|
|
|
374
374
|
// --- Recycle Bin, mass-action fuse, Undo (LOGIC: critical-logic/bridge.md) ---------------------
|
|
375
375
|
const fuse = createFuse();
|
|
376
376
|
let fuseAsking = false, inFlight = 0, lastCall = 0, wasPaused = false;
|
|
377
|
+
// One notice per stalled "doing" item (id + updatedAt), on unless the person turned it off.
|
|
378
|
+
const stalledWarned = new Set(), started = Date.now();
|
|
379
|
+
if (queue) setInterval(() => {
|
|
380
|
+
if (cfg.notify?.queueStalled === false || inFlight) return;
|
|
381
|
+
const fresh = stalledItems(queue.list('doing'), { now: Date.now(), idleSince: Math.max(lastCall, started) }).filter((it) => !stalledWarned.has(`${it.id}@${it.updatedAt}`));
|
|
382
|
+
for (const it of fresh) stalledWarned.add(`${it.id}@${it.updatedAt}`);
|
|
383
|
+
if (fresh.length) showNotice({ kind: 'queue_stalled', title: fresh[0].title, more: fresh.length - 1 }, log); // one notice per check, not a burst
|
|
384
|
+
}, 60000).unref();
|
|
377
385
|
const fuseCfg = () => ({ ...FUSE_DEFAULTS, ...(cfg.fuse || {}) });
|
|
378
386
|
const setPaused = (v) => { if (out.setPaused) out.setPaused(v); else { out.paused = v; out.onMode(mode()); } };
|
|
379
387
|
const currentTask = () => { const d = queue ? queue.list('doing') : []; return d.length === 1 ? { id: d[0].id, title: d[0].title } : undefined; };
|
|
@@ -495,7 +503,7 @@ export async function startDesktopCommander(cfg, log = console.log, { ask: askAr
|
|
|
495
503
|
const bannerNow = () => { const b = planBanner(out.plan); return { ...b, visible: bannerVisible(b, cfg.planBannerDismissed) }; };
|
|
496
504
|
out.banner = bannerNow;
|
|
497
505
|
out.onPlanMsg = (m) => { out.plan = readPlan(m); out.onPlan?.(); };
|
|
498
|
-
const BOOL_NOTIFY = ['sound', 'offline', 'queueDone', 'limit'];
|
|
506
|
+
const BOOL_NOTIFY = ['sound', 'offline', 'queueDone', 'queueStalled', 'limit'];
|
|
499
507
|
|
|
500
508
|
// Requests of the Bridgerator window (tray helper). Made by the person at the PC: no extra prompt.
|
|
501
509
|
async function rpc(method, p = {}, paths = []) {
|
|
@@ -690,6 +698,9 @@ export function cleanEnv(env = process.env) {
|
|
|
690
698
|
export function engineEnv(env = process.env) {
|
|
691
699
|
const e = cleanEnv(env);
|
|
692
700
|
for (const k of Object.keys(e)) if (/^(BRIDGERATOR_|BR_)/i.test(k)) delete e[k];
|
|
701
|
+
// The terminal's PowerShell may run .ps1 (npm.ps1, npx.ps1…): Windows' default "Restricted" blocks them (26.10, a user's
|
|
702
|
+
// PC). Process scope only — the PC's own policy is never changed, a Group Policy still wins, a value the person set stays.
|
|
703
|
+
if (!Object.keys(e).some((k) => k.toLowerCase() === 'psexecutionpolicypreference')) e.PSExecutionPolicyPreference = 'Bypass';
|
|
693
704
|
return e;
|
|
694
705
|
}
|
|
695
706
|
export function engineCwd(cfg, home = os.homedir()) {
|
package/src/notice.mjs
CHANGED
|
@@ -30,6 +30,11 @@ export function noticeText(n) {
|
|
|
30
30
|
const title = typeof n.title === 'string' ? n.title.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').trim().slice(0, 120) : '';
|
|
31
31
|
if (!title) return undefined;
|
|
32
32
|
t = ['Queue task done', title];
|
|
33
|
+
} else if (kind === 'queue_stalled') {
|
|
34
|
+
const title = typeof n.title === 'string' ? n.title.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').trim().slice(0, 120) : '';
|
|
35
|
+
if (!title) return undefined;
|
|
36
|
+
const more = int(n.more) || 0;
|
|
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.`];
|
|
33
38
|
} else if (kind === 'protection_off') {
|
|
34
39
|
// Internal list only (the bridge's own labels), never relay text.
|
|
35
40
|
const ok = new Set(['confirmations', 'your own patterns', ...Object.values(LABEL).map((l) => `confirmations for ${l}`)]);
|
package/src/queue.mjs
CHANGED
|
@@ -9,7 +9,11 @@ export const STATUSES = ['todo', 'doing', 'done', 'failed', 'skipped'];
|
|
|
9
9
|
const MAX_ITEMS = 500, MAX_TITLE = 200, MAX_DETAILS = 4000, MAX_NOTE = 1000, MAX_PATHS = 20;
|
|
10
10
|
const RULE = 'The usual loop: queue_next, do the task, queue_update with done / failed / skipped and a short note, then the next one until the queue is empty or the person stops. resultPath set: write the result to that file. from set: another AI added it and reads queue_list.';
|
|
11
11
|
const MAX_FROM = 80, MAX_RESULT_PATH = 1000;
|
|
12
|
-
export const STALE_MS = 30 * 60 * 1000;
|
|
12
|
+
export const STALE_MS = 30 * 60 * 1000;
|
|
13
|
+
export const STALLED_MS = 10 * 60 * 1000; // "doing" + no tool call at all for this long: ChatGPT is likely waiting for the person
|
|
14
|
+
/** "doing" items with no update and no tool call on this PC for `ms` (owner 26.10: ChatGPT's own Allow dialog is invisible on the PC). */
|
|
15
|
+
// ponytail: one PC-wide idle clock — a second chat busy with other calls hides a stalled one; per-chat tracking would need the worker on every call.
|
|
16
|
+
export const stalledItems = (list, { now, idleSince, ms = STALLED_MS }) => (now - idleSince < ms ? [] : list.filter((x) => x.status === 'doing' && now - Date.parse(x.updatedAt) >= ms)); // "doing" with no update for this long: another chat gave up
|
|
13
17
|
const ID = { type: 'string', description: 'item id' };
|
|
14
18
|
const STR = (n) => ({ type: 'string', maxLength: n });
|
|
15
19
|
const PATHS = { type: 'array', maxItems: MAX_PATHS, items: STR(1000) };
|
|
@@ -18,7 +22,7 @@ const HANDOFF = { from: { type: 'string', maxLength: MAX_FROM, description: 'Who
|
|
|
18
22
|
|
|
19
23
|
export const QUEUE_TOOL_DEFS = [
|
|
20
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 } } } },
|
|
21
|
-
{ 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) } } },
|
|
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' } } } },
|
|
22
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) } } },
|
|
23
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 } } } } } },
|
|
24
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'] } } } },
|
|
@@ -99,11 +103,15 @@ export function createQueue(file, { now = () => new Date().toISOString(), onChan
|
|
|
99
103
|
items.push(it); save(); return it;
|
|
100
104
|
},
|
|
101
105
|
// Synchronous take: parallel chats calling queue_next never get the same item.
|
|
102
|
-
|
|
106
|
+
// id: that item only (a chat told to take certain tasks), if it is "todo" or stale "doing"; else an error.
|
|
107
|
+
next({ worker, id } = {}) { return commit(() => {
|
|
103
108
|
const w = str(worker, 80, 'worker');
|
|
104
109
|
const t = Date.parse(now());
|
|
105
|
-
const
|
|
106
|
-
const
|
|
110
|
+
const isStale = (x) => x.status === 'doing' && t - Date.parse(x.updatedAt) >= STALE_MS;
|
|
111
|
+
const pick = id === undefined ? null : get(str(id, 40, 'id', true));
|
|
112
|
+
if (pick && pick.status !== 'todo' && !isStale(pick)) throw new Error(`item ${pick.id} is "${pick.status}", not free to take`);
|
|
113
|
+
const stale = pick ? (isStale(pick) ? pick : null) : sorted().find(isStale);
|
|
114
|
+
const it = stale || pick || sorted().find((x) => x.status === 'todo');
|
|
107
115
|
if (!it) return null;
|
|
108
116
|
if (stale) it.resumedFrom = it.worker || 'another chat';
|
|
109
117
|
it.status = 'doing'; it.updatedAt = now();
|
package/src/settings-tool.mjs
CHANGED
|
@@ -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, limit: true },
|
|
24
|
+
theme: 'system', language: 'en', popupStyle: 'toast', notify: { sound: true, offline: true, queueDone: true, queueStalled: true, limit: true },
|
|
25
25
|
deviceName: '', autostart: true, localHistory: false, telemetryEnabled: true,
|
|
26
26
|
...EXTRA.defaults(),
|
|
27
27
|
};
|
|
28
|
-
const NOTIFY = ['sound', 'offline', 'queueDone', 'limit'];
|
|
28
|
+
const NOTIFY = ['sound', 'offline', 'queueDone', 'queueStalled', '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
|
@@ -331,7 +331,7 @@ P.notifications = () => {
|
|
|
331
331
|
return `<main class="notifications">${bannerHtml()}
|
|
332
332
|
<div class="head"><h1>${esc(t('notify.title'))}</h1><div class="sub">${esc(t('notify.sub'))}</div></div>
|
|
333
333
|
<div class="styles" role="radiogroup" aria-label="${esc(t('aria.style'))}">${style('toast')}${style('popup')}</div>
|
|
334
|
-
<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('limit', 'notify.limit')}</section>
|
|
334
|
+
<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>
|
|
335
335
|
<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>
|
|
336
336
|
<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>
|
|
337
337
|
<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
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"sound": "Play a sound",
|
|
275
275
|
"offline": "When the PC goes offline",
|
|
276
276
|
"queueDone": "When a queue task is done",
|
|
277
|
+
"queueStalled": "When a queue task stalls (ChatGPT may be waiting for your OK in the chat)",
|
|
277
278
|
"limit": "Monthly limit warnings",
|
|
278
279
|
"preview": "Preview",
|
|
279
280
|
"light": "Light",
|
|
@@ -364,7 +365,8 @@
|
|
|
364
365
|
"Add tasks here, or ask ChatGPT to plan the work and add the steps itself.",
|
|
365
366
|
"Attach files or folders to a task so ChatGPT knows where to look.",
|
|
366
367
|
"In ChatGPT, say “Work through my Bridgerator queue”. Watch the ticks appear here.",
|
|
367
|
-
"Edit, reorder or delete tasks any time. A failed task shows why it failed."
|
|
368
|
+
"Edit, reorder or delete tasks any time. A failed task shows why it failed.",
|
|
369
|
+
"ChatGPT may ask you to allow an action in the chat; the task waits for your answer. If a task stalls, Bridgerator shows a notice."
|
|
368
370
|
],
|
|
369
371
|
"tipLabel": "say",
|
|
370
372
|
"tip": "Work through my Bridgerator queue"
|
package/ui/i18n/ru.json
CHANGED
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"sound": "Звук",
|
|
275
275
|
"offline": "Когда ПК уходит из сети",
|
|
276
276
|
"queueDone": "Когда задача из очереди готова",
|
|
277
|
+
"queueStalled": "Когда задача из очереди зависла (ChatGPT может ждать вашего «Разрешить» в чате)",
|
|
277
278
|
"limit": "Предупреждения о лимите",
|
|
278
279
|
"preview": "Пример",
|
|
279
280
|
"light": "Светлая",
|
|
@@ -364,7 +365,8 @@
|
|
|
364
365
|
"Добавляйте задачи здесь или попросите ChatGPT спланировать работу и добавить шаги самому.",
|
|
365
366
|
"Прикрепите к задаче файлы или папки, чтобы ChatGPT знал, где искать.",
|
|
366
367
|
"Скажите в ChatGPT «Work through my Bridgerator queue» и смотрите, как появляются галочки.",
|
|
367
|
-
"Меняйте, переставляйте и удаляйте задачи когда угодно. У невыполненной задачи видно, почему."
|
|
368
|
+
"Меняйте, переставляйте и удаляйте задачи когда угодно. У невыполненной задачи видно, почему.",
|
|
369
|
+
"ChatGPT может попросить в чате разрешить действие — задача ждёт вашего ответа. Если задача зависла, Bridgerator покажет уведомление."
|
|
368
370
|
],
|
|
369
371
|
"tipLabel": "say",
|
|
370
372
|
"tip": "Work through my Bridgerator queue"
|