chrome-bridge-mcp 1.15.0 → 1.15.1

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Legge lo stato reale di un download.
3
+ *
4
+ * `chrome.downloads.download()` restituisce un id e basta: dire "started: true"
5
+ * e fermarsi lì è come fidarsi di un HTTP 200 senza guardare il corpo. Su
6
+ * ChromeOS, con l'impostazione «Chiedi dove salvare ogni file» attiva, Chrome
7
+ * ignora `saveAs: false` e apre il selettore: il file viene scaricato per intero
8
+ * ma non viene scritto da nessuna parte finché un umano non sceglie.
9
+ *
10
+ * La firma di quella condizione è precisa e verificata dal vivo:
11
+ * `state: 'in_progress'`, `filename: ''`, `bytesReceived === totalBytes`.
12
+ * Senza distinguerla, un agente aspetta un file che non arriverà mai da solo.
13
+ */
14
+ export function classifyDownload(item) {
15
+ if (!item) return { state: 'unknown', done: false, reason: 'download not found' };
16
+
17
+ const { state, filename, bytesReceived = 0, totalBytes = 0, error } = item;
18
+
19
+ if (state === 'complete') return { state: 'complete', done: true, filename };
20
+ if (state === 'interrupted') {
21
+ return { state: 'interrupted', done: true, error: error || 'interrupted', filename };
22
+ }
23
+
24
+ // Nessuna destinazione decisa: il selettore è aperto e aspetta una persona.
25
+ if (!filename) {
26
+ const fetched = totalBytes > 0 && bytesReceived >= totalBytes;
27
+ return {
28
+ state: 'waiting_for_user',
29
+ done: false,
30
+ bytes: { received: bytesReceived, total: totalBytes },
31
+ reason: fetched
32
+ ? 'bytes fetched, but Chrome is asking where to save — the "Ask where to save each file" setting overrides saveAs:false'
33
+ : 'no destination chosen yet — Chrome is showing the save dialog',
34
+ };
35
+ }
36
+
37
+ return {
38
+ state: 'in_progress',
39
+ done: false,
40
+ filename,
41
+ bytes: { received: bytesReceived, total: totalBytes },
42
+ };
43
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Chrome Bridge for Claude Code",
4
- "version": "1.15.0",
4
+ "version": "1.15.1",
5
5
  "description": "Bridge your browser to Claude Code: 63 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",
@@ -8,6 +8,7 @@
8
8
  import './telemetry.js';
9
9
  import { buildMarkdown } from './lib/page-markdown.js';
10
10
  import { computeTiles } from './lib/tile-layout.js';
11
+ import { classifyDownload } from './lib/download-state.js';
11
12
  const { pushError } = globalThis.__cbTelemetry;
12
13
 
13
14
  const DEFAULT_PORT = 8765;
@@ -1092,7 +1093,7 @@ async function cmdTileWindows({ window_ids, reference_window_id, layout = 'grid'
1092
1093
  };
1093
1094
  }
1094
1095
 
1095
- async function cmdMoveTab({ tab_id, window_id, new_window = false, left, top, width, height, index = -1 }) {
1096
+ async function cmdMoveTab({ tab_id, window_id, new_window = false, window_type, left, top, width, height, index = -1 }) {
1096
1097
  if (typeof tab_id !== 'number') throw new Error('Missing required parameter: tab_id');
1097
1098
  if (typeof window_id !== 'number' && !new_window) throw new Error('Provide window_id or new_window: true');
1098
1099
 
@@ -1100,8 +1101,11 @@ async function cmdMoveTab({ tab_id, window_id, new_window = false, left, top, wi
1100
1101
 
1101
1102
  if (new_window) {
1102
1103
  // tabs.move vuole una finestra che esiste già; per estrarre in una finestra
1103
- // nuova (e posizionata) l'unica via è windows.create({tabId}).
1104
+ // nuova (e posizionata) l'unica via è windows.create({tabId}). windows.create
1105
+ // conosce solo normal e popup: le finestre app sono della SWA, non nostre —
1106
+ // popup è il massimo di "finestra terminale" ottenibile da un'estensione.
1104
1107
  const winOpts = { tabId: tab_id };
1108
+ if (window_type === 'popup') winOpts.type = 'popup';
1105
1109
  for (const [k, v] of [['left', left], ['top', top], ['width', width], ['height', height]]) {
1106
1110
  if (typeof v === 'number') winOpts[k] = v;
1107
1111
  }
@@ -1111,6 +1115,9 @@ async function cmdMoveTab({ tab_id, window_id, new_window = false, left, top, wi
1111
1115
  url: before.url,
1112
1116
  from_window: before.windowId,
1113
1117
  to_window: win.id,
1118
+ // Il tipo lo dice Chrome, non la richiesta: se popup viene declassato a
1119
+ // normal il chiamante deve vederlo da qui.
1120
+ window_type: win.type,
1114
1121
  new_window: true,
1115
1122
  index: 0,
1116
1123
  same_window: before.windowId === win.id,
@@ -1910,7 +1917,7 @@ async function cmdViewportResize({ preset, width, height, left, top, state, read
1910
1917
  if (Object.keys(updateOpts).length === 0) {
1911
1918
  if (state) {
1912
1919
  const w = await chrome.windows.get(tab.windowId);
1913
- return { requested: { state }, actual: { left: w.left, top: w.top, width: w.width, height: w.height, state: w.state } };
1920
+ return { requested: { state }, window: { left: w.left, top: w.top, width: w.width, height: w.height, state: w.state } };
1914
1921
  }
1915
1922
  throw new Error('Provide preset, width, height, left, top or state');
1916
1923
  }
@@ -1920,24 +1927,40 @@ async function cmdViewportResize({ preset, width, height, left, top, state, read
1920
1927
  await new Promise((r) => setTimeout(r, 200));
1921
1928
  }
1922
1929
 
1923
- const results = await chrome.scripting.executeScript({
1924
- target: { tabId },
1925
- func: () => ({
1926
- viewportWidth: window.innerWidth,
1927
- viewportHeight: window.innerHeight,
1928
- devicePixelRatio: window.devicePixelRatio,
1929
- }),
1930
- world: 'MAIN',
1931
- });
1932
- const actual = results?.[0]?.result ?? {};
1933
- if (read_only) return { actual };
1934
- const win = await chrome.windows.get(tab.windowId);
1930
+ // La misura del viewport passa da un'injection, e `chrome://` e
1931
+ // `chrome-untrusted://` la rifiutano: le finestre del Terminale e di
1932
+ // media-app sono esattamente quelle. Prima questo errore veniva propagato e
1933
+ // il tool falliva DOPO aver spostato la finestra — l'azione riuscita
1934
+ // riportata come guasto, che è il modo di far ritentare chi chiama qualcosa
1935
+ // che è già successo. La geometria della finestra non richiede injection:
1936
+ // arriva da chrome.windows.get e c'è sempre.
1937
+ let actual = {};
1938
+ let viewport_error = null;
1939
+ try {
1940
+ const results = await chrome.scripting.executeScript({
1941
+ target: { tabId },
1942
+ func: () => ({
1943
+ viewportWidth: window.innerWidth,
1944
+ viewportHeight: window.innerHeight,
1945
+ devicePixelRatio: window.devicePixelRatio,
1946
+ }),
1947
+ world: 'MAIN',
1948
+ });
1949
+ actual = results?.[0]?.result ?? {};
1950
+ } catch (e) {
1951
+ viewport_error = e.message;
1952
+ }
1953
+ const winNow = await chrome.windows.get(tab.windowId);
1954
+ const geometry = { left: winNow.left, top: winNow.top, width: winNow.width, height: winNow.height, state: winNow.state };
1955
+ if (read_only) return { actual, window: geometry, viewport_error };
1956
+ const win = winNow;
1935
1957
  return {
1936
1958
  requested: { width: targetW, height: targetH, preset: preset || null, left, top, state },
1937
1959
  actual,
1960
+ viewport_error,
1938
1961
  // Il window manager può accettare e ignorare: senza il confronto una
1939
1962
  // finestra rimasta ferma passerebbe per spostata.
1940
- window: { left: win.left, top: win.top, width: win.width, height: win.height, state: win.state },
1963
+ window: geometry,
1941
1964
  };
1942
1965
  }
1943
1966
 
@@ -3687,7 +3710,13 @@ async function cmdManageDownloads({ action, url, filename, timeout = 30000, limi
3687
3710
  const opts = { url, saveAs: false };
3688
3711
  if (filename) opts.filename = filename;
3689
3712
  const id = await chrome.downloads.download(opts);
3690
- return { started: true, id, url };
3713
+ // Non basta l'id: su ChromeOS l'impostazione «Chiedi dove salvare ogni file»
3714
+ // scavalca saveAs:false e apre il selettore, quindi il download resta fermo
3715
+ // ad aspettare una persona. Una breve occhiata allo stato reale distingue
3716
+ // "partito" da "in attesa di te".
3717
+ await new Promise((r) => setTimeout(r, 600));
3718
+ const [item] = await chrome.downloads.search({ id });
3719
+ return { id, url, ...classifyDownload(item) };
3691
3720
  }
3692
3721
 
3693
3722
  if (action === 'list') {
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.15.0",
4
+ "version": "1.15.1",
5
5
  "type": "module",
6
6
  "description": "MCP server with 63 tools for browser automation via Chrome extension. Works on ChromeOS/Crostini.",
7
7
  "main": "server/index.js",
package/server/tools.js CHANGED
@@ -1349,7 +1349,10 @@ export function registerTools(server, wsManager, caps = 'all') {
1349
1349
  'tab_action',
1350
1350
  'Tab lifecycle: close, activate (focus), reload (optional cache bypass), back, forward. close discards the '
1351
1351
  + 'tab and anything unsaved in it, and cannot be undone — it may be a tab the user is working in. '
1352
- + 'reload and navigation drop injected CSS, emulations and page hooks.',
1352
+ + 'reload and navigation drop injected CSS, emulations and page hooks. duplicate works where create_tab is '
1353
+ + 'forbidden (chrome-untrusted://, verified on a ChromeOS Terminal tab) and lands in the source tab\'s own '
1354
+ + 'window, app windows included — the only way to open a new Terminal session inside the Terminal window. '
1355
+ + 'discard replaces the tab id: the result carries the new one, saved ids go stale.',
1353
1356
  {
1354
1357
  action: z.enum(['close', 'activate', 'reload', 'back', 'forward', 'discard', 'mute', 'unmute', 'duplicate']).describe('close cannot be undone; discard frees memory, the tab reloads on focus; reload drops injected CSS and hooks'),
1355
1358
  bypass_cache: z.boolean().optional().default(false).describe('reload only'),
@@ -1666,9 +1669,12 @@ export function registerTools(server, wsManager, caps = 'all') {
1666
1669
  // --- manage_downloads ---
1667
1670
  server.tool(
1668
1671
  'manage_downloads',
1669
- 'List downloads or wait for one to complete. Files land in the browser Downloads folder, not on the server.',
1672
+ 'List downloads, start one, or wait for the newest to finish. Files land in the browser Downloads folder, '
1673
+ + 'not on the server. action=download reports the download\'s real state rather than just an id: with '
1674
+ + 'Chrome set to ask where to save each file, saveAs:false is overridden and it comes back as '
1675
+ + 'waiting_for_user — every byte fetched, no destination chosen, and nothing will move until someone picks one.',
1670
1676
  {
1671
- action: z.enum(['list', 'wait_for_complete', 'download']).describe('download starts one with the browser cookie jar; wait_for_complete blocks until the newest finishes'),
1677
+ action: z.enum(['list', 'wait_for_complete', 'download']).describe('download fetches with the browser cookie jar and reports the real state; wait_for_complete blocks until the newest finishes'),
1672
1678
  url: z.string().optional().describe('What to download (action=download); sent with the session cookies of its origin'),
1673
1679
  filename: z.string().optional().describe('Relative path inside the Downloads folder (action=download)'),
1674
1680
  timeout: z.number().optional().default(30000).describe('Max ms (wait_for_complete)'),
@@ -1757,25 +1763,30 @@ export function registerTools(server, wsManager, caps = 'all') {
1757
1763
  // --- move_tab ---
1758
1764
  server.tool(
1759
1765
  'move_tab',
1760
- 'Move an existing tab into another window (chrome.tabs.move), e.g. to consolidate windows that were '
1761
- + 'each opened separately. Does not create or close anything: the tab keeps its id, its history and its '
1762
- + 'page state. Both windows must be normal browser windows — Chrome refuses to move a tab into a popup or '
1763
- + 'an app window, and the refusal is reported verbatim rather than retried.',
1766
+ 'Move an existing tab into another window (chrome.tabs.move). Nothing is created or closed: the tab keeps '
1767
+ + 'its id, history and page state, and this works even on chrome-untrusted:// tabs where creating one is '
1768
+ + 'forbidden — verified on a ChromeOS Terminal tab. The **destination** must be a normal window: moving out '
1769
+ + 'of an app or popup window is fine, moving into one is refused with "Tabs can only be moved to and from '
1770
+ + 'normal windows". So a ChromeOS Terminal session can be pulled out to its own window (new_window, then '
1771
+ + 'position it with viewport_resize) but cannot be merged back into the Terminal window — the closest look '
1772
+ + 'is window_type popup: no tab strip, no omnibox, visually a terminal window. An extension cannot create '
1773
+ + 'app windows: normal and popup are all Chrome offers.',
1764
1774
  {
1765
1775
  tab_id: z.number().describe('Tab to move; get it from get_tabs'),
1766
1776
  window_id: z.number().optional().describe('Destination window; get_tabs reports windowId for every tab'),
1767
1777
  new_window: z.boolean().optional().default(false).describe('Extract the tab into a fresh window instead — tabs.move needs an existing window, this does not'),
1778
+ window_type: z.enum(['normal', 'popup']).optional().default('normal').describe('new_window only: popup has no tab strip and no omnibox — the terminal-window look for detached chrome-untrusted://terminal tabs'),
1768
1779
  left: z.number().optional().describe('New window x (new_window)'),
1769
1780
  top: z.number().optional().describe('New window y (new_window)'),
1770
1781
  width: z.number().optional().describe('New window width px (new_window)'),
1771
1782
  height: z.number().optional().describe('New window height px (new_window)'),
1772
1783
  index: z.number().optional().default(-1).describe('Position in the destination window; -1 appends at the end'),
1773
1784
  },
1774
- async ({ tab_id, window_id, new_window, left, top, width, height, index }) => {
1785
+ async ({ tab_id, window_id, new_window, window_type, left, top, width, height, index }) => {
1775
1786
  if (window_id == null && new_window !== true) {
1776
1787
  throw new Error('Provide window_id (an existing window) or new_window: true');
1777
1788
  }
1778
- const data = await send(MessageType.MOVE_TAB, { tab_id, window_id, new_window: new_window === true || undefined, left, top, width, height, index: index ?? -1 });
1789
+ const data = await send(MessageType.MOVE_TAB, { tab_id, window_id, new_window: new_window === true || undefined, window_type, left, top, width, height, index: index ?? -1 });
1779
1790
  return { content: [{ type: 'text', text: jsonText(data) }] };
1780
1791
  }
1781
1792
  );