chrome-ai-bridge 2.3.10 → 2.5.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/chrome-ai-bridge.svg)](https://npmjs.org/package/chrome-ai-bridge)
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
 
6
- > **⚠️ Requires Chrome Extension** — This MCP server controls ChatGPT/Gemini tabs via a browser extension.
6
+ > **Requires Chrome Extension** — This CLI tool controls ChatGPT/Gemini tabs via a browser extension.
7
7
 
8
8
  Let your AI assistant (Claude Code, Cursor, etc.) consult ChatGPT and Gemini for second opinions.
9
9
 
@@ -12,20 +12,20 @@ Let your AI assistant (Claude Code, Cursor, etc.) consult ChatGPT and Gemini for
12
12
  ## How it works
13
13
 
14
14
  ```
15
- Your AI Assistant → MCP Server → Chrome Extension → ChatGPT/Gemini tabs
15
+ Your AI Assistant → cab CLI / daemon → Chrome Extension → ChatGPT/Gemini tabs
16
16
  ```
17
17
 
18
- 1. **Chrome Extension** (you install) bridges MCP server and browser
19
- 2. **MCP Server** (npm package) receives requests from your AI assistant
18
+ 1. **Chrome Extension** (you install) bridges the daemon and browser
19
+ 2. **`cab` CLI / daemon** (npm package) receives requests from your AI assistant via REST API
20
20
  3. **Extension** controls ChatGPT/Gemini tabs via CDP (Chrome DevTools Protocol)
21
21
 
22
- **Why an extension?** ChatGPT and Gemini don't have public APIs. The extension automates the web UI while you stay logged in.
22
+ **Why an extension?** ChatGPT and Gemini don't have free public APIs. The extension automates the web UI while you stay logged in.
23
23
 
24
24
  ---
25
25
 
26
26
  ## What is this?
27
27
 
28
- chrome-ai-bridge is a [Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI assistants the ability to:
28
+ chrome-ai-bridge is a CLI tool and daemon that gives AI assistants the ability to:
29
29
 
30
30
  - **Consult other AIs**: Ask ChatGPT and Gemini questions via browser
31
31
  - **Get multiple perspectives**: Query both AIs in parallel for second opinions
@@ -71,7 +71,7 @@ Network extraction is the primary path. DOM extraction remains as an automatic f
71
71
 
72
72
  ## Quick Start
73
73
 
74
- > **⚠️ Both steps are required** — The extension and MCP server work together.
74
+ > **Both steps are required** — The extension and daemon work together.
75
75
 
76
76
  ### Step 1: Install Chrome Extension
77
77
 
@@ -95,30 +95,23 @@ Then load the extension in Chrome:
95
95
 
96
96
  You should see "Chrome AI Bridge" appear in your extensions list.
97
97
 
98
- ### Step 2: Configure your MCP client
98
+ ### Step 2: Install the `cab` CLI
99
99
 
100
- **For Claude Code** (`~/.claude.json`):
101
-
102
- ```json
103
- {
104
- "mcpServers": {
105
- "chrome-ai-bridge": {
106
- "command": "npx",
107
- "args": ["chrome-ai-bridge@latest"]
108
- }
109
- }
110
- }
100
+ ```bash
101
+ npm install -g chrome-ai-bridge
111
102
  ```
112
103
 
113
104
  ### Step 3: Connect the Extension
114
105
 
115
106
  1. Open ChatGPT (https://chatgpt.com) or Gemini (https://gemini.google.com) in Chrome
116
107
  2. Log in to both services
117
- 3. The extension will automatically connect when the MCP server starts
108
+ 3. The extension will automatically connect when the daemon starts
118
109
 
119
110
  ### Step 4: Verify it works
120
111
 
121
- Restart your AI client and try: `"Ask ChatGPT how to implement OAuth in Node.js"`
112
+ ```bash
113
+ cab ask chatgpt "How do I implement OAuth in Node.js?"
114
+ ```
122
115
 
123
116
  ---
124
117
 
@@ -187,7 +180,7 @@ User: "Ask ChatGPT specifically about this"
187
180
 
188
181
  | Variable | Description |
189
182
  |----------|-------------|
190
- | `MCP_DISABLE_WEB_LLM` | Set `true` to disable ChatGPT/Gemini tools |
183
+ | `CAI_DISABLE_WEB_LLM` | Set `true` to disable ChatGPT/Gemini tools |
191
184
 
192
185
  ---
193
186
 
@@ -201,17 +194,10 @@ cd chrome-ai-bridge
201
194
  npm install && npm run build
202
195
  ```
203
196
 
204
- Configure `~/.claude.json` to use local build:
205
-
206
- ```json
207
- {
208
- "mcpServers": {
209
- "chrome-ai-bridge": {
210
- "command": "node",
211
- "args": ["/path/to/chrome-ai-bridge/scripts/cli.mjs"]
212
- }
213
- }
214
- }
197
+ For local development, run the daemon directly:
198
+
199
+ ```bash
200
+ node /path/to/chrome-ai-bridge/scripts/cli.mjs
215
201
  ```
216
202
 
217
203
  ### Commands
@@ -230,7 +216,7 @@ chrome-ai-bridge/
230
216
  ├── src/
231
217
  │ ├── fast-cdp/ # CDP client and AI chat logic
232
218
  │ ├── extension/ # Chrome extension source
233
- │ ├── main.ts # MCP server entry point
219
+ │ ├── main.ts # Daemon entry point
234
220
  │ └── index.ts # Main exports
235
221
  ├── scripts/
236
222
  │ └── cli.mjs # CLI entry point
@@ -261,7 +247,7 @@ npm run cdp:gemini
261
247
  | Guide | Description |
262
248
  |-------|-------------|
263
249
  | [Technical Spec](docs/SPEC.md) | Detailed architecture and implementation |
264
- | [Setup Guide](docs/user/setup.md) | Detailed MCP configuration |
250
+ | [Setup Guide](docs/user/setup.md) | Detailed setup and configuration |
265
251
  | [Troubleshooting](docs/user/troubleshooting.md) | Problem solving |
266
252
  | [CI Policy](docs/ci-policy.md) | Required checks and browser E2E lane policy |
267
253
  | [Technical Spec - Architecture](docs/SPEC.md#1-architecture-overview) | Extension architecture |
@@ -276,10 +262,12 @@ npm run cdp:gemini
276
262
  2. Verify ChatGPT/Gemini tabs are open and logged in
277
263
  3. Check the extension popup for connection status
278
264
 
279
- ### MCP server not responding
265
+ ### Daemon not responding
280
266
 
281
267
  ```bash
282
- npx clear-npx-cache && npx chrome-ai-bridge@latest
268
+ cab status
269
+ # or restart:
270
+ cab daemon restart
283
271
  ```
284
272
 
285
273
  ### ChatGPT/Gemini not responding
@@ -295,9 +283,9 @@ npx clear-npx-cache && npx chrome-ai-bridge@latest
295
283
  ## Architecture (v2.0.0)
296
284
 
297
285
  ```
298
- ┌─────────────────┐ MCP ┌──────────────────┐
299
- │ Claude Code │ ◀──────────────────▶│ MCP Server
300
- │ (MCP Client) │ │ (Node.js) │
286
+ ┌─────────────────┐ REST API ┌──────────────────┐
287
+ │ Claude Code │ ◀──────────────────▶│ Chrome AI Bridge
288
+ │ (cab CLI) │ │ (Node.js) │
301
289
  └─────────────────┘ └────────┬─────────┘
302
290
 
303
291
 
@@ -1,17 +1,17 @@
1
1
  # chrome-ai-bridge Extension
2
2
 
3
- このChrome拡張機能は、chrome-ai-bridge MCPサーバーとChromeブラウザのタブを接続します。
3
+ このChrome拡張機能は、chrome-ai-bridge サーバーとChromeブラウザのタブを接続します。
4
4
 
5
5
  ## アーキテクチャ
6
6
 
7
7
  ```
8
- chrome-ai-bridge MCPサーバー (プロセス1)
8
+ chrome-ai-bridge サーバー (プロセス1)
9
9
  ↓ WebSocket
10
10
  Extension (TabShareExtension)
11
11
  ↓ chrome.debugger API
12
12
  Chrome Tab #101 (ChatGPT)
13
13
 
14
- chrome-ai-bridge MCPサーバー (プロセス2)
14
+ chrome-ai-bridge サーバー (プロセス2)
15
15
  ↓ WebSocket
16
16
  Extension (TabShareExtension)
17
17
  ↓ chrome.debugger API
@@ -66,10 +66,10 @@ npm run build
66
66
  }
67
67
  ```
68
68
 
69
- ### MCPサーバー起動
69
+ ### サーバー起動
70
70
 
71
71
  1. Claude Codeを起動
72
- 2. MCPサーバーが自動的に起動し、WebSocket Relayサーバーが立ち上がります
72
+ 2. サーバーが自動的に起動し、WebSocket Relayサーバーが立ち上がります
73
73
  3. ログに以下のようなメッセージが表示されます:
74
74
 
75
75
  ```
@@ -112,7 +112,7 @@ chrome-extension://[EXTENSION_ID]/ui/connect.html?mcpRelayUrl=ws://127.0.0.1:123
112
112
 
113
113
  ### 接続確認
114
114
 
115
- MCPサーバーのログに以下のメッセージが表示されれば成功:
115
+ サーバーのログに以下のメッセージが表示されれば成功:
116
116
 
117
117
  ```
118
118
  [Extension Bridge] Extension connected to tab 101
@@ -128,8 +128,8 @@ src/extension/
128
128
  ├── ui/
129
129
  │ ├── connect.html # タブ選択UI
130
130
  │ └── connect.js # UIロジック
131
- ├── relay-server.ts # WebSocketサーバー (MCPサーバー側)
132
- └── extension-transport.ts # Puppeteer Transport実装 (MCPサーバー側)
131
+ ├── relay-server.ts # WebSocketサーバー (サーバー側)
132
+ └── extension-transport.ts # Puppeteer Transport実装 (サーバー側)
133
133
  ```
134
134
 
135
135
  ## トラブルシューティング
@@ -144,10 +144,10 @@ src/extension/
144
144
 
145
145
  ### Invalid token
146
146
 
147
- **原因**: MCPサーバーのトークンが一致しない
147
+ **原因**: サーバーのトークンが一致しない
148
148
 
149
149
  **解決策**:
150
- 1. MCPサーバーのログからトークンを確認
150
+ 1. サーバーのログからトークンを確認
151
151
  2. URLパラメータに正しいトークンを含める
152
152
 
153
153
  ### Tab not found
@@ -170,6 +170,8 @@ class RelayConnection {
170
170
  }
171
171
  if (message.method === 'reloadExtension') {
172
172
  logInfo('reload', 'reloadExtension command received');
173
+ // Set flag so the reloaded service worker skips cooldown
174
+ chrome.storage.local.set({ _reloadTriggered: Date.now() }).catch(() => {});
173
175
  // Delay reload to allow response to be sent first
174
176
  setTimeout(() => {
175
177
  logInfo('reload', 'Calling chrome.runtime.reload()');
@@ -267,7 +269,7 @@ class TabShareExtension {
267
269
  case 'connectToRelay':
268
270
  this._connectToRelay(
269
271
  sender.tab?.id,
270
- message.mcpRelayUrl,
272
+ message.relayUrl,
271
273
  message.sessionId,
272
274
  ).then(
273
275
  () => sendResponse({success: true}),
@@ -290,7 +292,7 @@ class TabShareExtension {
290
292
  sender.tab?.id,
291
293
  message.tabId || sender.tab?.id,
292
294
  message.windowId || sender.tab?.windowId,
293
- message.mcpRelayUrl,
295
+ message.relayUrl,
294
296
  message.tabUrl,
295
297
  message.newTab,
296
298
  message.sessionId,
@@ -329,8 +331,8 @@ class TabShareExtension {
329
331
  return `selector:${selectorTabId}`;
330
332
  }
331
333
 
332
- async _connectToRelay(selectorTabId, mcpRelayUrl, sessionId) {
333
- if (!mcpRelayUrl) {
334
+ async _connectToRelay(selectorTabId, relayUrl, sessionId) {
335
+ if (!relayUrl) {
334
336
  logError('relay', 'Missing relay URL');
335
337
  throw new Error('Missing relay URL');
336
338
  }
@@ -342,10 +344,10 @@ class TabShareExtension {
342
344
  this._pendingTabSelection.delete(pendingKey);
343
345
  ensureKeepAliveAlarm('replace-stale-pending');
344
346
  }
345
- logInfo('relay', 'Connecting to relay', {mcpRelayUrl, selectorTabId, sessionId, pendingKey});
347
+ logInfo('relay', 'Connecting to relay', {relayUrl, selectorTabId, sessionId, pendingKey});
346
348
 
347
349
  const openSocket = async attempt => {
348
- const socket = new WebSocket(mcpRelayUrl);
350
+ const socket = new WebSocket(relayUrl);
349
351
  await new Promise((resolve, reject) => {
350
352
  let settled = false;
351
353
  const finish = (handler) => {
@@ -391,7 +393,7 @@ class TabShareExtension {
391
393
  let lastError;
392
394
  const maxAttempts = 5;
393
395
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
394
- logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {mcpRelayUrl});
396
+ logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {relayUrl});
395
397
  try {
396
398
  socket = await openSocket(attempt);
397
399
  logInfo('relay', 'WebSocket connected', {attempt: attempt + 1});
@@ -426,7 +428,7 @@ class TabShareExtension {
426
428
  selectorTabId,
427
429
  tabId,
428
430
  windowId,
429
- mcpRelayUrl,
431
+ relayUrl,
430
432
  tabUrl,
431
433
  newTab,
432
434
  sessionId,
@@ -480,14 +482,14 @@ class TabShareExtension {
480
482
 
481
483
  const pending = this._pendingTabSelection.get(pendingKey);
482
484
  if (!pending) {
483
- logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId, mcpRelayUrl});
485
+ logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId, relayUrl});
484
486
  // If no pending connection, create one now.
485
- await this._connectToRelay(selectorTabId, mcpRelayUrl, sessionId);
487
+ await this._connectToRelay(selectorTabId, relayUrl, sessionId);
486
488
  }
487
489
  const newPending = this._pendingTabSelection.get(pendingKey);
488
490
  if (!newPending) {
489
- logError('connect', 'No active MCP relay connection');
490
- throw new Error('No active MCP relay connection');
491
+ logError('connect', 'No active relay connection');
492
+ throw new Error('No active relay connection');
491
493
  }
492
494
 
493
495
  if (
@@ -519,7 +521,7 @@ class TabShareExtension {
519
521
  this._tabSessionOwners.set(tabId, sessionId || `selector:${selectorTabId}`);
520
522
  logInfo('connect', 'Tab connected successfully', {tabId, windowId, sessionId});
521
523
  ensureKeepAliveAlarm('tab-connected');
522
- // バッジのみ設定(フォーカスはMCPサーバー側が必要に応じて制御)
524
+ // バッジのみ設定(フォーカスはサーバー側が必要に応じて制御)
523
525
  await this._setConnectedTab(tabId, true);
524
526
  }
525
527
 
@@ -599,6 +601,9 @@ class TabShareExtension {
599
601
  async _getDebugLogs(filter, limit) {
600
602
  const result = await chrome.storage.local.get('logs');
601
603
  const rawLogs = Array.isArray(result.logs) ? result.logs : [];
604
+ const toIso = value => (typeof value === 'number' && value > 0
605
+ ? new Date(value).toISOString()
606
+ : null);
602
607
  const normalized = rawLogs.map(logEntry => ({
603
608
  ts: logEntry.timestamp || logEntry.ts || new Date().toISOString(),
604
609
  category: logEntry.category || 'unknown',
@@ -626,6 +631,25 @@ class TabShareExtension {
626
631
  activeConnections: Array.from(this._activeConnections.keys()),
627
632
  pendingTabSelection: Array.from(this._pendingTabSelection.keys()),
628
633
  tabSessionOwners: Object.fromEntries(this._tabSessionOwners.entries()),
634
+ discovery: {
635
+ mode: discoveryMode,
636
+ intervalMs: getDiscoveryIntervalMs(),
637
+ isRunning: isDiscoveryRunning,
638
+ hasScheduledTick: discoveryIntervalId !== null,
639
+ emptyDiscoveryStreak,
640
+ lastSuccessfulPort,
641
+ lastTickStartedAt: toIso(lastDiscoveryTickStartedAt),
642
+ lastTickFinishedAt: toIso(lastDiscoveryTickFinishedAt),
643
+ lastTickDurationMs: lastDiscoveryTickDurationMs,
644
+ lastSummary: lastDiscoverySummary,
645
+ lastError: lastDiscoveryError,
646
+ lastRelayProbeAt: toIso(lastRelayProbeAt),
647
+ lastRelayProbePort,
648
+ lastRelayProbeStatus,
649
+ lastRelayProbeError,
650
+ keepAliveActive,
651
+ lastKeepAliveAlarmAt: toIso(lastKeepAliveAlarmAt),
652
+ },
629
653
  },
630
654
  };
631
655
  }
@@ -715,7 +739,7 @@ class TabShareExtension {
715
739
 
716
740
  const tabShareExtension = new TabShareExtension();
717
741
 
718
- const DISCOVERY_ALARM = 'mcp-relay-discovery';
742
+ const DISCOVERY_ALARM = 'cab-relay-discovery';
719
743
  const KEEPALIVE_ALARM = 'keepAlive';
720
744
  const KEEPALIVE_PERIOD_MINUTES = 0.5;
721
745
  const DISCOVERY_PORTS = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
@@ -731,7 +755,7 @@ const DISCOVERY_INTERVAL_MS = {
731
755
  };
732
756
  const FAST_TO_NORMAL_EMPTY_STREAK = 5;
733
757
  const NORMAL_TO_IDLE_EMPTY_STREAK = 20;
734
- const ACTIVE_TO_IDLE_EMPTY_STREAK = 3;
758
+ const ACTIVE_TO_IDLE_EMPTY_STREAK = 10;
735
759
  let lastSuccessfulPort = null;
736
760
  const lastRelayByPort = new Map();
737
761
 
@@ -743,10 +767,35 @@ let isDiscoveryRunning = false;
743
767
  let discoveryMode = DISCOVERY_MODE.FAST;
744
768
  let emptyDiscoveryStreak = 0;
745
769
  let keepAliveActive = false;
770
+ let lastDiscoveryTickStartedAt = 0;
771
+ let lastDiscoveryTickFinishedAt = 0;
772
+ let lastDiscoveryTickDurationMs = null;
773
+ let lastDiscoverySummary = null;
774
+ let lastDiscoveryError = null;
775
+ let lastRelayProbeAt = 0;
776
+ let lastRelayProbePort = null;
777
+ let lastRelayProbeStatus = null;
778
+ let lastRelayProbeError = null;
779
+ let lastKeepAliveAlarmAt = 0;
746
780
 
747
781
  // リロード時クールダウン: 5秒間は「新しいrelay」検出をスキップ
782
+ // ただし reloadExtension コマンド経由のリロード時はスキップしない
748
783
  const extensionStartTime = Date.now();
749
784
  const COOLDOWN_MS = 5000;
785
+ let cooldownDisabled = false;
786
+
787
+ // Check if this is a reload triggered by reloadExtension command
788
+ chrome.storage.local.get('_reloadTriggered').then(result => {
789
+ if (result._reloadTriggered) {
790
+ const age = Date.now() - result._reloadTriggered;
791
+ if (age < 10000) { // Within 10 seconds of reload trigger
792
+ cooldownDisabled = true;
793
+ logInfo('discovery', 'Cooldown disabled (reloadExtension triggered)', {age});
794
+ }
795
+ // Clear the flag
796
+ chrome.storage.local.remove('_reloadTriggered').catch(() => {});
797
+ }
798
+ }).catch(() => {});
750
799
 
751
800
  // ユーザー操作によるDiscoveryかどうかのフラグ
752
801
  // Chrome起動時やService Worker再起動時はfalse、アイコンクリック時のみtrue
@@ -838,7 +887,7 @@ function buildConnectUrl(
838
887
  sessionId,
839
888
  allowTabTakeover = false,
840
889
  ) {
841
- const params = new URLSearchParams({mcpRelayUrl: wsUrl});
890
+ const params = new URLSearchParams({relayUrl: wsUrl});
842
891
  if (tabUrl) params.set('tabUrl', tabUrl);
843
892
  if (newTab) params.set('newTab', 'true');
844
893
  if (autoMode) params.set('auto', 'true');
@@ -898,16 +947,34 @@ async function ensureConnectUiTab(
898
947
  async function fetchRelayInfo(port, timeoutMs = 800) {
899
948
  const discoveryUrl = `http://127.0.0.1:${port}/relay-info`;
900
949
  let timer = null;
950
+ lastRelayProbeAt = Date.now();
951
+ lastRelayProbePort = port;
901
952
  try {
902
953
  const controller = new AbortController();
903
954
  timer = setTimeout(() => controller.abort(), timeoutMs);
904
955
  const res = await fetch(discoveryUrl, {signal: controller.signal});
905
- if (!res.ok) return null;
956
+ if (!res.ok) {
957
+ lastRelayProbeStatus = `http-${res.status}`;
958
+ lastRelayProbeError = null;
959
+ return null;
960
+ }
906
961
  const data = await res.json();
907
- if (!data?.wsUrl) return null;
962
+ if (!data?.wsUrl) {
963
+ lastRelayProbeStatus = 'invalid-payload';
964
+ lastRelayProbeError = null;
965
+ return null;
966
+ }
967
+ lastRelayProbeStatus = 'ok';
968
+ lastRelayProbeError = null;
908
969
  lastSuccessfulPort = port;
909
970
  return data;
910
- } catch {
971
+ } catch (error) {
972
+ const message =
973
+ error && typeof error === 'object' && 'message' in error
974
+ ? String(error.message)
975
+ : String(error);
976
+ lastRelayProbeStatus = 'fetch-error';
977
+ lastRelayProbeError = message;
911
978
  return null;
912
979
  } finally {
913
980
  if (timer) {
@@ -937,15 +1004,15 @@ async function autoConnectRelay(best) {
937
1004
 
938
1005
  // tabUrl があれば、connect.html を開かずに直接接続
939
1006
  // preferredTabId があれば優先的に使用
1007
+ const requestedNewTab = Boolean(best?.data?.newTab);
940
1008
  let targetTabId;
941
1009
  try {
942
1010
  // autoConnectRelay経由の場合はフォーカスしない(active: false)
943
- // リロード時に勝手にタブがフォーカスされる問題を防ぐ
944
- // newTab: false に固定 - 自動接続では既存タブを優先してタブスパムを防止
1011
+ // newTab: relay の要求を尊重する(サーバーが newTab: true を指定した場合は新規タブ作成を許可)
945
1012
  targetTabId = await tabShareExtension._resolveTabId(
946
1013
  tabUrl,
947
1014
  preferredTabId,
948
- false, // newTab: false - always prefer existing tabs in auto-connect
1015
+ requestedNewTab,
949
1016
  false, // active: false - 自動接続時はタブをフォーカスしない
950
1017
  );
951
1018
  } catch (error) {
@@ -957,8 +1024,23 @@ async function autoConnectRelay(best) {
957
1024
  return false;
958
1025
  }
959
1026
  if (tabShareExtension._activeConnections?.has(targetTabId)) {
960
- logInfo('auto-connect', 'Tab already connected', {targetTabId});
961
- return true; // 既に接続済み
1027
+ const existingSessionId = tabShareExtension._tabSessionOwners?.get(targetTabId);
1028
+ const newSessionId = best?.data?.sessionId;
1029
+ if (existingSessionId && newSessionId && existingSessionId !== newSessionId) {
1030
+ // Different session wants the same tab — replace the old connection
1031
+ logInfo('auto-connect', 'Replacing stale connection with newer session', {
1032
+ targetTabId, oldSession: existingSessionId, newSession: newSessionId,
1033
+ });
1034
+ const oldConn = tabShareExtension._activeConnections.get(targetTabId);
1035
+ if (oldConn) {
1036
+ oldConn.close('Replaced by newer session');
1037
+ tabShareExtension._activeConnections.delete(targetTabId);
1038
+ tabShareExtension._tabSessionOwners.delete(targetTabId);
1039
+ }
1040
+ } else {
1041
+ logInfo('auto-connect', 'Tab already connected', {targetTabId});
1042
+ return true; // 同じセッションで接続済み
1043
+ }
962
1044
  }
963
1045
 
964
1046
  const targetTab = await chrome.tabs.get(targetTabId).catch(() => null);
@@ -1008,9 +1090,10 @@ async function autoOpenConnectUi() {
1008
1090
  failureCount: 0,
1009
1091
  };
1010
1092
 
1011
- // リロード直後はタブを開かない(既存MCPサーバーとの再接続を防ぐ)
1093
+ // リロード直後はタブを開かない(既存サーバーとの再接続を防ぐ)
1094
+ // ただし reloadExtension コマンド経由の場合はスキップしない
1012
1095
  const elapsed = Date.now() - extensionStartTime;
1013
- if (elapsed < COOLDOWN_MS) {
1096
+ if (elapsed < COOLDOWN_MS && !cooldownDisabled) {
1014
1097
  logDebug('discovery', `Cooldown active (${elapsed}ms < ${COOLDOWN_MS}ms), skipping`);
1015
1098
  result.skippedCooldown = true;
1016
1099
  return result;
@@ -1038,7 +1121,7 @@ async function autoOpenConnectUi() {
1038
1121
  continue;
1039
1122
  }
1040
1123
 
1041
- logInfo('discovery', 'New relay detected', {port, tabUrl: data.tabUrl, wsUrl: data.wsUrl});
1124
+ logInfo('discovery', 'New relay detected', {port, tabUrl: data.tabUrl, wsUrl: data.wsUrl, startedAt});
1042
1125
  lastRelayByPort.set(port, {
1043
1126
  wsUrl: data.wsUrl,
1044
1127
  startedAt,
@@ -1047,14 +1130,34 @@ async function autoOpenConnectUi() {
1047
1130
  newRelays.push({port, data});
1048
1131
  }
1049
1132
 
1050
- result.newRelayCount = newRelays.length;
1133
+ // Deduplicate: when multiple relays serve the same tabUrl, prefer the newest (by startedAt)
1134
+ // This prevents stale servers from stealing connections from active ones
1135
+ const bestByTabUrl = new Map();
1136
+ for (const relay of newRelays) {
1137
+ const tabUrl = relay.data?.tabUrl || '';
1138
+ const existing = bestByTabUrl.get(tabUrl);
1139
+ const relayStartedAt = relay.data?.startedAt || 0;
1140
+ if (!existing || relayStartedAt > (existing.data?.startedAt || 0)) {
1141
+ bestByTabUrl.set(tabUrl, relay);
1142
+ }
1143
+ }
1144
+ const dedupedRelays = [...bestByTabUrl.values()];
1145
+ if (dedupedRelays.length < newRelays.length) {
1146
+ logInfo('discovery', 'Deduped relays by tabUrl (preferring newest)', {
1147
+ before: newRelays.length,
1148
+ after: dedupedRelays.length,
1149
+ dropped: newRelays.filter(r => !dedupedRelays.includes(r)).map(r => ({port: r.port, tabUrl: r.data?.tabUrl, startedAt: r.data?.startedAt})),
1150
+ });
1151
+ }
1051
1152
 
1052
- if (newRelays.length > 0) {
1053
- logInfo('discovery', `Processing ${newRelays.length} new relay(s)`);
1153
+ result.newRelayCount = dedupedRelays.length;
1154
+
1155
+ if (dedupedRelays.length > 0) {
1156
+ logInfo('discovery', `Processing ${dedupedRelays.length} new relay(s)`);
1054
1157
  }
1055
1158
 
1056
1159
  // 全ての新しい relay を処理(並列ではなく順次)
1057
- for (const relay of newRelays) {
1160
+ for (const relay of dedupedRelays) {
1058
1161
  logInfo('discovery', 'Processing relay', {port: relay.port, tabUrl: relay.data.tabUrl});
1059
1162
  debugLog('Processing new relay:', relay.port, relay.data.tabUrl);
1060
1163
  let ok = false;
@@ -1104,9 +1207,9 @@ async function autoOpenConnectUi() {
1104
1207
  return result;
1105
1208
  }
1106
1209
 
1107
- // Discovery is now passive - only triggered by MCP server requests
1210
+ // Discovery is now passive - only triggered by Chrome AI Bridge requests
1108
1211
  // The extension no longer auto-opens tabs on install/startup
1109
- // MCPサーバーからの明示的な接続要求時のみ動作する
1212
+ // サーバーからの明示的な接続要求時のみ動作する
1110
1213
 
1111
1214
  // Clear any existing discovery alarms from previous sessions
1112
1215
  // This prevents leftover alarms from auto-opening tabs
@@ -1170,21 +1273,38 @@ function scheduleDiscoveryTick(delayMs) {
1170
1273
  }
1171
1274
 
1172
1275
  isDiscoveryRunning = true;
1276
+ lastDiscoveryTickStartedAt = Date.now();
1277
+ lastDiscoveryError = null;
1278
+ lastDiscoverySummary = null;
1173
1279
  try {
1174
1280
  const result = await autoOpenConnectUi();
1281
+ lastDiscoverySummary = {
1282
+ ...result,
1283
+ mode: discoveryMode,
1284
+ intervalMs: getDiscoveryIntervalMs(),
1285
+ };
1175
1286
  updateDiscoveryMode(result);
1176
1287
  } catch (error) {
1288
+ lastDiscoveryError =
1289
+ error && typeof error === 'object' && 'message' in error
1290
+ ? String(error.message)
1291
+ : String(error);
1177
1292
  logWarn('discovery', 'Discovery cycle failed', {
1178
- error: error?.message || String(error),
1293
+ error: lastDiscoveryError,
1179
1294
  });
1180
1295
  emptyDiscoveryStreak = 0;
1181
1296
  setDiscoveryMode(DISCOVERY_MODE.FAST, 'cycle-error');
1182
1297
  } finally {
1298
+ lastDiscoveryTickFinishedAt = Date.now();
1299
+ lastDiscoveryTickDurationMs =
1300
+ lastDiscoveryTickFinishedAt - lastDiscoveryTickStartedAt;
1183
1301
  isDiscoveryRunning = false;
1184
- ensureKeepAliveAlarm('discovery-cycle');
1185
1302
  }
1186
1303
 
1187
1304
  scheduleDiscoveryTick(getDiscoveryIntervalMs());
1305
+ // Must run AFTER scheduleDiscoveryTick so discoveryIntervalId is set,
1306
+ // otherwise shouldKeepAlive() returns false and clears the alarm.
1307
+ ensureKeepAliveAlarm('discovery-cycle');
1188
1308
  }, Math.max(0, delayMs));
1189
1309
  }
1190
1310
 
@@ -1217,6 +1337,7 @@ function kickDiscovery(reason) {
1217
1337
 
1218
1338
  chrome.alarms.onAlarm.addListener((alarm) => {
1219
1339
  if (alarm.name === KEEPALIVE_ALARM) {
1340
+ lastKeepAliveAlarmAt = Date.now();
1220
1341
  const {activeCount, pendingCount} = getConnectionCounts();
1221
1342
  if (activeCount > 0 || pendingCount > 0) {
1222
1343
  logDebug('keepalive', 'Alarm triggered', {activeCount, pendingCount});
@@ -1235,7 +1356,7 @@ chrome.alarms.onAlarm.addListener((alarm) => {
1235
1356
  });
1236
1357
 
1237
1358
  // Note: We no longer register an onAlarm listener for DISCOVERY_ALARM
1238
- // The scheduleDiscovery function is only called on explicit MCP requests
1359
+ // The scheduleDiscovery function is only called on explicit server requests
1239
1360
 
1240
1361
  // Discovery auto-starts on Chrome startup
1241
1362
  // connect.html only opens when user clicks the extension icon
@@ -1259,5 +1380,7 @@ chrome.runtime.onStartup.addListener(() => {
1259
1380
  scheduleDiscovery();
1260
1381
  });
1261
1382
  scheduleDiscovery(); // Start immediately
1383
+ // Ensure keepAlive is active from the start to prevent SW termination during discovery
1384
+ ensureKeepAliveAlarm('startup');
1262
1385
 
1263
1386
  logInfo('background', 'Extension loaded (discovery active)');
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "chrome-ai-bridge Extension",
4
- "version": "2.0.23",
4
+ "version": "2.0.30",
5
5
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqDLiSB+b/gbnQ4zWRP65jnd27KmzpjJyR1JAQIjCD/dNORzTgk6+G0TjYEDZLIDceKHzGJudqnwq4q9g3T1eJ0SECZNXnaoE00WkgXfAUSQn6cmmXR3aQGFky/zbCmxnkRa0vYupxszlhw0yrlSZrIJd/weWF75Byh0zJfZ84kqDDhaj7TlB5laHICnoSLmPTif4mQcUW9oOKmAJPriPw4CWATKZsrQ4X46djxefSmfbqYfb9rttAqJVst40gO0Gsl6GOGxMHMds5Cl9GELc0dI3Gpobw07hQldZb8TeyilI/SnOaeS3HPtrp+KyEgRu8SgRdlrvuq6DeEZsP+kK7wIDAQAB",
6
- "description": "Bridge between Chrome tabs and chrome-ai-bridge MCP server",
6
+ "description": "Bridge between Chrome tabs and Chrome AI Bridge",
7
7
  "permissions": ["debugger", "activeTab", "tabs", "storage", "alarms"],
8
8
  "host_permissions": ["<all_urls>"],
9
9
  "background": {