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
package/build/src/plugin-api.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* By default, scope is derived from the current git root (or cwd fallback),
|
|
5
5
|
* then hashed into a stable namespace.
|
|
6
|
-
* This isolates lock files between different projects using the same
|
|
6
|
+
* This isolates lock files between different projects using the same chrome-ai-bridge instance.
|
|
7
7
|
*/
|
|
8
8
|
import crypto from 'node:crypto';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import { askChatGPTFastWithTimings, askGeminiFastWithTimings, getClient } from '../fast-cdp/fast-chat.js';
|
|
6
|
+
import { askChatGPTFastWithTimings, askGeminiFastWithTimings, getClient, resetConnection } from '../fast-cdp/fast-chat.js';
|
|
7
7
|
/**
|
|
8
8
|
* GEMINI_STUCK_* エラーかどうかを判定
|
|
9
9
|
*/
|
|
@@ -13,19 +13,49 @@ function isGeminiStuckError(error) {
|
|
|
13
13
|
}
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* 接続系エラー(リトライ対象)かどうかを判定。
|
|
18
|
+
* これらのエラーは resetConnection → 再接続で回復する可能性がある。
|
|
19
|
+
*/
|
|
20
|
+
function isRetryableConnectionError(error) {
|
|
21
|
+
if (!(error instanceof Error))
|
|
22
|
+
return false;
|
|
23
|
+
const msg = error.message;
|
|
24
|
+
const patterns = [
|
|
25
|
+
'RELAY_DISCONNECTED',
|
|
26
|
+
'RELAY_STOPPED',
|
|
27
|
+
'RELAY_REQUEST_TIMEOUT',
|
|
28
|
+
'EXT_READY_TIMEOUT',
|
|
29
|
+
'EXT_DISCONNECTED',
|
|
30
|
+
'Extension not connected',
|
|
31
|
+
'WebSocket not open',
|
|
32
|
+
];
|
|
33
|
+
return patterns.some(p => msg.includes(p));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* リトライすべきでないエラーかどうかを判定。
|
|
37
|
+
* 質問送信済みの場合やバジェット超過は再試行しても無意味または有害。
|
|
38
|
+
*/
|
|
39
|
+
function isNonRetryableError(error) {
|
|
40
|
+
if (!(error instanceof Error))
|
|
41
|
+
return false;
|
|
42
|
+
const msg = error.message;
|
|
43
|
+
return msg.includes('TOOL_BUDGET_EXCEEDED') ||
|
|
44
|
+
msg.includes('Timed out waiting for function');
|
|
45
|
+
}
|
|
16
46
|
/**
|
|
17
47
|
* AIに質問を送信し、結果を返す
|
|
18
48
|
* 接続確立からクエリ送信までを一括で行う
|
|
19
|
-
* Gemini
|
|
49
|
+
* 接続エラー・Geminiスタックエラーの場合は自動リトライ(resetConnection → 再接続)
|
|
20
50
|
*/
|
|
21
|
-
export async function askAI(kind, question, debug) {
|
|
51
|
+
export async function askAI(kind, question, debug, budgetMs) {
|
|
22
52
|
const askFn = kind === 'chatgpt' ? askChatGPTFastWithTimings : askGeminiFastWithTimings;
|
|
23
53
|
const label = kind === 'chatgpt' ? 'ChatGPT' : 'Gemini';
|
|
24
|
-
const maxRetries =
|
|
54
|
+
const maxRetries = 2;
|
|
25
55
|
let lastError = null;
|
|
26
56
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
27
57
|
try {
|
|
28
|
-
const result = await askFn(question, debug);
|
|
58
|
+
const result = await askFn(question, debug, budgetMs);
|
|
29
59
|
return {
|
|
30
60
|
provider: label,
|
|
31
61
|
success: true,
|
|
@@ -35,12 +65,26 @@ export async function askAI(kind, question, debug) {
|
|
|
35
65
|
}
|
|
36
66
|
catch (error) {
|
|
37
67
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
if (isNonRetryableError(error) || attempt >= maxRetries) {
|
|
69
|
+
return {
|
|
70
|
+
provider: label,
|
|
71
|
+
success: false,
|
|
72
|
+
answer: '',
|
|
73
|
+
error: lastError.message,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// 接続系エラーまたは Gemini stuck → resetConnection してリトライ
|
|
77
|
+
if (isRetryableConnectionError(error) || isGeminiStuckError(error)) {
|
|
78
|
+
console.error(`[askAI] ${label} error on attempt ${attempt} (${isRetryableConnectionError(error) ? 'connection' : 'stuck'}), resetting and retrying...`);
|
|
79
|
+
try {
|
|
80
|
+
await resetConnection(kind);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// resetConnection failure is not fatal — retry anyway
|
|
84
|
+
}
|
|
42
85
|
continue;
|
|
43
86
|
}
|
|
87
|
+
// Unknown error — don't retry
|
|
44
88
|
return {
|
|
45
89
|
provider: label,
|
|
46
90
|
success: false,
|
|
@@ -49,7 +93,7 @@ export async function askAI(kind, question, debug) {
|
|
|
49
93
|
};
|
|
50
94
|
}
|
|
51
95
|
}
|
|
52
|
-
//
|
|
96
|
+
// Unreachable, but satisfies type checker
|
|
53
97
|
return {
|
|
54
98
|
provider: label,
|
|
55
99
|
success: false,
|
|
@@ -59,10 +103,10 @@ export async function askAI(kind, question, debug) {
|
|
|
59
103
|
}
|
|
60
104
|
/**
|
|
61
105
|
* AIへの接続を確立する(並列接続用)
|
|
62
|
-
* Gemini
|
|
106
|
+
* 接続エラー・Geminiスタックエラーの場合は自動リトライ(resetConnection → 再接続)
|
|
63
107
|
*/
|
|
64
108
|
export async function connectAI(kind) {
|
|
65
|
-
const maxRetries =
|
|
109
|
+
const maxRetries = 2;
|
|
66
110
|
let lastError = null;
|
|
67
111
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
68
112
|
try {
|
|
@@ -71,19 +115,30 @@ export async function connectAI(kind) {
|
|
|
71
115
|
}
|
|
72
116
|
catch (error) {
|
|
73
117
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
if (isNonRetryableError(error) || attempt >= maxRetries) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
error: lastError.message,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (isRetryableConnectionError(error) || isGeminiStuckError(error)) {
|
|
125
|
+
console.error(`[connectAI] ${kind} error on attempt ${attempt} (${isRetryableConnectionError(error) ? 'connection' : 'stuck'}), resetting and retrying...`);
|
|
126
|
+
try {
|
|
127
|
+
await resetConnection(kind);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// resetConnection failure is not fatal — retry anyway
|
|
131
|
+
}
|
|
78
132
|
continue;
|
|
79
133
|
}
|
|
134
|
+
// Unknown error — don't retry
|
|
80
135
|
return {
|
|
81
136
|
success: false,
|
|
82
137
|
error: lastError.message,
|
|
83
138
|
};
|
|
84
139
|
}
|
|
85
140
|
}
|
|
86
|
-
//
|
|
141
|
+
// Unreachable, but satisfies type checker
|
|
87
142
|
return {
|
|
88
143
|
success: false,
|
|
89
144
|
error: lastError?.message || 'Unknown error',
|
|
@@ -61,7 +61,7 @@ export const askChatGptGeminiWeb = defineTool({
|
|
|
61
61
|
schema: {
|
|
62
62
|
question: z
|
|
63
63
|
.string()
|
|
64
|
-
.describe('Question to ask. Do not include secrets/PII. No mention of
|
|
64
|
+
.describe('Question to ask. Do not include secrets/PII. No mention of AI bridging.'),
|
|
65
65
|
debug: z
|
|
66
66
|
.boolean()
|
|
67
67
|
.optional()
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import z from 'zod';
|
|
7
|
-
import {
|
|
7
|
+
import { askAI } from './ai-helpers.js';
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
9
|
import { defineTool } from './ToolDefinition.js';
|
|
10
10
|
/**
|
|
@@ -57,7 +57,7 @@ export const askChatGPTWeb = defineTool({
|
|
|
57
57
|
schema: {
|
|
58
58
|
question: z
|
|
59
59
|
.string()
|
|
60
|
-
.describe('Question to ask. Do not include secrets/PII. No mention of
|
|
60
|
+
.describe('Question to ask. Do not include secrets/PII. No mention of AI bridging.'),
|
|
61
61
|
debug: z
|
|
62
62
|
.boolean()
|
|
63
63
|
.optional()
|
|
@@ -70,15 +70,15 @@ export const askChatGPTWeb = defineTool({
|
|
|
70
70
|
},
|
|
71
71
|
handler: async (request, response) => {
|
|
72
72
|
const { question, debug } = request.params;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
response.appendResponseLine(result.answer
|
|
73
|
+
const result = await askAI('chatgpt', question, debug);
|
|
74
|
+
if (result.success) {
|
|
75
|
+
response.appendResponseLine(result.answer);
|
|
76
76
|
if (debug && result.debug) {
|
|
77
77
|
response.appendResponseLine(formatDebugInfo(result.debug));
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
response.appendResponseLine(`❌ ChatGPT接続に失敗しました: ${
|
|
80
|
+
else {
|
|
81
|
+
response.appendResponseLine(`❌ ChatGPT接続に失敗しました: ${result.error}`);
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import z from 'zod';
|
|
7
|
-
import {
|
|
7
|
+
import { askAI } from './ai-helpers.js';
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
9
|
import { defineTool } from './ToolDefinition.js';
|
|
10
10
|
/**
|
|
@@ -58,7 +58,7 @@ export const askGeminiWeb = defineTool({
|
|
|
58
58
|
schema: {
|
|
59
59
|
question: z
|
|
60
60
|
.string()
|
|
61
|
-
.describe('Question to ask. Do not include secrets/PII. No mention of
|
|
61
|
+
.describe('Question to ask. Do not include secrets/PII. No mention of AI bridging.'),
|
|
62
62
|
debug: z
|
|
63
63
|
.boolean()
|
|
64
64
|
.optional()
|
|
@@ -71,27 +71,15 @@ export const askGeminiWeb = defineTool({
|
|
|
71
71
|
},
|
|
72
72
|
handler: async (request, response) => {
|
|
73
73
|
const { question, debug } = request.params;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
response.appendResponseLine(result.answer || '(空の応答)');
|
|
80
|
-
if (debug && result.debug) {
|
|
81
|
-
response.appendResponseLine(formatDebugInfo(result.debug));
|
|
82
|
-
}
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
87
|
-
// GEMINI_STUCK_* エラーの場合はリトライ
|
|
88
|
-
if (lastError.message.includes('GEMINI_STUCK_') && attempt < maxRetries) {
|
|
89
|
-
console.error(`[ask_gemini_web] Gemini stuck error on attempt ${attempt}, retrying...`);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
response.appendResponseLine(`❌ Gemini接続に失敗しました: ${lastError.message}`);
|
|
93
|
-
return;
|
|
74
|
+
const result = await askAI('gemini', question, debug);
|
|
75
|
+
if (result.success) {
|
|
76
|
+
response.appendResponseLine(result.answer);
|
|
77
|
+
if (debug && result.debug) {
|
|
78
|
+
response.appendResponseLine(formatDebugInfo(result.debug));
|
|
94
79
|
}
|
|
95
80
|
}
|
|
81
|
+
else {
|
|
82
|
+
response.appendResponseLine(`❌ Gemini接続に失敗しました: ${result.error}`);
|
|
83
|
+
}
|
|
96
84
|
},
|
|
97
85
|
});
|
|
@@ -18,19 +18,22 @@ export const optionalTools = [
|
|
|
18
18
|
];
|
|
19
19
|
/**
|
|
20
20
|
* Check if web-llm tools should be loaded.
|
|
21
|
-
* Returns false if
|
|
21
|
+
* Returns false if CAI_DISABLE_WEB_LLM is set to 'true'.
|
|
22
22
|
*/
|
|
23
23
|
export function shouldLoadWebLlmTools() {
|
|
24
|
-
const disable = process.env.MCP_DISABLE_WEB_LLM;
|
|
24
|
+
const disable = process.env.CAI_DISABLE_WEB_LLM || process.env.MCP_DISABLE_WEB_LLM;
|
|
25
|
+
if (process.env.MCP_DISABLE_WEB_LLM && !process.env.CAI_DISABLE_WEB_LLM) {
|
|
26
|
+
console.error('[deprecation] MCP_DISABLE_WEB_LLM is deprecated, use CAI_DISABLE_WEB_LLM instead');
|
|
27
|
+
}
|
|
25
28
|
return disable !== 'true' && disable !== '1';
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* Register optional tools with a ToolRegistry.
|
|
29
|
-
* Respects
|
|
32
|
+
* Respects CAI_DISABLE_WEB_LLM environment variable.
|
|
30
33
|
*/
|
|
31
34
|
export function registerOptionalTools(registry) {
|
|
32
35
|
if (!shouldLoadWebLlmTools()) {
|
|
33
|
-
console.error('[tools] Web-LLM tools disabled via
|
|
36
|
+
console.error('[tools] Web-LLM tools disabled via CAI_DISABLE_WEB_LLM');
|
|
34
37
|
return 0;
|
|
35
38
|
}
|
|
36
39
|
let count = 0;
|
|
@@ -68,7 +71,7 @@ export const WEB_LLM_TOOLS_INFO = {
|
|
|
68
71
|
disclaimer: 'Web-LLM tools (ask_chatgpt_web, ask_gemini_web, ask_chatgpt_gemini_web, take_cdp_snapshot, get_page_dom) are experimental and best-effort. ' +
|
|
69
72
|
'They depend on specific website UIs and may break when those UIs change. ' +
|
|
70
73
|
'For production use, consider using official APIs instead.',
|
|
71
|
-
disableEnvVar: '
|
|
74
|
+
disableEnvVar: 'CAI_DISABLE_WEB_LLM',
|
|
72
75
|
tools: [
|
|
73
76
|
'ask_chatgpt_web',
|
|
74
77
|
'ask_gemini_web',
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-ai-bridge",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.5.2",
|
|
4
|
+
"description": "CLI tool for querying ChatGPT and Gemini via Chrome extension. No Puppeteer required.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"chrome-ai-bridge": "./scripts/cli.mjs",
|
|
8
|
+
"cab": "./scripts/cab"
|
|
9
|
+
},
|
|
7
10
|
"main": "index.js",
|
|
8
11
|
"exports": {
|
|
9
12
|
".": "./index.js",
|
|
@@ -13,14 +16,14 @@
|
|
|
13
16
|
"build": "tsc && cp -R src/extension build/ && node scripts/reload-extension.mjs",
|
|
14
17
|
"build:noext": "tsc && cp -R src/extension build/",
|
|
15
18
|
"reload-ext": "node scripts/reload-extension.mjs",
|
|
16
|
-
"dev": "
|
|
19
|
+
"dev": "CAI_ENV=development node scripts/daemon-wrapper.mjs",
|
|
17
20
|
"typecheck": "tsc --noEmit",
|
|
18
21
|
"format": "eslint --cache --fix . && prettier --write --cache .",
|
|
19
22
|
"check-format": "eslint --cache . && prettier --check --cache .;",
|
|
20
23
|
"start": "npm run build && node build/src/index.js",
|
|
21
|
-
"start-debug": "DEBUG=
|
|
24
|
+
"start-debug": "DEBUG=cab:* DEBUG_COLORS=false npm run build && node build/src/index.js",
|
|
22
25
|
"cleanup": "node scripts/cleanup.mjs",
|
|
23
|
-
"restart
|
|
26
|
+
"restart": "node scripts/cleanup.mjs",
|
|
24
27
|
"test:chatgpt": "npm run build && node scripts/test-fast-chat.mjs chatgpt",
|
|
25
28
|
"test:gemini": "npm run build && node scripts/test-fast-chat.mjs gemini",
|
|
26
29
|
"test:both": "npm run build && node scripts/test-fast-chat.mjs both",
|
|
@@ -31,23 +34,20 @@
|
|
|
31
34
|
"cdp:gemini": "npm run build && node scripts/cdp-snapshot.mjs gemini",
|
|
32
35
|
"measure:chatgpt": "npm run build && node scripts/measure-timings.mjs chatgpt",
|
|
33
36
|
"measure:gemini": "npm run build && node scripts/measure-timings.mjs gemini",
|
|
34
|
-
"test:mcp": "npm run build && node scripts/test-mcp.mjs",
|
|
35
|
-
"test:mcp:chatgpt": "npm run build && node scripts/test-mcp.mjs --chatgpt",
|
|
36
|
-
"test:mcp:gemini": "npm run build && node scripts/test-mcp.mjs --gemini",
|
|
37
|
-
"test:mcp:parallel": "npm run build && node scripts/test-mcp.mjs --parallel",
|
|
38
37
|
"test:network": "npm run build && node scripts/test-network-intercept.mjs",
|
|
39
|
-
"test": "npm run build:noext &&
|
|
38
|
+
"test": "npm run build:noext && npm run typecheck",
|
|
39
|
+
"check:extension-discovery": "node scripts/check-extension-discovery.mjs",
|
|
40
40
|
"discord:collect": "node scripts/discord-readonly-collector.mjs",
|
|
41
41
|
"discord:preflight": "node scripts/discord-readonly-preflight.mjs",
|
|
42
42
|
"discord:status": "node scripts/discord-readonly-status.mjs",
|
|
43
43
|
"docs": "npm run build:noext && node --experimental-strip-types scripts/generate-docs.ts",
|
|
44
|
-
"generate-docs": "npm run docs"
|
|
45
|
-
"sync-server-json-version": "node --experimental-strip-types scripts/sync-server-json-version.ts"
|
|
44
|
+
"generate-docs": "npm run docs"
|
|
46
45
|
},
|
|
47
46
|
"files": [
|
|
48
47
|
"build/src",
|
|
49
48
|
"build/extension",
|
|
50
49
|
"scripts/cli.mjs",
|
|
50
|
+
"scripts/cab",
|
|
51
51
|
"scripts/browser-globals-mock.mjs",
|
|
52
52
|
"README.md",
|
|
53
53
|
"LICENSE",
|
|
@@ -63,21 +63,20 @@
|
|
|
63
63
|
"url": "https://github.com/usedhonda/chrome-ai-bridge/issues"
|
|
64
64
|
},
|
|
65
65
|
"homepage": "https://github.com/usedhonda/chrome-ai-bridge#readme",
|
|
66
|
-
"mcpName": "chrome-ai-bridge",
|
|
67
66
|
"keywords": [
|
|
68
|
-
"mcp",
|
|
69
67
|
"chrome",
|
|
70
68
|
"chrome-extension",
|
|
71
69
|
"chatgpt",
|
|
72
70
|
"gemini",
|
|
73
|
-
"ai-bridge"
|
|
71
|
+
"ai-bridge",
|
|
72
|
+
"cli"
|
|
74
73
|
],
|
|
75
74
|
"dependencies": {
|
|
76
|
-
"@modelcontextprotocol/sdk": "1.18.1",
|
|
77
75
|
"debug": "4.4.3",
|
|
78
76
|
"playwright": "^1.58.1",
|
|
79
77
|
"ws": "^8.19.0",
|
|
80
|
-
"yargs": "18.0.0"
|
|
78
|
+
"yargs": "18.0.0",
|
|
79
|
+
"zod": "^3.25.76"
|
|
81
80
|
},
|
|
82
81
|
"devDependencies": {
|
|
83
82
|
"@eslint/js": "^9.35.0",
|
package/scripts/cab
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cab - Chrome AI Bridge CLI
|
|
3
|
+
# Usage: cab <chatgpt|gemini|both> "question"
|
|
4
|
+
# cab serve - Start daemon directly
|
|
5
|
+
# cab health - Check daemon health
|
|
6
|
+
# cab stop - Stop the daemon
|
|
7
|
+
# cab --help - Show help
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
CAB_PORT="${CAI_IPC_PORT:-9321}"
|
|
12
|
+
CAB_HOST="127.0.0.1"
|
|
13
|
+
CAB_BASE="http://${CAB_HOST}:${CAB_PORT}"
|
|
14
|
+
# Resolve symlinks to get the real script path (macOS compatible)
|
|
15
|
+
_CAB_SELF="$(realpath "$0" 2>/dev/null || python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0")"
|
|
16
|
+
CAB_DIR="$(cd "$(dirname "$_CAB_SELF")" && pwd)"
|
|
17
|
+
CAB_LOG_DIR="${HOME}/.cache/chrome-ai-bridge"
|
|
18
|
+
CAB_LOG="${CAB_LOG_DIR}/cab-daemon.log"
|
|
19
|
+
CAB_STARTUP_TIMEOUT=30
|
|
20
|
+
|
|
21
|
+
usage() {
|
|
22
|
+
cat <<'HELP'
|
|
23
|
+
cab - Chrome AI Bridge CLI
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
cab chatgpt "question" Ask ChatGPT
|
|
27
|
+
cab gemini "question" Ask Gemini
|
|
28
|
+
cab both "question" Ask both in parallel
|
|
29
|
+
|
|
30
|
+
cab serve Start daemon (foreground)
|
|
31
|
+
cab health Check daemon status
|
|
32
|
+
cab stop Stop the daemon
|
|
33
|
+
cab --help Show this help
|
|
34
|
+
|
|
35
|
+
Environment:
|
|
36
|
+
CAI_IPC_PORT Daemon port (default: 9321)
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
cab chatgpt "How to deep copy in JS?"
|
|
40
|
+
cab both "Explain async/await"
|
|
41
|
+
HELP
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Check if daemon is healthy
|
|
45
|
+
check_health() {
|
|
46
|
+
curl -sf "${CAB_BASE}/health" 2>/dev/null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Start daemon in background, wait for health
|
|
50
|
+
ensure_daemon() {
|
|
51
|
+
if check_health >/dev/null 2>&1; then
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
mkdir -p "${CAB_LOG_DIR}"
|
|
56
|
+
echo "Starting cab daemon..." >&2
|
|
57
|
+
|
|
58
|
+
nohup node \
|
|
59
|
+
--import "${CAB_DIR}/browser-globals-mock.mjs" \
|
|
60
|
+
"${CAB_DIR}/../build/src/main.js" \
|
|
61
|
+
--daemon \
|
|
62
|
+
>> "${CAB_LOG}" 2>&1 &
|
|
63
|
+
local daemon_pid=$!
|
|
64
|
+
disown "$daemon_pid" 2>/dev/null || true
|
|
65
|
+
|
|
66
|
+
local waited=0
|
|
67
|
+
while [ "$waited" -lt "$CAB_STARTUP_TIMEOUT" ]; do
|
|
68
|
+
if check_health >/dev/null 2>&1; then
|
|
69
|
+
echo "Daemon ready (pid=${daemon_pid}, port=${CAB_PORT})" >&2
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
# Check if process is still alive
|
|
73
|
+
if ! kill -0 "$daemon_pid" 2>/dev/null; then
|
|
74
|
+
echo "Error: daemon process exited unexpectedly. Check ${CAB_LOG}" >&2
|
|
75
|
+
return 1
|
|
76
|
+
fi
|
|
77
|
+
sleep 1
|
|
78
|
+
waited=$((waited + 1))
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
echo "Error: daemon did not become healthy within ${CAB_STARTUP_TIMEOUT}s" >&2
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Send question via REST API
|
|
86
|
+
ask_ai() {
|
|
87
|
+
local target="$1"
|
|
88
|
+
local question="$2"
|
|
89
|
+
local debug="${3:-false}"
|
|
90
|
+
|
|
91
|
+
ensure_daemon || exit 1
|
|
92
|
+
|
|
93
|
+
local payload
|
|
94
|
+
payload=$(printf '{"target":"%s","question":"%s","debug":%s}' \
|
|
95
|
+
"$target" \
|
|
96
|
+
"$(echo "$question" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ')" \
|
|
97
|
+
"$debug")
|
|
98
|
+
|
|
99
|
+
local response
|
|
100
|
+
response=$(curl -sf -X POST "${CAB_BASE}/api/ask" \
|
|
101
|
+
-H 'Content-Type: application/json' \
|
|
102
|
+
-d "$payload" \
|
|
103
|
+
--max-time 300) || {
|
|
104
|
+
echo "Error: failed to connect to daemon at ${CAB_BASE}" >&2
|
|
105
|
+
exit 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Parse and output results
|
|
109
|
+
local success
|
|
110
|
+
success=$(echo "$response" | node -e "
|
|
111
|
+
let d='';
|
|
112
|
+
process.stdin.on('data',c=>d+=c);
|
|
113
|
+
process.stdin.on('end',()=>{
|
|
114
|
+
try {
|
|
115
|
+
const r=JSON.parse(d);
|
|
116
|
+
if(!r.success && r.error){
|
|
117
|
+
console.error('Error: '+r.error);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
for(const res of r.results||[]){
|
|
121
|
+
if(r.results.length>1) console.log('--- '+res.provider+' ---');
|
|
122
|
+
if(res.success){
|
|
123
|
+
console.log(res.answer);
|
|
124
|
+
} else {
|
|
125
|
+
console.error('['+res.provider+' error] '+(res.error||'Unknown error'));
|
|
126
|
+
}
|
|
127
|
+
if(r.results.length>1) console.log('');
|
|
128
|
+
}
|
|
129
|
+
} catch(e){
|
|
130
|
+
console.error('Error parsing response: '+e.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
" 2>&1) || exit 1
|
|
135
|
+
|
|
136
|
+
echo "$success"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# --- Main ---
|
|
140
|
+
|
|
141
|
+
if [ $# -eq 0 ]; then
|
|
142
|
+
usage
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
case "$1" in
|
|
147
|
+
--help|-h)
|
|
148
|
+
usage
|
|
149
|
+
exit 0
|
|
150
|
+
;;
|
|
151
|
+
health)
|
|
152
|
+
if result=$(check_health 2>/dev/null); then
|
|
153
|
+
echo "$result" | node -e "
|
|
154
|
+
let d='';
|
|
155
|
+
process.stdin.on('data',c=>d+=c);
|
|
156
|
+
process.stdin.on('end',()=>{
|
|
157
|
+
const r=JSON.parse(d);
|
|
158
|
+
console.log('Status: '+r.status);
|
|
159
|
+
console.log('PID: '+r.pid);
|
|
160
|
+
console.log('Version: '+r.version);
|
|
161
|
+
console.log('Sessions: '+r.activeSessions+'/'+r.sessionCapacity);
|
|
162
|
+
});
|
|
163
|
+
"
|
|
164
|
+
else
|
|
165
|
+
echo "Daemon is not running" >&2
|
|
166
|
+
exit 1
|
|
167
|
+
fi
|
|
168
|
+
;;
|
|
169
|
+
serve)
|
|
170
|
+
exec node \
|
|
171
|
+
--import "${CAB_DIR}/browser-globals-mock.mjs" \
|
|
172
|
+
"${CAB_DIR}/../build/src/main.js" \
|
|
173
|
+
--daemon \
|
|
174
|
+
"${@:2}"
|
|
175
|
+
;;
|
|
176
|
+
stop)
|
|
177
|
+
if result=$(check_health 2>/dev/null); then
|
|
178
|
+
pid=$(echo "$result" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).pid));")
|
|
179
|
+
if [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null; then
|
|
180
|
+
echo "Stopped daemon (pid=${pid})" >&2
|
|
181
|
+
else
|
|
182
|
+
echo "Failed to stop daemon" >&2
|
|
183
|
+
exit 1
|
|
184
|
+
fi
|
|
185
|
+
else
|
|
186
|
+
echo "Daemon is not running" >&2
|
|
187
|
+
fi
|
|
188
|
+
;;
|
|
189
|
+
chatgpt|gemini|both)
|
|
190
|
+
if [ $# -lt 2 ]; then
|
|
191
|
+
echo "Error: missing question argument" >&2
|
|
192
|
+
echo "Usage: cab $1 \"your question here\"" >&2
|
|
193
|
+
exit 1
|
|
194
|
+
fi
|
|
195
|
+
ask_ai "$1" "$2" "${3:-false}"
|
|
196
|
+
;;
|
|
197
|
+
*)
|
|
198
|
+
echo "Error: unknown command '$1'" >&2
|
|
199
|
+
usage
|
|
200
|
+
exit 1
|
|
201
|
+
;;
|
|
202
|
+
esac
|
package/scripts/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CLI Entry Point for chrome-ai-bridge
|
|
4
4
|
*
|
|
5
|
-
* Launches
|
|
5
|
+
* Launches Chrome AI Bridge daemon with browser-globals mock in a child process.
|
|
6
6
|
*
|
|
7
7
|
* Why child process:
|
|
8
8
|
* - main.js may intentionally enter a never-returning proxy path.
|
package/build/src/McpResponse.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
export class McpResponse {
|
|
7
|
-
#textResponseLines = [];
|
|
8
|
-
#images = [];
|
|
9
|
-
// Stub methods for interface compatibility (not supported in extension mode)
|
|
10
|
-
setIncludePages(_value) {
|
|
11
|
-
// Not supported in extension-only mode
|
|
12
|
-
}
|
|
13
|
-
setIncludeSnapshot(_value) {
|
|
14
|
-
// Not supported in extension-only mode
|
|
15
|
-
}
|
|
16
|
-
setIncludeNetworkRequests(_value, _options) {
|
|
17
|
-
// Not supported in extension-only mode
|
|
18
|
-
}
|
|
19
|
-
setIncludeConsoleData(_value) {
|
|
20
|
-
// Not supported in extension-only mode
|
|
21
|
-
}
|
|
22
|
-
attachNetworkRequest(_url) {
|
|
23
|
-
// Not supported in extension-only mode
|
|
24
|
-
}
|
|
25
|
-
appendResponseLine(value) {
|
|
26
|
-
this.#textResponseLines.push(value);
|
|
27
|
-
}
|
|
28
|
-
attachImage(value) {
|
|
29
|
-
this.#images.push(value);
|
|
30
|
-
}
|
|
31
|
-
get responseLines() {
|
|
32
|
-
return this.#textResponseLines;
|
|
33
|
-
}
|
|
34
|
-
get images() {
|
|
35
|
-
return this.#images;
|
|
36
|
-
}
|
|
37
|
-
async handle(toolName, _context) {
|
|
38
|
-
return this.format(toolName);
|
|
39
|
-
}
|
|
40
|
-
format(toolName) {
|
|
41
|
-
const response = [`# ${toolName} response`];
|
|
42
|
-
for (const line of this.#textResponseLines) {
|
|
43
|
-
response.push(line);
|
|
44
|
-
}
|
|
45
|
-
const text = {
|
|
46
|
-
type: 'text',
|
|
47
|
-
text: response.join('\n'),
|
|
48
|
-
};
|
|
49
|
-
const images = this.#images.map(imageData => {
|
|
50
|
-
return {
|
|
51
|
-
type: 'image',
|
|
52
|
-
...imageData,
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
return [text, ...images];
|
|
56
|
-
}
|
|
57
|
-
resetResponseLineForTesting() {
|
|
58
|
-
this.#textResponseLines = [];
|
|
59
|
-
}
|
|
60
|
-
}
|