@jobshimo/browser-link 0.18.3 → 0.19.0
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/dist/bridge/ipc-client.d.ts +17 -0
- package/dist/bridge/ipc-client.js +42 -0
- package/dist/bridge/ipc-client.js.map +1 -1
- package/dist/bridge/protocol.d.ts +28 -1
- package/dist/bridge/protocol.js +17 -0
- package/dist/bridge/protocol.js.map +1 -1
- package/dist/bridge/server.d.ts +13 -0
- package/dist/bridge/server.js +10 -0
- package/dist/bridge/server.js.map +1 -1
- package/dist/bridge/ws-bridge.d.ts +32 -0
- package/dist/bridge/ws-bridge.js +52 -1
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/cli.js +23 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +158 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -1
- package/dist/extension/background.js +126 -29
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/idle-policy.d.ts +106 -0
- package/dist/extension/idle-policy.js +132 -0
- package/dist/extension/idle-policy.js.map +1 -0
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/popup.d.ts +1 -38
- package/dist/extension/popup.html +45 -0
- package/dist/extension/popup.js +87 -5
- package/dist/extension/popup.js.map +1 -1
- package/dist/messages.d.ts +26 -1
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { MAX_IDLE_TTL_MINUTES, MIN_IDLE_TTL_MINUTES, clampIdleTtlMinutes, loadConfig, saveConfig, } from '../config.js';
|
|
2
|
+
import { HandshakeError, IpcClient } from '../bridge/ipc-client.js';
|
|
3
|
+
import { readToken } from '../bridge/token.js';
|
|
4
|
+
const CFG_I18N = {
|
|
5
|
+
en: {
|
|
6
|
+
header: 'browser-link config',
|
|
7
|
+
idleTtlLabel: 'idle-ttl',
|
|
8
|
+
never: 'never (auto-disconnect disabled)',
|
|
9
|
+
minutes: (n) => `${n} min`,
|
|
10
|
+
notSetViaCli: "not set via CLI — the extension's own default/popup choice applies (30 min unless changed in the popup)",
|
|
11
|
+
invalidValue: (raw) => `Invalid idle-ttl value: "${raw}". Use a whole number of minutes (${MIN_IDLE_TTL_MINUTES}-${MAX_IDLE_TTL_MINUTES}) or "never".`,
|
|
12
|
+
clampedNote: (original, clamped) => ` (clamped from ${original} to ${clamped})`,
|
|
13
|
+
setSaved: (label) => `Idle-disconnect TTL set to ${label}.`,
|
|
14
|
+
pushedLive: (n) => ` Pushed immediately to ${n} connected tab(s).`,
|
|
15
|
+
pushedNone: ' No tabs are currently connected — it will apply the next time one connects.',
|
|
16
|
+
pushUnavailable: ' Could not reach a running browser-link primary (not running, or multi-agent mode is off) — it will apply the next time a tab connects.',
|
|
17
|
+
pushRejected: ' A running primary rejected the push (stale or mismatched multi-agent token — it may have just restarted). The value is saved and applies the next time a tab connects.',
|
|
18
|
+
usage: 'Usage: browser-link config get [idle-ttl] | browser-link config set idle-ttl <minutes|never>',
|
|
19
|
+
unknownKey: (key) => `Unknown config key: ${key}. Known keys: idle-ttl.`,
|
|
20
|
+
unknownAction: (action) => `Unknown config action: ${action}. Use get or set.`,
|
|
21
|
+
},
|
|
22
|
+
es: {
|
|
23
|
+
header: 'Configuración de browser-link',
|
|
24
|
+
idleTtlLabel: 'idle-ttl',
|
|
25
|
+
never: 'nunca (auto-desconexión deshabilitada)',
|
|
26
|
+
minutes: (n) => `${n} min`,
|
|
27
|
+
notSetViaCli: 'no configurado por CLI — se aplica el valor por defecto o el elegido en el popup de la extensión (30 min salvo que se haya cambiado en el popup)',
|
|
28
|
+
invalidValue: (raw) => `Valor de idle-ttl inválido: "${raw}". Usá un número entero de minutos (${MIN_IDLE_TTL_MINUTES}-${MAX_IDLE_TTL_MINUTES}) o "never".`,
|
|
29
|
+
clampedNote: (original, clamped) => ` (ajustado de ${original} a ${clamped})`,
|
|
30
|
+
setSaved: (label) => `TTL de auto-desconexión configurado a ${label}.`,
|
|
31
|
+
pushedLive: (n) => ` Aplicado de inmediato a ${n} pestaña(s) conectada(s).`,
|
|
32
|
+
pushedNone: ' No hay pestañas conectadas actualmente — se aplicará la próxima vez que una se conecte.',
|
|
33
|
+
pushUnavailable: ' No se pudo contactar a un browser-link primary en ejecución (no está corriendo, o el modo multi-agente está apagado) — se aplicará la próxima vez que una pestaña se conecte.',
|
|
34
|
+
pushRejected: ' Un primary en ejecución rechazó el push (token multi-agente obsoleto o no coincidente — puede haberse reiniciado recién). El valor quedó guardado y se aplicará la próxima vez que una pestaña se conecte.',
|
|
35
|
+
usage: 'Uso: browser-link config get [idle-ttl] | browser-link config set idle-ttl <minutos|never>',
|
|
36
|
+
unknownKey: (key) => `Clave de configuración desconocida: ${key}. Claves conocidas: idle-ttl.`,
|
|
37
|
+
unknownAction: (action) => `Acción de config desconocida: ${action}. Usá get o set.`,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/** One formatted line describing the current `idleTtlMinutes` state, shared
|
|
41
|
+
* by both `config get` (no key — lists everything) and `config get idle-ttl`
|
|
42
|
+
* (just this one). */
|
|
43
|
+
export function getIdleTtlLine(language = 'en') {
|
|
44
|
+
const t = CFG_I18N[language];
|
|
45
|
+
const cfg = loadConfig();
|
|
46
|
+
const value = cfg.idleTtlMinutes === undefined
|
|
47
|
+
? t.notSetViaCli
|
|
48
|
+
: cfg.idleTtlMinutes === 0
|
|
49
|
+
? t.never
|
|
50
|
+
: t.minutes(cfg.idleTtlMinutes);
|
|
51
|
+
return ` ${t.idleTtlLabel} ${value}`;
|
|
52
|
+
}
|
|
53
|
+
export function listConfig(language = 'en') {
|
|
54
|
+
const t = CFG_I18N[language];
|
|
55
|
+
return [t.header, '', getIdleTtlLine(language)].join('\n');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse the CLI's raw `<minutes|never>` argument.
|
|
59
|
+
*
|
|
60
|
+
* Distinct from `clampIdleTtlMinutes` (config.ts) on purpose: that function
|
|
61
|
+
* is a defensive safety net for VALUES THE USER NEVER TYPED (a corrupted
|
|
62
|
+
* config.json, a hand-edited file) — for those, silently falling back to
|
|
63
|
+
* the 30-minute default is the safer failure mode, since there's no one to
|
|
64
|
+
* tell. Here the user just typed something on a terminal RIGHT NOW: a
|
|
65
|
+
* genuinely unparsable value (letters, decimals) gets a clear error instead
|
|
66
|
+
* of a silent substitution, and an out-of-range integer gets clamped to the
|
|
67
|
+
* boundary WITH a note explaining what happened — both are more honest
|
|
68
|
+
* feedback loops for an interactive command than defaulting silently.
|
|
69
|
+
*/
|
|
70
|
+
function parseIdleTtlArg(raw, t) {
|
|
71
|
+
const normalized = raw.trim().toLowerCase();
|
|
72
|
+
if (normalized === 'never' || normalized === '0')
|
|
73
|
+
return { minutes: 0, note: '' };
|
|
74
|
+
const parsed = Number(normalized);
|
|
75
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
76
|
+
throw new Error(t.invalidValue(raw));
|
|
77
|
+
}
|
|
78
|
+
if (parsed < MIN_IDLE_TTL_MINUTES || parsed > MAX_IDLE_TTL_MINUTES) {
|
|
79
|
+
const clamped = Math.min(Math.max(parsed, MIN_IDLE_TTL_MINUTES), MAX_IDLE_TTL_MINUTES);
|
|
80
|
+
return { minutes: clamped, note: t.clampedNote(parsed, clamped) };
|
|
81
|
+
}
|
|
82
|
+
return { minutes: parsed, note: '' };
|
|
83
|
+
}
|
|
84
|
+
/** Best-effort live push to an already-running primary over the
|
|
85
|
+
* multi-agent IPC bridge. Never throws — every failure degrades to a
|
|
86
|
+
* message. Failures are NOT collapsed into one diagnosis: an explicit
|
|
87
|
+
* hello-reject (stale/mismatched token, version mismatch) proves a primary
|
|
88
|
+
* IS running and gets `pushRejected`, while connection-refused / timeout /
|
|
89
|
+
* missing-token-file — where "no primary, or multi-agent off" is the
|
|
90
|
+
* honest reading — get `pushUnavailable`. */
|
|
91
|
+
async function tryPushLive(settings, t, ipcOptions = {}) {
|
|
92
|
+
const token = readToken();
|
|
93
|
+
if (!token)
|
|
94
|
+
return t.pushUnavailable;
|
|
95
|
+
const client = new IpcClient();
|
|
96
|
+
try {
|
|
97
|
+
await client.connect(token, {
|
|
98
|
+
host: ipcOptions.host,
|
|
99
|
+
port: ipcOptions.port,
|
|
100
|
+
handshakeTimeoutMs: 1500,
|
|
101
|
+
});
|
|
102
|
+
const ack = await client.sendSettingsPush(settings);
|
|
103
|
+
return ack.notified > 0 ? t.pushedLive(ack.notified) : t.pushedNone;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
// HandshakeError message shapes (see ipc-client.ts): "Primary
|
|
107
|
+
// rejected: <reason>" only when a live primary answered with an
|
|
108
|
+
// explicit hello-reject — timeouts and unexpected frames use other
|
|
109
|
+
// wording, and socket-level failures are not HandshakeErrors at all.
|
|
110
|
+
if (err instanceof HandshakeError && /rejected/i.test(err.message)) {
|
|
111
|
+
return t.pushRejected;
|
|
112
|
+
}
|
|
113
|
+
return t.pushUnavailable;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await client.disconnect().catch(() => {
|
|
117
|
+
/* best effort */
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export async function setIdleTtl(rawValue, language = 'en', ipcOptions = {}) {
|
|
122
|
+
const t = CFG_I18N[language];
|
|
123
|
+
// Defensive final pass: parseIdleTtlArg already guarantees an in-range
|
|
124
|
+
// value or 0, so this is a no-op in practice — but it keeps ONE function
|
|
125
|
+
// as the single source of truth for "what counts as a safe stored value",
|
|
126
|
+
// matching the extension side's clampIdleTtlMinutes.
|
|
127
|
+
const { minutes: parsedMinutes, note } = parseIdleTtlArg(rawValue, t);
|
|
128
|
+
const minutes = clampIdleTtlMinutes(parsedMinutes);
|
|
129
|
+
const updatedAt = Date.now();
|
|
130
|
+
saveConfig({ idleTtlMinutes: minutes, idleTtlUpdatedAt: updatedAt });
|
|
131
|
+
const label = (minutes === 0 ? t.never : t.minutes(minutes)) + note;
|
|
132
|
+
const pushMessage = await tryPushLive({ idleTtlMinutes: minutes, updatedAt }, t, ipcOptions);
|
|
133
|
+
return t.setSaved(label) + pushMessage;
|
|
134
|
+
}
|
|
135
|
+
export async function runConfigCommand(argv, language = 'en') {
|
|
136
|
+
const t = CFG_I18N[language];
|
|
137
|
+
const action = argv.at(0);
|
|
138
|
+
const key = argv.at(1);
|
|
139
|
+
const value = argv.at(2);
|
|
140
|
+
if (action === undefined || action === 'get') {
|
|
141
|
+
if (key === undefined)
|
|
142
|
+
return listConfig(language);
|
|
143
|
+
if (key === 'idle-ttl')
|
|
144
|
+
return getIdleTtlLine(language);
|
|
145
|
+
throw new Error(t.unknownKey(key));
|
|
146
|
+
}
|
|
147
|
+
if (action === 'set') {
|
|
148
|
+
if (key === undefined)
|
|
149
|
+
throw new Error(t.usage);
|
|
150
|
+
if (key !== 'idle-ttl')
|
|
151
|
+
throw new Error(t.unknownKey(key));
|
|
152
|
+
if (value === undefined)
|
|
153
|
+
throw new Error(t.usage);
|
|
154
|
+
return setIdleTtl(value, language);
|
|
155
|
+
}
|
|
156
|
+
throw new Error(t.unknownAction(action));
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA+C/C,MAAM,QAAQ,GAAiC;IAC7C,EAAE,EAAE;QACF,MAAM,EAAE,qBAAqB;QAC7B,YAAY,EAAE,UAAU;QACxB,KAAK,EAAE,kCAAkC;QACzC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM;QAC1B,YAAY,EACV,yGAAyG;QAC3G,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CACpB,4BAA4B,GAAG,qCAAqC,oBAAoB,IAAI,oBAAoB,eAAe;QACjI,WAAW,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,kBAAkB,QAAQ,OAAO,OAAO,GAAG;QAC/E,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,8BAA8B,KAAK,GAAG;QAC3D,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,0BAA0B,CAAC,oBAAoB;QAClE,UAAU,EAAE,8EAA8E;QAC1F,eAAe,EACb,yIAAyI;QAC3I,YAAY,EACV,yKAAyK;QAC3K,KAAK,EACH,8FAA8F;QAChG,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,GAAG,yBAAyB;QACxE,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,0BAA0B,MAAM,mBAAmB;KAC/E;IACD,EAAE,EAAE;QACF,MAAM,EAAE,+BAA+B;QACvC,YAAY,EAAE,UAAU;QACxB,KAAK,EAAE,wCAAwC;QAC/C,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM;QAC1B,YAAY,EACV,kJAAkJ;QACpJ,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CACpB,gCAAgC,GAAG,uCAAuC,oBAAoB,IAAI,oBAAoB,cAAc;QACtI,WAAW,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,iBAAiB,QAAQ,MAAM,OAAO,GAAG;QAC7E,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,yCAAyC,KAAK,GAAG;QACtE,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,4BAA4B,CAAC,2BAA2B;QAC3E,UAAU,EACR,0FAA0F;QAC5F,eAAe,EACb,gLAAgL;QAClL,YAAY,EACV,6MAA6M;QAC/M,KAAK,EACH,4FAA4F;QAC9F,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,uCAAuC,GAAG,+BAA+B;QAC9F,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,iCAAiC,MAAM,kBAAkB;KACrF;CACF,CAAC;AAEF;;sBAEsB;AACtB,MAAM,UAAU,cAAc,CAAC,WAAqB,IAAI;IACtD,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,MAAM,KAAK,GACT,GAAG,CAAC,cAAc,KAAK,SAAS;QAC9B,CAAC,CAAC,CAAC,CAAC,YAAY;QAChB,CAAC,CAAC,GAAG,CAAC,cAAc,KAAK,CAAC;YACxB,CAAC,CAAC,CAAC,CAAC,KAAK;YACT,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,OAAO,KAAK,CAAC,CAAC,YAAY,MAAM,KAAK,EAAE,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,WAAqB,IAAI;IAClD,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7B,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,eAAe,CAAC,GAAW,EAAE,CAAa;IACjD,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,UAAU,KAAK,OAAO,IAAI,UAAU,KAAK,GAAG;QAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAClF,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IAClC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,MAAM,GAAG,oBAAoB,IAAI,MAAM,GAAG,oBAAoB,EAAE,CAAC;QACnE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,EAAE,oBAAoB,CAAC,CAAC;QACvF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACpE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AACvC,CAAC;AAYD;;;;;;6CAM6C;AAC7C,KAAK,UAAU,WAAW,CACxB,QAAuD,EACvD,CAAa,EACb,aAA6B,EAAE;IAE/B,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC,eAAe,CAAC;IACrC,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE;YAC1B,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,kBAAkB,EAAE,IAAI;SACzB,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,8DAA8D;QAC9D,gEAAgE;QAChE,mEAAmE;QACnE,qEAAqE;QACrE,IAAI,GAAG,YAAY,cAAc,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACnE,OAAO,CAAC,CAAC,YAAY,CAAC;QACxB,CAAC;QACD,OAAO,CAAC,CAAC,eAAe,CAAC;IAC3B,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;YACnC,iBAAiB;QACnB,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,QAAgB,EAChB,WAAqB,IAAI,EACzB,aAA6B,EAAE;IAE/B,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7B,uEAAuE;IACvE,yEAAyE;IACzE,0EAA0E;IAC1E,qDAAqD;IACrD,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,mBAAmB,CAAC,aAAa,CAAC,CAAC;IACnD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,UAAU,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC,CAAC;IAErE,MAAM,KAAK,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IACpE,MAAM,WAAW,GAAG,MAAM,WAAW,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC;IAC7F,OAAO,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAc,EAAE,WAAqB,IAAI;IAC9E,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvB,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAEzB,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QAC7C,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC;QACnD,IAAI,GAAG,KAAK,UAAU;YAAE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QACrB,IAAI,GAAG,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAChD,IAAI,GAAG,KAAK,UAAU;YAAE,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3D,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAClD,OAAO,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3C,CAAC"}
|
package/dist/config.d.ts
CHANGED
|
@@ -30,6 +30,44 @@ export interface BrowserLinkConfig {
|
|
|
30
30
|
* the primary does). Has no effect when multiAgent is false.
|
|
31
31
|
*/
|
|
32
32
|
autoReelect?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* CLI-side mirror of the extension's idle-disconnect TTL (see
|
|
35
|
+
* `packages/extension/src/idle-policy.ts`), in minutes. `0` means
|
|
36
|
+
* "never". Set via `browser-link config set idle-ttl <minutes|never>`.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately left `undefined` — NOT defaulted to 30 here — when the
|
|
39
|
+
* CLI has never touched it: the WS bridge only pushes a `settings.update`
|
|
40
|
+
* to the extension when this field is explicitly set, so a popup-only
|
|
41
|
+
* user (who never ran the CLI command) never sees a spurious overwrite
|
|
42
|
+
* of their own choice with a server-side "default". See `idleTtlUpdatedAt`
|
|
43
|
+
* for the precedence rule when both sides have set a value.
|
|
44
|
+
*/
|
|
45
|
+
idleTtlMinutes?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Epoch-ms timestamp of the last `idleTtlMinutes` write from the CLI.
|
|
48
|
+
* Sent alongside `idleTtlMinutes` in every `settings.update` push so the
|
|
49
|
+
* extension can apply a last-write-wins precedence rule against its own
|
|
50
|
+
* locally-stored timestamp (popup edits stamp their own `Date.now()`).
|
|
51
|
+
* Always set together with `idleTtlMinutes` — never one without the other.
|
|
52
|
+
*/
|
|
53
|
+
idleTtlUpdatedAt?: number;
|
|
33
54
|
}
|
|
55
|
+
/** Safety-rail bounds mirrored from the extension's `idle-policy.ts` — kept
|
|
56
|
+
* as an independent copy (not a shared import) for the same reason
|
|
57
|
+
* `messages.ts` duplicates the wire types instead of depending on
|
|
58
|
+
* `@browser-link/shared`: the server publishes to npm standalone and must
|
|
59
|
+
* not carry an unresolvable workspace dependency. */
|
|
60
|
+
export declare const MIN_IDLE_TTL_MINUTES = 1;
|
|
61
|
+
export declare const MAX_IDLE_TTL_MINUTES = 1440;
|
|
62
|
+
export declare const DEFAULT_IDLE_TTL_MINUTES = 30;
|
|
63
|
+
/**
|
|
64
|
+
* Clamp a CLI-provided idle-TTL value the same way the extension clamps a
|
|
65
|
+
* stored one: `0` ("never") passes through untouched; a non-integer, an
|
|
66
|
+
* out-of-range value, or anything malformed falls back to
|
|
67
|
+
* `DEFAULT_IDLE_TTL_MINUTES` rather than being snapped to the nearest
|
|
68
|
+
* boundary. Exported so `commands/config.ts` and its tests share one
|
|
69
|
+
* implementation instead of re-deriving the bounds.
|
|
70
|
+
*/
|
|
71
|
+
export declare function clampIdleTtlMinutes(value: number): number;
|
|
34
72
|
export declare function loadConfig(): BrowserLinkConfig;
|
|
35
73
|
export declare function saveConfig(patch: Partial<BrowserLinkConfig>): BrowserLinkConfig;
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,31 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { getDataDir } from './map/paths.js';
|
|
4
4
|
import { sanitizeDisabledTools } from './permissions.js';
|
|
5
|
+
/** Safety-rail bounds mirrored from the extension's `idle-policy.ts` — kept
|
|
6
|
+
* as an independent copy (not a shared import) for the same reason
|
|
7
|
+
* `messages.ts` duplicates the wire types instead of depending on
|
|
8
|
+
* `@browser-link/shared`: the server publishes to npm standalone and must
|
|
9
|
+
* not carry an unresolvable workspace dependency. */
|
|
10
|
+
export const MIN_IDLE_TTL_MINUTES = 1;
|
|
11
|
+
export const MAX_IDLE_TTL_MINUTES = 1440;
|
|
12
|
+
export const DEFAULT_IDLE_TTL_MINUTES = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Clamp a CLI-provided idle-TTL value the same way the extension clamps a
|
|
15
|
+
* stored one: `0` ("never") passes through untouched; a non-integer, an
|
|
16
|
+
* out-of-range value, or anything malformed falls back to
|
|
17
|
+
* `DEFAULT_IDLE_TTL_MINUTES` rather than being snapped to the nearest
|
|
18
|
+
* boundary. Exported so `commands/config.ts` and its tests share one
|
|
19
|
+
* implementation instead of re-deriving the bounds.
|
|
20
|
+
*/
|
|
21
|
+
export function clampIdleTtlMinutes(value) {
|
|
22
|
+
if (value === 0)
|
|
23
|
+
return 0;
|
|
24
|
+
if (!Number.isFinite(value) || !Number.isInteger(value))
|
|
25
|
+
return DEFAULT_IDLE_TTL_MINUTES;
|
|
26
|
+
if (value < MIN_IDLE_TTL_MINUTES || value > MAX_IDLE_TTL_MINUTES)
|
|
27
|
+
return DEFAULT_IDLE_TTL_MINUTES;
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
5
30
|
/** Defaults applied at load time so consumers can read `cfg.multiAgent`
|
|
6
31
|
* directly. Persisted form (see `normaliseForWrite`) only carries the field
|
|
7
32
|
* when the user overrides the default, keeping the on-disk config minimal. */
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,23 +2,81 @@ import { buildSnapshotJs, buildFindJs, buildClickResolveJs, buildTypeResolveJs,
|
|
|
2
2
|
import { buildKeyEventSequence, resolveKey, modifiersToBitmask, MODIFIER_BITS, } from './keymap.js';
|
|
3
3
|
import { resolveSettleParams, settleSafely } from './settle.js';
|
|
4
4
|
import { runFlow, } from './flow.js';
|
|
5
|
+
import { DEFAULT_IDLE_TTL_MINUTES, IDLE_TTL_STORAGE_KEY, IDLE_TTL_UPDATED_AT_STORAGE_KEY, clampIdleTtlMinutes, parseIncomingSettings, shouldAcceptIncomingSettings, shouldDisconnectForIdle, shouldScheduleIdleSweep, } from './idle-policy.js';
|
|
5
6
|
const WS_URL = 'ws://127.0.0.1:17529';
|
|
6
7
|
const CDP_VERSION = '1.3';
|
|
7
8
|
const CONSOLE_BUFFER_MAX = 200;
|
|
8
9
|
const NETWORK_BUFFER_MAX = 200;
|
|
9
10
|
/* Idle TTL for a tab's WebSocket bridge.
|
|
10
11
|
*
|
|
11
|
-
* After this many
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* re-presses Connect when they want to reactivate
|
|
12
|
+
* After this many minutes without a tool.request from the server, the
|
|
13
|
+
* extension closes its side of the WS and detaches the debugger. The
|
|
14
|
+
* popup then shows "Not connected" again and the user explicitly
|
|
15
|
+
* re-presses Connect when they want to reactivate — unless the user
|
|
16
|
+
* configured "Never" in the popup, in which case the sweep never fires.
|
|
15
17
|
*
|
|
16
18
|
* This replaces the previous behaviour where the bridge implicitly
|
|
17
19
|
* died together with the MCP server subprocess (parent_death_guard,
|
|
18
20
|
* removed in v0.9.0). Lifecycle is now client-side: agent activity
|
|
19
|
-
* keeps the tab warm, silence eventually parks it
|
|
20
|
-
|
|
21
|
+
* keeps the tab warm, silence eventually parks it (or never does, if
|
|
22
|
+
* the user opted out).
|
|
23
|
+
*
|
|
24
|
+
* The TTL itself is user-configurable (`idleTtlMinutes` in
|
|
25
|
+
* `chrome.storage.local`, edited from the popup — see idle-policy.ts for
|
|
26
|
+
* the decision logic and popup.ts for the control). `idleTtlMinutesCache`
|
|
27
|
+
* is the in-memory mirror the sweep reads on every tick: chrome.storage is
|
|
28
|
+
* async, the sweep loop below is not, so the value is loaded once at
|
|
29
|
+
* startup and kept current via `chrome.storage.onChanged` instead of being
|
|
30
|
+
* awaited inside the interval callback. */
|
|
21
31
|
const WS_IDLE_SWEEP_MS = 60 * 1000;
|
|
32
|
+
let idleTtlMinutesCache = DEFAULT_IDLE_TTL_MINUTES;
|
|
33
|
+
async function loadIdleTtlMinutes() {
|
|
34
|
+
try {
|
|
35
|
+
const data = await chrome.storage.local.get(IDLE_TTL_STORAGE_KEY);
|
|
36
|
+
idleTtlMinutesCache = clampIdleTtlMinutes(data[IDLE_TTL_STORAGE_KEY]);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
idleTtlMinutesCache = DEFAULT_IDLE_TTL_MINUTES;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
void loadIdleTtlMinutes();
|
|
43
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
44
|
+
if (areaName !== 'local')
|
|
45
|
+
return;
|
|
46
|
+
if (!(IDLE_TTL_STORAGE_KEY in changes))
|
|
47
|
+
return;
|
|
48
|
+
idleTtlMinutesCache = clampIdleTtlMinutes(changes[IDLE_TTL_STORAGE_KEY].newValue);
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Apply an incoming `settings.update` push from the server (see
|
|
52
|
+
* `handleTool`'s caller — the WS message listener in `connectTab` below).
|
|
53
|
+
* The server sends this both right after `tab.registered` (when
|
|
54
|
+
* `browser-link config set idle-ttl` was run at least once) AND on demand
|
|
55
|
+
* when the CLI pushes a change to an already-connected tab.
|
|
56
|
+
*
|
|
57
|
+
* Applies `shouldAcceptIncomingSettings` (idle-policy.ts) — newest
|
|
58
|
+
* `updatedAt` wins against whatever was last written locally (typically by
|
|
59
|
+
* the popup). Writing through `chrome.storage.local.set` here is enough to
|
|
60
|
+
* take effect: the `onChanged` listener above keeps `idleTtlMinutesCache`
|
|
61
|
+
* current, and the popup re-reads storage on its own refresh interval.
|
|
62
|
+
*/
|
|
63
|
+
async function applyIncomingSettings(settings) {
|
|
64
|
+
try {
|
|
65
|
+
const data = await chrome.storage.local.get(IDLE_TTL_UPDATED_AT_STORAGE_KEY);
|
|
66
|
+
const rawLocalUpdatedAt = data[IDLE_TTL_UPDATED_AT_STORAGE_KEY];
|
|
67
|
+
const localUpdatedAt = typeof rawLocalUpdatedAt === 'number' ? rawLocalUpdatedAt : undefined;
|
|
68
|
+
if (!shouldAcceptIncomingSettings(localUpdatedAt, settings.updatedAt))
|
|
69
|
+
return;
|
|
70
|
+
await chrome.storage.local.set({
|
|
71
|
+
[IDLE_TTL_STORAGE_KEY]: clampIdleTtlMinutes(settings.idleTtlMinutes),
|
|
72
|
+
[IDLE_TTL_UPDATED_AT_STORAGE_KEY]: settings.updatedAt,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Best effort — a failed sync just leaves the local value as-is; the
|
|
77
|
+
// next settings.update (or a popup edit) can still apply it.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
22
80
|
function isRuntimeMessage(msg) {
|
|
23
81
|
if (typeof msg !== 'object' || msg === null)
|
|
24
82
|
return false;
|
|
@@ -1436,27 +1494,56 @@ async function connectTab(tabId) {
|
|
|
1436
1494
|
const msg = safeParse(typeof ev.data === 'string' ? ev.data : '');
|
|
1437
1495
|
if (!msg)
|
|
1438
1496
|
return;
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1497
|
+
// Exhaustive routing over ServerToExtension. The `default` branch
|
|
1498
|
+
// pins `msg` to `never`, so adding a fourth message kind to the
|
|
1499
|
+
// union fails to COMPILE here instead of silently misrouting the
|
|
1500
|
+
// new kind into the tool.request handler.
|
|
1501
|
+
switch (msg.kind) {
|
|
1502
|
+
case 'tab.registered': {
|
|
1503
|
+
state.serverTabId = msg.payload.tabId;
|
|
1504
|
+
// serverVersion is optional on the wire so older servers that
|
|
1505
|
+
// predate the field still parse — narrow on `typeof string`.
|
|
1506
|
+
if (typeof msg.payload.serverVersion === 'string') {
|
|
1507
|
+
state.serverVersion = msg.payload.serverVersion;
|
|
1508
|
+
}
|
|
1509
|
+
// Remember this id so the next reconnect (after a primary swap)
|
|
1510
|
+
// asks the new primary to honour it. The primary emits
|
|
1511
|
+
// `tab-renamed` in the bridge event log if it can't.
|
|
1512
|
+
void storeTabId(tabId, msg.payload.tabId);
|
|
1513
|
+
settle({ ok: true, serverTabId: msg.payload.tabId });
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
case 'settings.update': {
|
|
1517
|
+
// Housekeeping, not agent activity — deliberately does NOT touch
|
|
1518
|
+
// state.lastActivityAt. Counting a server-pushed settings sync as
|
|
1519
|
+
// "activity" would let it silently keep an otherwise-idle tab
|
|
1520
|
+
// connected forever, defeating the idle TTL it is updating.
|
|
1521
|
+
//
|
|
1522
|
+
// `safeParse` is only a cast — validate the payload shape at the
|
|
1523
|
+
// wire boundary (same rigor the IPC settings.push frame gets in
|
|
1524
|
+
// the server's protocol.ts) so a malformed push is dropped here
|
|
1525
|
+
// instead of throwing inside the apply path.
|
|
1526
|
+
const settings = parseIncomingSettings(msg.settings);
|
|
1527
|
+
if (settings)
|
|
1528
|
+
void applyIncomingSettings(settings);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
case 'tool.request': {
|
|
1532
|
+
// Touch the activity timestamp so the WS-idle sweeper keeps
|
|
1533
|
+
// this tab warm.
|
|
1534
|
+
state.lastActivityAt = Date.now();
|
|
1535
|
+
const response = await handleTool(state, msg);
|
|
1536
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
1537
|
+
send(ws, response);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
default: {
|
|
1541
|
+
// Compile-time exhaustiveness check; at runtime an unknown kind
|
|
1542
|
+
// (newer server, older extension) is ignored defensively.
|
|
1543
|
+
const _exhaustive = msg;
|
|
1544
|
+
return;
|
|
1445
1545
|
}
|
|
1446
|
-
// Remember this id so the next reconnect (after a primary swap)
|
|
1447
|
-
// asks the new primary to honour it. The primary emits
|
|
1448
|
-
// `tab-renamed` in the bridge event log if it can't.
|
|
1449
|
-
void storeTabId(tabId, msg.payload.tabId);
|
|
1450
|
-
settle({ ok: true, serverTabId: msg.payload.tabId });
|
|
1451
|
-
return;
|
|
1452
1546
|
}
|
|
1453
|
-
// ServerToExtension is the union { tab.registered | tool.request },
|
|
1454
|
-
// so by elimination this branch handles the tool.request case. Touch
|
|
1455
|
-
// the activity timestamp so the WS-idle sweeper keeps this tab warm.
|
|
1456
|
-
state.lastActivityAt = Date.now();
|
|
1457
|
-
const response = await handleTool(state, msg);
|
|
1458
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
1459
|
-
send(ws, response);
|
|
1460
1547
|
})();
|
|
1461
1548
|
});
|
|
1462
1549
|
ws.addEventListener('error', () => {
|
|
@@ -1651,17 +1738,27 @@ chrome.debugger.onDetach.addListener((source) => {
|
|
|
1651
1738
|
/* WS-idle sweeper.
|
|
1652
1739
|
*
|
|
1653
1740
|
* Every WS_IDLE_SWEEP_MS, walk every connected tab. Any tab whose last
|
|
1654
|
-
* tool.request landed more than
|
|
1655
|
-
* the WS closes, the debugger detaches, the popup
|
|
1656
|
-
* connected". The user explicitly re-presses Connect to
|
|
1741
|
+
* tool.request landed more than the user's configured `idleTtlMinutes`
|
|
1742
|
+
* ago gets disconnected: the WS closes, the debugger detaches, the popup
|
|
1743
|
+
* goes back to "Not connected". The user explicitly re-presses Connect to
|
|
1744
|
+
* bring it back.
|
|
1745
|
+
*
|
|
1746
|
+
* When the user picked "Never" (idleTtlMinutesCache === 0),
|
|
1747
|
+
* `shouldScheduleIdleSweep` short-circuits the tick before it walks
|
|
1748
|
+
* `tabStates` — the interval itself keeps ticking (cancelling/rearming a
|
|
1749
|
+
* service-worker timer from a storage listener is more moving parts than
|
|
1750
|
+
* it is worth for a once-a-minute no-op), but each tick does zero work
|
|
1751
|
+
* beyond that one check.
|
|
1657
1752
|
*
|
|
1658
1753
|
* This is the replacement for the parent_death_guard that used to kill
|
|
1659
1754
|
* the entire MCP server on stdio close — now the server stays alive and
|
|
1660
1755
|
* the bridge is parked tab-by-tab from the client side. */
|
|
1661
1756
|
setInterval(() => {
|
|
1757
|
+
if (!shouldScheduleIdleSweep(idleTtlMinutesCache))
|
|
1758
|
+
return;
|
|
1662
1759
|
const now = Date.now();
|
|
1663
1760
|
for (const [tabId, state] of tabStates) {
|
|
1664
|
-
if (
|
|
1761
|
+
if (!shouldDisconnectForIdle(state.lastActivityAt, now, idleTtlMinutesCache))
|
|
1665
1762
|
continue;
|
|
1666
1763
|
void cleanup(tabId).catch(() => {
|
|
1667
1764
|
// Best effort.
|