chrome-bridge-mcp 1.30.0 → 1.30.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.
@@ -25,6 +25,7 @@
25
25
  max: 0,
26
26
  items: [],
27
27
  blobs: new Map(),
28
+ names: new Map(),
28
29
  seq: 0,
29
30
  };
30
31
  Object.defineProperty(window, '__cbCapture', { value: st, configurable: true });
@@ -65,6 +66,16 @@
65
66
  return url;
66
67
  };
67
68
 
69
+ // a[download] su un URL http(s): lo scarica Chrome, e il service worker lo
70
+ // annulla a onCreated, quando Chrome non ha ancora scelto il nome. Qui si
71
+ // segna il nome dell'attributo, che il service worker chiede per quell'URL.
72
+ const noteName = (a) => {
73
+ if (st.armed && a instanceof HTMLAnchorElement && a.hasAttribute('download') && /^https?:/i.test(a.href || '')) {
74
+ const n = a.getAttribute('download');
75
+ if (n) st.names.set(a.href, n);
76
+ }
77
+ };
78
+
68
79
  const capturable = (a) => a instanceof HTMLAnchorElement && a.hasAttribute('download') && /^(blob|data):/i.test(a.href || '');
69
80
  const take = (a) => fromUrl(a.href, a.getAttribute('download') || '', a.href.startsWith('data:') ? 'data_url' : 'blob');
70
81
 
@@ -74,6 +85,7 @@
74
85
  window.addEventListener('click', (e) => {
75
86
  if (!st.armed || e.defaultPrevented || handled.has(e)) return;
76
87
  const a = e.composedPath().find((n) => n instanceof HTMLAnchorElement);
88
+ noteName(a);
77
89
  if (!capturable(a)) return;
78
90
  handled.add(e);
79
91
  e.preventDefault();
@@ -87,7 +99,10 @@
87
99
  // quelli della pagina, annulla l'azione nativa se la pagina non l'ha fatto.
88
100
  const origClick = HTMLAnchorElement.prototype.click;
89
101
  HTMLAnchorElement.prototype.click = function click() {
90
- if (!st.armed || !capturable(this)) return origClick.apply(this, arguments);
102
+ if (!st.armed || !capturable(this)) {
103
+ noteName(this);
104
+ return origClick.apply(this, arguments);
105
+ }
91
106
  const ev = new MouseEvent('click', { bubbles: true, cancelable: true, composed: true, view: window });
92
107
  let wanted = false;
93
108
  const last = (e) => {
@@ -202,5 +217,5 @@
202
217
  };
203
218
  st.drop = (id) => { st.items = st.items.filter((x) => x.id !== id); };
204
219
  st.arm = (max) => { st.armed = true; st.max = max; };
205
- st.disarm = () => { st.armed = false; st.items = []; st.blobs.clear(); };
220
+ st.disarm = () => { st.armed = false; st.items = []; st.blobs.clear(); st.names.clear(); };
206
221
  })();
@@ -3,7 +3,10 @@
3
3
  * del server (server/capture.js, che decide il nome sul disco).
4
4
  *
5
5
  * Ordine: filename* di Content-Disposition (RFC 5987, UTF-8 percent-encoded),
6
- * filename, il nome che Chrome aveva già scelto, l'ultimo segmento dell'URL.
6
+ * filename, l'attributo download dell'ancora cliccata (come in Chrome, dopo
7
+ * Content-Disposition), il nome che Chrome aveva già scelto, l'ultimo segmento
8
+ * dell'URL. A onCreated Chrome di solito non ha ancora scelto il nome: senza
9
+ * l'attributo, <a href="/" download="x.html"> arrivava come download.html.
7
10
  */
8
11
  export function dispositionName(header) {
9
12
  const h = String(header || '');
@@ -27,6 +30,6 @@ export function nameFromUrl(url) {
27
30
  }
28
31
  }
29
32
 
30
- export function downloadName({ disposition, chromeName, url }) {
31
- return dispositionName(disposition) || String(chromeName || '').split(/[\\/]/).pop() || nameFromUrl(url) || '';
33
+ export function downloadName({ disposition, chromeName, pageName, url }) {
34
+ return dispositionName(disposition) || String(pageName || '') || String(chromeName || '').split(/[\\/]/).pop() || nameFromUrl(url) || '';
32
35
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Chrome Bridge for Claude Code",
4
- "version": "1.30.0",
4
+ "version": "1.30.2",
5
5
  "description": "Bridge your browser to Claude Code: 67 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
6
6
  "homepage_url": "https://github.com/frsorrentino/chrome-bridge",
7
7
  "minimum_chrome_version": "135",
@@ -1507,14 +1507,24 @@ function recordTabEnding(tabId, ending) {
1507
1507
  if (tabEndings.size > TAB_ENDINGS_MAX) tabEndings.delete(tabEndings.keys().next().value);
1508
1508
  }
1509
1509
 
1510
+ const CLOSE_WAIT_MS = 5000;
1511
+
1510
1512
  async function cmdTabAction({ action, tab_id, bypass_cache = false }) {
1511
1513
  if (!action) throw new Error('Missing required parameter: action');
1512
1514
  const tabId = await resolveTabId(tab_id);
1513
1515
 
1514
1516
  if (action === 'close') {
1517
+ // chrome.tabs.remove aspetta che la pagina lasci andare: con un dialogo
1518
+ // «Vuoi uscire?» aperto o la pagina bloccata in un ciclo resterebbe muta
1519
+ // fino al timeout del comando (30 s, visto il 15/10). Dopo 5 s si risponde
1520
+ // con un errore; la chiusura resta in corso e Chrome la completa quando
1521
+ // la pagina cede (get_tabs la riporta come closed_by_bridge).
1515
1522
  bridgeClosing.add(tabId);
1516
- try { await chrome.tabs.remove(tabId); } catch (e) { bridgeClosing.delete(tabId); throw e; }
1517
- return { action, closed: tabId };
1523
+ const removing = chrome.tabs.remove(tabId).then(() => true, (e) => { bridgeClosing.delete(tabId); throw e; });
1524
+ const done = await Promise.race([removing, new Promise((r) => setTimeout(() => r(false), CLOSE_WAIT_MS))]);
1525
+ if (done) return { action, closed: tabId };
1526
+ removing.catch(() => {});
1527
+ throw new Error(`Tab ${tabId} did not close within ${CLOSE_WAIT_MS / 1000} s: the page is not answering, either a "Leave site?" dialog is open (beforeunload) or the page is stuck in a loop. The close is still pending and completes if the page lets go. Look at the tab: answer the dialog, or close it by hand.`);
1518
1528
  }
1519
1529
  if (action === 'discard') {
1520
1530
  // La scheda resta nella barra e si ricarica al focus: il processo muore, la
@@ -5112,7 +5122,16 @@ async function captureDownload(item) {
5112
5122
  const res = await fetch(url, { credentials: 'include', redirect: 'follow' });
5113
5123
  const declared = Number(res.headers.get('content-length') || 0);
5114
5124
  entry.mime = res.headers.get('content-type') || entry.mime;
5115
- entry.name = downloadName({ disposition: res.headers.get('content-disposition'), chromeName: item.filename, url: res.url || url });
5125
+ let pageName = '';
5126
+ try {
5127
+ const probes = await chrome.scripting.executeScript({
5128
+ target: { tabId, allFrames: true }, world: 'MAIN',
5129
+ func: (u) => window.__cbCapture?.names.get(u) ?? '',
5130
+ args: [item.url],
5131
+ });
5132
+ pageName = probes.map((r) => r.result).find(Boolean) || '';
5133
+ } catch {}
5134
+ entry.name = downloadName({ disposition: res.headers.get('content-disposition'), chromeName: item.filename, pageName, url: res.url || url });
5116
5135
  if (!res.ok) throw new Error(`refetch answered HTTP ${res.status} (a POST download or a one-time link cannot be fetched again)`);
5117
5136
  if (declared > max) {
5118
5137
  try { await res.body?.cancel(); } catch {}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chrome-bridge-mcp",
3
3
  "mcpName": "io.github.frsorrentino/chrome-bridge",
4
- "version": "1.30.0",
4
+ "version": "1.30.2",
5
5
  "type": "module",
6
6
  "description": "Chrome Bridge by frsorrentino: MCP server that lets Claude Code borrow a tab from your own logged-in Chrome and hand it back — logins, 2FA and CAPTCHAs stay with you (handoff). 67 tools; 2.8-4.3× cheaper and up to 9.3× fewer turns than Claude in Chrome, measured; ChromeOS included.",
7
7
  "main": "server/index.js",
@@ -9,6 +9,8 @@
9
9
  * chrome-bridge gira da Node 18: sotto si usa la 12.8.2 (Node >= 18.16).
10
10
  */
11
11
  import { spawn } from 'node:child_process';
12
+ import { existsSync } from 'node:fs';
13
+ import { win32 } from 'node:path';
12
14
 
13
15
  export function lighthouseVersion(node = process.versions.node) {
14
16
  const [maj, min] = node.split('.').map(Number);
@@ -21,9 +23,24 @@ export function lighthouseArgs(url, port, outBase, { categories, formFactor = 'm
21
23
  '--quiet', ...(categories?.length ? [`--only-categories=${categories.join(',')}`] : []), ...(formFactor === 'desktop' ? ['--preset=desktop'] : [])];
22
24
  }
23
25
 
26
+ /**
27
+ * Come lanciare npx. Su Windows npx è npx.cmd, che spawn senza shell non trova
28
+ * (ENOENT): si usa npx-cli.js accanto a node.exe con il node del server, senza
29
+ * shell. Se manca, npx.cmd con la shell e gli argomenti fra virgolette.
30
+ */
31
+ export function npxCommand({ platform = process.platform, execPath = process.execPath, env = process.env, exists = existsSync } = {}) {
32
+ if (env.CHROME_BRIDGE_NPX) return { cmd: env.CHROME_BRIDGE_NPX, pre: [], shell: false };
33
+ if (platform !== 'win32') return { cmd: 'npx', pre: [], shell: false };
34
+ const cli = win32.join(win32.dirname(execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js');
35
+ if (exists(cli)) return { cmd: execPath, pre: [cli], shell: false };
36
+ return { cmd: 'npx.cmd', pre: [], shell: true };
37
+ }
38
+
24
39
  export function runLighthouse(args, { timeoutMs = 240000 } = {}) {
25
40
  return new Promise((resolve, reject) => {
26
- const p = spawn(process.env.CHROME_BRIDGE_NPX || 'npx', args, { stdio: ['ignore', 'ignore', 'pipe'] });
41
+ const { cmd, pre, shell } = npxCommand();
42
+ const argv = [...pre, ...args].map((a) => (shell ? `"${a}"` : a));
43
+ const p = spawn(cmd, argv, { stdio: ['ignore', 'ignore', 'pipe'], shell });
27
44
  let err = '';
28
45
  const timer = setTimeout(() => { p.kill('SIGKILL'); reject(new Error(`Lighthouse did not finish within ${timeoutMs / 1000} s`)); }, timeoutMs);
29
46
  p.stderr.on('data', (d) => { err += d; });
@@ -100,7 +100,7 @@ export const MessageType = Object.freeze({
100
100
  });
101
101
 
102
102
  // Versione
103
- export const VERSION = '1.30.0';
103
+ export const VERSION = '1.30.2';
104
104
 
105
105
  // Configurazione
106
106
  export const DEFAULT_PORT = 8765;