chrome-ai-bridge 2.4.0 → 2.5.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/README.md +28 -40
- package/build/extension/README.md +10 -10
- package/build/extension/background.mjs +94 -26
- package/build/extension/manifest.json +2 -2
- package/build/extension/relay-server.ts +16 -2
- package/build/extension/ui/connect.html +1 -1
- package/build/extension/ui/connect.js +46 -10
- package/build/src/cli.js +6 -1
- package/build/src/config.js +2 -4
- package/build/src/extension/relay-server.js +20 -2
- package/build/src/fast-cdp/agent-context.js +2 -2
- package/build/src/fast-cdp/{mcp-logger.js → debug-logger.js} +11 -11
- package/build/src/fast-cdp/extension-raw.js +51 -5
- package/build/src/fast-cdp/fast-chat.js +166 -101
- package/build/src/logger.js +3 -3
- package/build/src/main.js +104 -568
- package/build/src/plugin-api.js +1 -1
- package/build/src/runtime-scope.js +1 -1
- package/build/src/tools/ai-helpers.js +72 -17
- package/build/src/tools/chatgpt-gemini-web.js +1 -1
- package/build/src/tools/chatgpt-web.js +7 -7
- package/build/src/tools/gemini-web.js +10 -22
- package/build/src/tools/optional-tools.js +8 -5
- package/package.json +17 -18
- package/scripts/cab +202 -0
- package/scripts/cli.mjs +1 -1
- package/build/src/McpResponse.js +0 -60
- package/build/src/stdio-http-proxy.js +0 -157
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* chrome-ai-bridge Connect UI
|
|
3
3
|
* Extension2-style simple flow:
|
|
4
|
-
* 1.
|
|
4
|
+
* 1. Chrome AI Bridge opens connect.html?relayUrl=ws://...
|
|
5
5
|
* 2. Tab list is displayed
|
|
6
6
|
* 3. User selects tab -> Click "Connect"
|
|
7
7
|
* 4. Done
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
class ConnectUI {
|
|
11
11
|
constructor() {
|
|
12
|
-
this.
|
|
12
|
+
this.relayUrl = null;
|
|
13
13
|
this.sessionId = null;
|
|
14
14
|
this.allowTabTakeover = false;
|
|
15
15
|
this.autoMode = false;
|
|
@@ -50,7 +50,7 @@ class ConnectUI {
|
|
|
50
50
|
try {
|
|
51
51
|
// Parse URL parameters (Extension2 style: parameters are always provided)
|
|
52
52
|
const params = new URLSearchParams(window.location.search);
|
|
53
|
-
this.
|
|
53
|
+
this.relayUrl = params.get('relayUrl');
|
|
54
54
|
this.sessionId = params.get('sessionId');
|
|
55
55
|
this.allowTabTakeover = params.get('allowTabTakeover') === 'true';
|
|
56
56
|
this.autoMode = params.get('auto') === 'true';
|
|
@@ -61,8 +61,8 @@ class ConnectUI {
|
|
|
61
61
|
rawTabId && /^\d+$/.test(rawTabId) ? Number(rawTabId) : null;
|
|
62
62
|
|
|
63
63
|
// Validate relay URL
|
|
64
|
-
if (!this.
|
|
65
|
-
this.showError('Missing
|
|
64
|
+
if (!this.relayUrl) {
|
|
65
|
+
this.showError('Missing relayUrl parameter. Make sure Chrome AI Bridge is running.');
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -87,7 +87,7 @@ class ConnectUI {
|
|
|
87
87
|
|
|
88
88
|
validateRelayUrl() {
|
|
89
89
|
try {
|
|
90
|
-
const url = new URL(this.
|
|
90
|
+
const url = new URL(this.relayUrl);
|
|
91
91
|
if (!['127.0.0.1', 'localhost', '::1'].includes(url.hostname)) {
|
|
92
92
|
this.showError('Invalid relay URL: must be loopback address (127.0.0.1)');
|
|
93
93
|
return false;
|
|
@@ -109,7 +109,7 @@ class ConnectUI {
|
|
|
109
109
|
|
|
110
110
|
const relayResponse = await chrome.runtime.sendMessage({
|
|
111
111
|
type: 'connectToRelay',
|
|
112
|
-
|
|
112
|
+
relayUrl: this.relayUrl,
|
|
113
113
|
sessionId: this.sessionId,
|
|
114
114
|
});
|
|
115
115
|
if (!relayResponse || !relayResponse.success) {
|
|
@@ -118,7 +118,7 @@ class ConnectUI {
|
|
|
118
118
|
|
|
119
119
|
const connectResponse = await chrome.runtime.sendMessage({
|
|
120
120
|
type: 'connectToTab',
|
|
121
|
-
|
|
121
|
+
relayUrl: this.relayUrl,
|
|
122
122
|
tabId: this.autoTabId,
|
|
123
123
|
tabUrl: this.autoTabUrl,
|
|
124
124
|
newTab: this.autoNewTab,
|
|
@@ -263,7 +263,7 @@ class ConnectUI {
|
|
|
263
263
|
// Step 1: Connect to relay
|
|
264
264
|
const relayResponse = await chrome.runtime.sendMessage({
|
|
265
265
|
type: 'connectToRelay',
|
|
266
|
-
|
|
266
|
+
relayUrl: this.relayUrl,
|
|
267
267
|
sessionId: this.sessionId,
|
|
268
268
|
});
|
|
269
269
|
|
|
@@ -274,7 +274,7 @@ class ConnectUI {
|
|
|
274
274
|
// Step 2: Connect to tab
|
|
275
275
|
const connectResponse = await chrome.runtime.sendMessage({
|
|
276
276
|
type: 'connectToTab',
|
|
277
|
-
|
|
277
|
+
relayUrl: this.relayUrl,
|
|
278
278
|
tabId: tab.id,
|
|
279
279
|
windowId: tab.windowId,
|
|
280
280
|
sessionId: this.sessionId,
|
|
@@ -340,6 +340,18 @@ class ConnectUI {
|
|
|
340
340
|
return div.innerHTML;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
formatDiscoverySummary(summary) {
|
|
344
|
+
if (!summary || typeof summary !== 'object') {
|
|
345
|
+
return 'n/a';
|
|
346
|
+
}
|
|
347
|
+
return [
|
|
348
|
+
`new=${summary.newRelayCount ?? 0}`,
|
|
349
|
+
`ok=${summary.successCount ?? 0}`,
|
|
350
|
+
`fail=${summary.failureCount ?? 0}`,
|
|
351
|
+
`cooldown=${Boolean(summary.skippedCooldown)}`,
|
|
352
|
+
].join(', ');
|
|
353
|
+
}
|
|
354
|
+
|
|
343
355
|
// ========== Debug Panel Methods ==========
|
|
344
356
|
|
|
345
357
|
initDebugPanel() {
|
|
@@ -398,11 +410,35 @@ class ConnectUI {
|
|
|
398
410
|
// Update stats
|
|
399
411
|
const stats = response.stats;
|
|
400
412
|
const state = response.state;
|
|
413
|
+
const discovery = state.discovery || {};
|
|
401
414
|
this.debugStatsEl.innerHTML = `
|
|
402
415
|
<strong>Total Logs:</strong> ${stats.total} |
|
|
403
416
|
<strong>Active Connections:</strong> ${state.activeConnections?.length || 0} |
|
|
404
417
|
<strong>Pending:</strong> ${state.pendingTabSelection?.length || 0}
|
|
405
418
|
<br>
|
|
419
|
+
<strong>Discovery:</strong>
|
|
420
|
+
mode=${discovery.mode || 'n/a'},
|
|
421
|
+
running=${Boolean(discovery.isRunning)},
|
|
422
|
+
scheduled=${Boolean(discovery.hasScheduledTick)},
|
|
423
|
+
interval=${discovery.intervalMs ?? 'n/a'}ms,
|
|
424
|
+
streak=${discovery.emptyDiscoveryStreak ?? 'n/a'},
|
|
425
|
+
lastPort=${discovery.lastSuccessfulPort ?? 'n/a'}
|
|
426
|
+
<br>
|
|
427
|
+
<strong>Last Tick:</strong>
|
|
428
|
+
started=${discovery.lastTickStartedAt || 'n/a'},
|
|
429
|
+
done=${discovery.lastTickFinishedAt || 'n/a'},
|
|
430
|
+
dur=${discovery.lastTickDurationMs ?? 'n/a'}ms,
|
|
431
|
+
result=${this.formatDiscoverySummary(discovery.lastSummary)},
|
|
432
|
+
error=${discovery.lastError || 'none'}
|
|
433
|
+
<br>
|
|
434
|
+
<strong>Last Probe:</strong>
|
|
435
|
+
at=${discovery.lastRelayProbeAt || 'n/a'},
|
|
436
|
+
port=${discovery.lastRelayProbePort ?? 'n/a'},
|
|
437
|
+
status=${discovery.lastRelayProbeStatus || 'n/a'},
|
|
438
|
+
error=${discovery.lastRelayProbeError || 'none'},
|
|
439
|
+
keepAlive=${Boolean(discovery.keepAliveActive)},
|
|
440
|
+
alarm=${discovery.lastKeepAliveAlarmAt || 'n/a'}
|
|
441
|
+
<br>
|
|
406
442
|
<strong>By Category:</strong>
|
|
407
443
|
${Object.entries(stats.byCategory || {}).map(([cat, count]) => `${cat}: ${count}`).join(', ') || 'none'}
|
|
408
444
|
`;
|
package/build/src/cli.js
CHANGED
|
@@ -21,13 +21,18 @@ export const cliOptions = {
|
|
|
21
21
|
description: 'Port for Extension Bridge WebSocket relay server. Default: 0 (auto-assign).',
|
|
22
22
|
default: 0,
|
|
23
23
|
},
|
|
24
|
+
daemon: {
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
description: 'Run as HTTP-only daemon. Used by cab CLI.',
|
|
27
|
+
default: true,
|
|
28
|
+
},
|
|
24
29
|
};
|
|
25
30
|
export function parseArguments(version, argv = process.argv) {
|
|
26
31
|
const yargsInstance = yargs(hideBin(argv))
|
|
27
32
|
.scriptName('npx chrome-ai-bridge@latest')
|
|
28
33
|
.options(cliOptions)
|
|
29
34
|
.example([
|
|
30
|
-
['$0', 'Start
|
|
35
|
+
['$0', 'Start Chrome AI Bridge daemon (requires chrome-ai-bridge extension)'],
|
|
31
36
|
['$0 --logFile /tmp/log.txt', 'Save logs to a file'],
|
|
32
37
|
['$0 --help', 'Print CLI options'],
|
|
33
38
|
]);
|
package/build/src/config.js
CHANGED
|
@@ -37,15 +37,13 @@ export const GEMINI_CONFIG = {
|
|
|
37
37
|
BASE_URL: 'https://gemini.google.com/',
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* The Primary instance exposes an HTTP endpoint on this port
|
|
42
|
-
* Secondary instances connect as stdio-to-HTTP proxies.
|
|
40
|
+
* HTTP server configuration for the daemon.
|
|
41
|
+
* The Primary instance exposes an HTTP endpoint on this port.
|
|
43
42
|
*/
|
|
44
43
|
export const IPC_CONFIG = {
|
|
45
44
|
port: Number(process.env.CAI_IPC_PORT) || 9321,
|
|
46
45
|
host: '127.0.0.1',
|
|
47
46
|
healthPath: '/health',
|
|
48
|
-
mcpPath: '/mcp',
|
|
49
47
|
};
|
|
50
48
|
/**
|
|
51
49
|
* Get session configuration from environment variables or defaults.
|
|
@@ -377,9 +377,27 @@ export class RelayServer extends EventEmitter {
|
|
|
377
377
|
this.rejectPendingRequests(new Error('RELAY_STOPPED: Relay stopped before request completion'));
|
|
378
378
|
this.stopDiscoveryServer();
|
|
379
379
|
if (this.wss) {
|
|
380
|
+
// Capture ref before nulling so isReady() returns false immediately
|
|
381
|
+
const wss = this.wss;
|
|
382
|
+
this.wss = null;
|
|
380
383
|
return new Promise((resolve) => {
|
|
381
|
-
|
|
382
|
-
|
|
384
|
+
const timeout = setTimeout(() => {
|
|
385
|
+
debugLog('[RelayServer] stop() timed out after 5s — force-terminating remaining clients');
|
|
386
|
+
for (const client of wss.clients) {
|
|
387
|
+
try {
|
|
388
|
+
client.terminate();
|
|
389
|
+
}
|
|
390
|
+
catch { /* ignore */ }
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
wss.close();
|
|
394
|
+
}
|
|
395
|
+
catch { /* ignore */ }
|
|
396
|
+
debugLog('[RelayServer] Server stopped (forced)');
|
|
397
|
+
resolve();
|
|
398
|
+
}, 5000);
|
|
399
|
+
wss.close(() => {
|
|
400
|
+
clearTimeout(timeout);
|
|
383
401
|
debugLog('[RelayServer] Server stopped');
|
|
384
402
|
resolve();
|
|
385
403
|
});
|
|
@@ -18,7 +18,7 @@ let currentAgentId = null;
|
|
|
18
18
|
* 1. If CAI_AGENT_ID environment variable is set, use it + PID
|
|
19
19
|
* 2. Otherwise, generate from PID + timestamp
|
|
20
20
|
*
|
|
21
|
-
* @param clientName Optional client name
|
|
21
|
+
* @param clientName Optional client name (e.g., "claude-code")
|
|
22
22
|
* @returns Unique agent ID
|
|
23
23
|
*/
|
|
24
24
|
export function generateAgentId(clientName) {
|
|
@@ -28,7 +28,7 @@ export function generateAgentId(clientName) {
|
|
|
28
28
|
return `${envAgentId}-${process.pid}`;
|
|
29
29
|
}
|
|
30
30
|
if (clientName) {
|
|
31
|
-
// Use client name if available
|
|
31
|
+
// Use client name if available
|
|
32
32
|
return `${clientName}-${process.pid}`;
|
|
33
33
|
}
|
|
34
34
|
// Fallback: generate from PID + timestamp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Outputs logs to stderr and appends to .local/
|
|
2
|
+
* Debug Logger
|
|
3
|
+
* Outputs logs to stderr and appends to .local/debug.log
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -21,7 +21,7 @@ const LOG_LEVEL_NAMES = {
|
|
|
21
21
|
let currentLogLevel = LogLevel.DEBUG;
|
|
22
22
|
// Log file path
|
|
23
23
|
function getLogFilePath() {
|
|
24
|
-
return path.join(process.cwd(), '.local', '
|
|
24
|
+
return path.join(process.cwd(), '.local', 'debug.log');
|
|
25
25
|
}
|
|
26
26
|
// Max log file size (5MB)
|
|
27
27
|
const MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
@@ -52,7 +52,7 @@ function rotateLogIfNeeded(logPath) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Main logging function
|
|
54
54
|
*/
|
|
55
|
-
export function
|
|
55
|
+
export function debugLog(level, category, message, data) {
|
|
56
56
|
if (level < currentLogLevel)
|
|
57
57
|
return;
|
|
58
58
|
const timestamp = new Date().toISOString();
|
|
@@ -76,35 +76,35 @@ export function mcpLog(level, category, message, data) {
|
|
|
76
76
|
}
|
|
77
77
|
// Convenience methods
|
|
78
78
|
export function logDebug(category, message, data) {
|
|
79
|
-
|
|
79
|
+
debugLog(LogLevel.DEBUG, category, message, data);
|
|
80
80
|
}
|
|
81
81
|
export function logInfo(category, message, data) {
|
|
82
|
-
|
|
82
|
+
debugLog(LogLevel.INFO, category, message, data);
|
|
83
83
|
}
|
|
84
84
|
export function logWarn(category, message, data) {
|
|
85
|
-
|
|
85
|
+
debugLog(LogLevel.WARN, category, message, data);
|
|
86
86
|
}
|
|
87
87
|
export function logError(category, message, data) {
|
|
88
|
-
|
|
88
|
+
debugLog(LogLevel.ERROR, category, message, data);
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Log connection state changes
|
|
92
92
|
*/
|
|
93
93
|
export function logConnectionState(kind, state, details) {
|
|
94
94
|
const level = state === 'disconnected' || state === 'unhealthy' ? LogLevel.WARN : LogLevel.INFO;
|
|
95
|
-
|
|
95
|
+
debugLog(level, 'connection', `${kind} ${state}`, details);
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
98
|
* Log relay server events
|
|
99
99
|
*/
|
|
100
100
|
export function logRelay(event, details) {
|
|
101
101
|
const level = event === 'error' || event === 'timeout' ? LogLevel.ERROR : LogLevel.INFO;
|
|
102
|
-
|
|
102
|
+
debugLog(level, 'relay', event, details);
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* Log extension communication
|
|
106
106
|
*/
|
|
107
107
|
export function logExtension(event, details) {
|
|
108
108
|
const level = event === 'timeout' ? LogLevel.ERROR : LogLevel.INFO;
|
|
109
|
-
|
|
109
|
+
debugLog(level, 'extension', event, details);
|
|
110
110
|
}
|
|
@@ -1,14 +1,59 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
4
7
|
import { RelayServer } from '../extension/relay-server.js';
|
|
5
|
-
import { logRelay, logExtension, logInfo, logError } from './
|
|
6
|
-
// Stable extension ID (from manifest.json key)
|
|
7
|
-
const EXTENSION_ID = 'ibjplbopgmcacpmfpnaeoloepdhenlbm';
|
|
8
|
+
import { logRelay, logExtension, logInfo, logError } from './debug-logger.js';
|
|
8
9
|
// Wake connect page disabled by default — it opens a chrome-extension:// URL
|
|
9
10
|
// that gets ERR_BLOCKED_BY_CLIENT and annoys users. Discovery polling is the
|
|
10
11
|
// primary mechanism; wake is only useful in rare edge cases.
|
|
11
12
|
const ENABLE_WAKE_CONNECT_PAGE = process.env.CAI_ENABLE_WAKE_CONNECT_PAGE === '1';
|
|
13
|
+
const EXTENSION_ID_ALPHABET = 'abcdefghijklmnop';
|
|
14
|
+
const EXTENSION_ID_PATTERN = /^[a-p]{32}$/;
|
|
15
|
+
const MANIFEST_FILE_URL = new URL('../../extension/manifest.json', import.meta.url);
|
|
16
|
+
let cachedExtensionId = null;
|
|
17
|
+
function deriveExtensionIdFromManifestKey(manifestKey) {
|
|
18
|
+
const derKey = Buffer.from(manifestKey, 'base64');
|
|
19
|
+
if (derKey.length === 0) {
|
|
20
|
+
throw new Error('EXT_CONFIG_ERROR: manifest key is empty or invalid base64');
|
|
21
|
+
}
|
|
22
|
+
const hash = crypto.createHash('sha256').update(derKey).digest();
|
|
23
|
+
let extensionId = '';
|
|
24
|
+
for (const byte of hash.subarray(0, 16)) {
|
|
25
|
+
extensionId += EXTENSION_ID_ALPHABET[byte >> 4];
|
|
26
|
+
extensionId += EXTENSION_ID_ALPHABET[byte & 0x0f];
|
|
27
|
+
}
|
|
28
|
+
return extensionId;
|
|
29
|
+
}
|
|
30
|
+
function resolveExtensionId() {
|
|
31
|
+
if (cachedExtensionId) {
|
|
32
|
+
return cachedExtensionId;
|
|
33
|
+
}
|
|
34
|
+
const envId = process.env.CAI_EXTENSION_ID?.trim();
|
|
35
|
+
if (envId) {
|
|
36
|
+
if (!EXTENSION_ID_PATTERN.test(envId)) {
|
|
37
|
+
throw new Error(`EXT_CONFIG_ERROR: CAI_EXTENSION_ID must match ${EXTENSION_ID_PATTERN.source}, got "${envId}"`);
|
|
38
|
+
}
|
|
39
|
+
cachedExtensionId = envId;
|
|
40
|
+
return cachedExtensionId;
|
|
41
|
+
}
|
|
42
|
+
const manifestPath = fileURLToPath(MANIFEST_FILE_URL);
|
|
43
|
+
try {
|
|
44
|
+
const manifestRaw = fs.readFileSync(manifestPath, 'utf8');
|
|
45
|
+
const manifest = JSON.parse(manifestRaw);
|
|
46
|
+
if (!manifest.key) {
|
|
47
|
+
throw new Error('manifest key is missing');
|
|
48
|
+
}
|
|
49
|
+
cachedExtensionId = deriveExtensionIdFromManifestKey(manifest.key);
|
|
50
|
+
return cachedExtensionId;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const relativePath = path.relative(process.cwd(), manifestPath);
|
|
54
|
+
throw new Error(`EXT_CONFIG_ERROR: failed to resolve extension ID from ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
12
57
|
/**
|
|
13
58
|
* Get Chrome executable path for current platform
|
|
14
59
|
*/
|
|
@@ -96,8 +141,9 @@ function spawnChromeWithConnectUrl(connectUrl) {
|
|
|
96
141
|
}
|
|
97
142
|
}
|
|
98
143
|
function buildConnectUrl(options) {
|
|
144
|
+
const extensionId = resolveExtensionId();
|
|
99
145
|
const params = new URLSearchParams();
|
|
100
|
-
params.set('
|
|
146
|
+
params.set('relayUrl', options.wsUrl);
|
|
101
147
|
params.set('sessionId', options.sessionId);
|
|
102
148
|
if (options.tabUrl)
|
|
103
149
|
params.set('tabUrl', options.tabUrl);
|
|
@@ -109,7 +155,7 @@ function buildConnectUrl(options) {
|
|
|
109
155
|
params.set('allowTabTakeover', 'true');
|
|
110
156
|
if (options.auto)
|
|
111
157
|
params.set('auto', 'true');
|
|
112
|
-
return `chrome-extension://${
|
|
158
|
+
return `chrome-extension://${extensionId}/ui/connect.html?${params.toString()}`;
|
|
113
159
|
}
|
|
114
160
|
export async function connectViaExtensionRaw(options) {
|
|
115
161
|
const startTime = Date.now();
|