chrome-ai-bridge 2.0.19 → 2.2.0
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/build/src/config.js +15 -0
- package/build/src/fast-cdp/agent-context.js +108 -0
- package/build/src/fast-cdp/fast-chat.js +85 -126
- package/build/src/fast-cdp/session-manager.js +214 -0
- package/build/src/main.js +20 -0
- package/package.json +1 -1
package/build/src/config.js
CHANGED
|
@@ -36,3 +36,18 @@ export const GEMINI_CONFIG = {
|
|
|
36
36
|
*/
|
|
37
37
|
BASE_URL: 'https://gemini.google.com/',
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Get session configuration from environment variables or defaults.
|
|
41
|
+
*/
|
|
42
|
+
export function getSessionConfig() {
|
|
43
|
+
const raw = {
|
|
44
|
+
ttl: Number(process.env.CAI_SESSION_TTL_MINUTES),
|
|
45
|
+
max: Number(process.env.CAI_MAX_AGENTS),
|
|
46
|
+
interval: Number(process.env.CAI_CLEANUP_INTERVAL_MINUTES),
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
sessionTtlMinutes: raw.ttl > 0 ? raw.ttl : 30,
|
|
50
|
+
maxAgents: raw.max > 0 ? Math.floor(raw.max) : 10,
|
|
51
|
+
cleanupIntervalMinutes: raw.interval > 0 ? raw.interval : 5,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Map of agent ID to connection state
|
|
8
|
+
*/
|
|
9
|
+
const agentConnections = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Current agent ID for this process
|
|
12
|
+
*/
|
|
13
|
+
let currentAgentId = null;
|
|
14
|
+
/**
|
|
15
|
+
* Generate a unique agent ID.
|
|
16
|
+
*
|
|
17
|
+
* Strategy (hybrid):
|
|
18
|
+
* 1. If CAI_AGENT_ID environment variable is set, use it + PID
|
|
19
|
+
* 2. Otherwise, generate from PID + timestamp
|
|
20
|
+
*
|
|
21
|
+
* @param clientName Optional client name from MCP initialize (e.g., "claude-code")
|
|
22
|
+
* @returns Unique agent ID
|
|
23
|
+
*/
|
|
24
|
+
export function generateAgentId(clientName) {
|
|
25
|
+
const envAgentId = process.env.CAI_AGENT_ID;
|
|
26
|
+
if (envAgentId) {
|
|
27
|
+
// Environment variable takes precedence (allows explicit control)
|
|
28
|
+
return `${envAgentId}-${process.pid}`;
|
|
29
|
+
}
|
|
30
|
+
if (clientName) {
|
|
31
|
+
// Use client name if available (from MCP initialize)
|
|
32
|
+
return `${clientName}-${process.pid}`;
|
|
33
|
+
}
|
|
34
|
+
// Fallback: generate from PID + timestamp
|
|
35
|
+
return `agent-${process.pid}-${Date.now()}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Set the current agent ID for this process.
|
|
39
|
+
* Should be called once at process startup.
|
|
40
|
+
*
|
|
41
|
+
* @param id Agent ID to set
|
|
42
|
+
*/
|
|
43
|
+
export function setAgentId(id) {
|
|
44
|
+
if (currentAgentId !== null && currentAgentId !== id) {
|
|
45
|
+
console.error(`[agent-context] Warning: Agent ID changed from ${currentAgentId} to ${id}`);
|
|
46
|
+
}
|
|
47
|
+
currentAgentId = id;
|
|
48
|
+
console.error(`[agent-context] Agent ID set: ${id}`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the current agent ID.
|
|
52
|
+
*
|
|
53
|
+
* @returns Current agent ID
|
|
54
|
+
* @throws Error if agent ID is not set
|
|
55
|
+
*/
|
|
56
|
+
export function getAgentId() {
|
|
57
|
+
if (!currentAgentId) {
|
|
58
|
+
throw new Error('Agent ID not set. Call setAgentId() first.');
|
|
59
|
+
}
|
|
60
|
+
return currentAgentId;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if agent ID is set.
|
|
64
|
+
*
|
|
65
|
+
* @returns true if agent ID is set
|
|
66
|
+
*/
|
|
67
|
+
export function hasAgentId() {
|
|
68
|
+
return currentAgentId !== null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get or create connection state for the current agent.
|
|
72
|
+
*
|
|
73
|
+
* @returns AgentConnection for the current agent
|
|
74
|
+
*/
|
|
75
|
+
export function getAgentConnection() {
|
|
76
|
+
const agentId = getAgentId();
|
|
77
|
+
let conn = agentConnections.get(agentId);
|
|
78
|
+
if (!conn) {
|
|
79
|
+
conn = {
|
|
80
|
+
chatgptClient: null,
|
|
81
|
+
geminiClient: null,
|
|
82
|
+
chatgptRelay: null,
|
|
83
|
+
geminiRelay: null,
|
|
84
|
+
lastAccess: new Date(),
|
|
85
|
+
};
|
|
86
|
+
agentConnections.set(agentId, conn);
|
|
87
|
+
console.error(`[agent-context] Created new connection for agent: ${agentId}`);
|
|
88
|
+
}
|
|
89
|
+
// Update last access time
|
|
90
|
+
conn.lastAccess = new Date();
|
|
91
|
+
return conn;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get all agent connections (for cleanup purposes).
|
|
95
|
+
*
|
|
96
|
+
* @returns Map of agent ID to AgentConnection
|
|
97
|
+
*/
|
|
98
|
+
export function getAllAgentConnections() {
|
|
99
|
+
return agentConnections;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Clear all agent connections.
|
|
103
|
+
* Used during shutdown.
|
|
104
|
+
*/
|
|
105
|
+
export function clearAllAgentConnections() {
|
|
106
|
+
agentConnections.clear();
|
|
107
|
+
console.error('[agent-context] Cleared all agent connections');
|
|
108
|
+
}
|
|
@@ -6,11 +6,49 @@ import { logConnectionState, logInfo, logError, logWarn } from './mcp-logger.js'
|
|
|
6
6
|
import { DOM_UTILS_CODE } from './utils/index.js';
|
|
7
7
|
import { getDriver } from './drivers/index.js';
|
|
8
8
|
import { NetworkInterceptor } from './network-interceptor.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
import { getAgentConnection, getAllAgentConnections, clearAllAgentConnections, hasAgentId, } from './agent-context.js';
|
|
10
|
+
import { saveAgentSession, getPreferredSessionV2, clearAgentSession } from './session-manager.js';
|
|
11
|
+
/**
|
|
12
|
+
* Get current agent's client for the specified kind.
|
|
13
|
+
* Returns null if not connected.
|
|
14
|
+
*/
|
|
15
|
+
function getClientFromAgent(kind) {
|
|
16
|
+
if (!hasAgentId()) {
|
|
17
|
+
// Fallback for backward compatibility (no agent ID set)
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const conn = getAgentConnection();
|
|
21
|
+
return kind === 'chatgpt' ? conn.chatgptClient : conn.geminiClient;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get current agent's relay for the specified kind.
|
|
25
|
+
* Returns null if not connected.
|
|
26
|
+
*/
|
|
27
|
+
function getRelayFromAgent(kind) {
|
|
28
|
+
if (!hasAgentId()) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const conn = getAgentConnection();
|
|
32
|
+
return kind === 'chatgpt' ? conn.chatgptRelay : conn.geminiRelay;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Set client and relay for the current agent.
|
|
36
|
+
*/
|
|
37
|
+
function setClientForAgent(kind, client, relay) {
|
|
38
|
+
if (!hasAgentId()) {
|
|
39
|
+
console.error('[fast-chat] Warning: setClientForAgent called without agent ID');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const conn = getAgentConnection();
|
|
43
|
+
if (kind === 'chatgpt') {
|
|
44
|
+
conn.chatgptClient = client;
|
|
45
|
+
conn.chatgptRelay = relay;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
conn.geminiClient = client;
|
|
49
|
+
conn.geminiRelay = relay;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
14
52
|
const CONNECT_REUSE_TIMEOUT_MS = Number(process.env.MCP_CONNECT_REUSE_TIMEOUT_MS || '7000');
|
|
15
53
|
const CONNECT_NEWTAB_TIMEOUT_MS = Number(process.env.MCP_CONNECT_NEWTAB_TIMEOUT_MS || '12000');
|
|
16
54
|
function nowMs() {
|
|
@@ -78,9 +116,6 @@ async function waitForStableCount(client, countExpr, maxWaitMs = 3000, pollInter
|
|
|
78
116
|
function getProjectName() {
|
|
79
117
|
return path.basename(process.cwd()) || 'default';
|
|
80
118
|
}
|
|
81
|
-
function getSessionPath() {
|
|
82
|
-
return path.join(process.cwd(), '.local', 'chrome-ai-bridge', 'sessions.json');
|
|
83
|
-
}
|
|
84
119
|
function getHistoryPath() {
|
|
85
120
|
return path.join(process.cwd(), '.local', 'chrome-ai-bridge', 'history.jsonl');
|
|
86
121
|
}
|
|
@@ -129,93 +164,52 @@ async function rotateHistoryIfNeeded() {
|
|
|
129
164
|
}
|
|
130
165
|
}
|
|
131
166
|
}
|
|
132
|
-
async function loadSessions() {
|
|
133
|
-
try {
|
|
134
|
-
const data = await fs.readFile(getSessionPath(), 'utf-8');
|
|
135
|
-
const parsed = JSON.parse(data);
|
|
136
|
-
if (parsed && typeof parsed === 'object' && parsed.projects) {
|
|
137
|
-
return parsed;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
// ignore
|
|
142
|
-
}
|
|
143
|
-
return { projects: {} };
|
|
144
|
-
}
|
|
145
|
-
async function saveSession(kind, url, tabId) {
|
|
146
|
-
const project = getProjectName();
|
|
147
|
-
const sessions = await loadSessions();
|
|
148
|
-
if (!sessions.projects[project]) {
|
|
149
|
-
sessions.projects[project] = {};
|
|
150
|
-
}
|
|
151
|
-
const entry = {
|
|
152
|
-
url,
|
|
153
|
-
lastUsed: new Date().toISOString(),
|
|
154
|
-
};
|
|
155
|
-
if (tabId !== undefined) {
|
|
156
|
-
entry.tabId = tabId;
|
|
157
|
-
}
|
|
158
|
-
sessions.projects[project][kind] = entry;
|
|
159
|
-
const targetPath = getSessionPath();
|
|
160
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
161
|
-
await fs.writeFile(targetPath, JSON.stringify(sessions, null, 2), 'utf-8');
|
|
162
|
-
}
|
|
163
|
-
async function clearGeminiSession() {
|
|
164
|
-
const project = getProjectName();
|
|
165
|
-
const sessions = await loadSessions();
|
|
166
|
-
if (sessions.projects[project]?.gemini) {
|
|
167
|
-
delete sessions.projects[project].gemini;
|
|
168
|
-
const targetPath = getSessionPath();
|
|
169
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
170
|
-
await fs.writeFile(targetPath, JSON.stringify(sessions, null, 2), 'utf-8');
|
|
171
|
-
console.error(`[Gemini] Session cleared for project: ${project}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
167
|
/**
|
|
175
168
|
* キャッシュされたGeminiクライアントをクリア(リトライ用)
|
|
176
169
|
*/
|
|
177
170
|
export function clearGeminiClient() {
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
const client = getClientFromAgent('gemini');
|
|
172
|
+
const relay = getRelayFromAgent('gemini');
|
|
173
|
+
if (client) {
|
|
180
174
|
console.error('[Gemini] Cached client cleared');
|
|
181
175
|
}
|
|
182
|
-
if (
|
|
176
|
+
if (relay) {
|
|
183
177
|
try {
|
|
184
|
-
|
|
178
|
+
relay.stop();
|
|
185
179
|
}
|
|
186
180
|
catch {
|
|
187
181
|
// ignore stop errors
|
|
188
182
|
}
|
|
189
|
-
geminiRelay = null;
|
|
190
183
|
}
|
|
184
|
+
setClientForAgent('gemini', null, null);
|
|
191
185
|
}
|
|
192
186
|
/**
|
|
193
187
|
* 全接続をクリーンアップ(プロセス終了時用)
|
|
194
188
|
* MCPサーバー終了時にゾンビプロセスを防ぐために使用
|
|
195
189
|
*/
|
|
196
190
|
export async function cleanupAllConnections() {
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
chatgptClient = null;
|
|
208
|
-
// Gemini
|
|
209
|
-
if (geminiRelay) {
|
|
210
|
-
try {
|
|
211
|
-
await geminiRelay.stop();
|
|
191
|
+
// Snapshot entries before clearing to avoid mutation during iteration
|
|
192
|
+
const entries = Array.from(getAllAgentConnections().entries());
|
|
193
|
+
for (const [, conn] of entries) {
|
|
194
|
+
if (conn.chatgptRelay) {
|
|
195
|
+
try {
|
|
196
|
+
await conn.chatgptRelay.stop();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// ignore stop errors
|
|
200
|
+
}
|
|
212
201
|
}
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
if (conn.geminiRelay) {
|
|
203
|
+
try {
|
|
204
|
+
await conn.geminiRelay.stop();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// ignore stop errors
|
|
208
|
+
}
|
|
215
209
|
}
|
|
216
|
-
geminiRelay = null;
|
|
217
210
|
}
|
|
218
|
-
|
|
211
|
+
// Clear all at once after iteration
|
|
212
|
+
clearAllAgentConnections();
|
|
219
213
|
console.error('[fast-cdp] All connections cleaned up');
|
|
220
214
|
}
|
|
221
215
|
/**
|
|
@@ -278,20 +272,6 @@ async function saveDebug(kind, payload) {
|
|
|
278
272
|
const file = path.join(targetDir, `${kind}-${Date.now()}.json`);
|
|
279
273
|
await fs.writeFile(file, JSON.stringify(payload, null, 2), 'utf-8');
|
|
280
274
|
}
|
|
281
|
-
async function getPreferredSession(kind) {
|
|
282
|
-
const project = getProjectName();
|
|
283
|
-
const sessions = await loadSessions();
|
|
284
|
-
const entry = sessions.projects[project]?.[kind];
|
|
285
|
-
return {
|
|
286
|
-
url: entry?.url || null,
|
|
287
|
-
tabId: entry?.tabId,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
// Keep for backward compatibility
|
|
291
|
-
async function getPreferredUrl(kind) {
|
|
292
|
-
const session = await getPreferredSession(kind);
|
|
293
|
-
return session.url;
|
|
294
|
-
}
|
|
295
275
|
function normalizeGeminiResponse(text, question) {
|
|
296
276
|
if (!text)
|
|
297
277
|
return '';
|
|
@@ -323,7 +303,7 @@ function normalizeGeminiResponse(text, question) {
|
|
|
323
303
|
async function createConnection(kind) {
|
|
324
304
|
const startTime = Date.now();
|
|
325
305
|
logConnectionState(kind, 'connecting');
|
|
326
|
-
const preferredSession = await
|
|
306
|
+
const preferredSession = await getPreferredSessionV2(kind);
|
|
327
307
|
const preferred = preferredSession.url;
|
|
328
308
|
const preferredTabId = preferredSession.tabId;
|
|
329
309
|
const defaultUrl = kind === 'chatgpt'
|
|
@@ -377,14 +357,7 @@ async function createConnection(kind) {
|
|
|
377
357
|
await client.waitForFunction(`location.href !== 'about:blank' && document.readyState === 'complete'`, 3000);
|
|
378
358
|
}
|
|
379
359
|
// クライアントとRelay参照を保存
|
|
380
|
-
|
|
381
|
-
chatgptClient = client;
|
|
382
|
-
chatgptRelay = relayResult.relay;
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
geminiClient = client;
|
|
386
|
-
geminiRelay = relayResult.relay;
|
|
387
|
-
}
|
|
360
|
+
setClientForAgent(kind, client, relayResult.relay);
|
|
388
361
|
const elapsed = Date.now() - startTime;
|
|
389
362
|
logConnectionState(kind, 'connected', { elapsed, reused: true });
|
|
390
363
|
console.error(`[fast-cdp] ${kind} reused existing tab successfully`);
|
|
@@ -428,14 +401,7 @@ async function createConnection(kind) {
|
|
|
428
401
|
console.error(`[fast-cdp] ${kind} setFocusEmulationEnabled failed (non-critical):`, e instanceof Error ? e.message : String(e));
|
|
429
402
|
}
|
|
430
403
|
// クライアントとRelay参照を保存
|
|
431
|
-
|
|
432
|
-
chatgptClient = client;
|
|
433
|
-
chatgptRelay = relayResult.relay;
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
geminiClient = client;
|
|
437
|
-
geminiRelay = relayResult.relay;
|
|
438
|
-
}
|
|
404
|
+
setClientForAgent(kind, client, relayResult.relay);
|
|
439
405
|
// 新規タブ作成後、ページが読み込まれるまで待機(about:blank でなくなるまで)
|
|
440
406
|
const debugUrl = await client.evaluate('location.href');
|
|
441
407
|
if (debugUrl === 'about:blank') {
|
|
@@ -469,8 +435,8 @@ async function createConnection(kind) {
|
|
|
469
435
|
* @public 外部から接続を事前確立するためにエクスポート
|
|
470
436
|
*/
|
|
471
437
|
export async function getClient(kind) {
|
|
472
|
-
|
|
473
|
-
|
|
438
|
+
const existing = getClientFromAgent(kind);
|
|
439
|
+
logInfo('fast-chat', `getClient called`, { kind, hasExisting: !!existing });
|
|
474
440
|
// 既存接続がある場合、健全性をチェック
|
|
475
441
|
if (existing) {
|
|
476
442
|
logInfo('fast-chat', `Checking health of existing ${kind} connection`);
|
|
@@ -484,7 +450,7 @@ export async function getClient(kind) {
|
|
|
484
450
|
logConnectionState(kind, 'reconnecting');
|
|
485
451
|
console.error(`[fast-cdp] ${kind} connection lost, reconnecting...`);
|
|
486
452
|
// 古いRelayServerを停止
|
|
487
|
-
const oldRelay = kind
|
|
453
|
+
const oldRelay = getRelayFromAgent(kind);
|
|
488
454
|
if (oldRelay) {
|
|
489
455
|
logInfo('fast-chat', `Stopping stale ${kind} RelayServer`);
|
|
490
456
|
console.error(`[fast-cdp] Stopping stale ${kind} RelayServer`);
|
|
@@ -495,14 +461,7 @@ export async function getClient(kind) {
|
|
|
495
461
|
});
|
|
496
462
|
}
|
|
497
463
|
// キャッシュをクリア
|
|
498
|
-
|
|
499
|
-
chatgptClient = null;
|
|
500
|
-
chatgptRelay = null;
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
geminiClient = null;
|
|
504
|
-
geminiRelay = null;
|
|
505
|
-
}
|
|
464
|
+
setClientForAgent(kind, null, null);
|
|
506
465
|
}
|
|
507
466
|
// 新しい接続を作成
|
|
508
467
|
return await createConnection(kind);
|
|
@@ -1947,7 +1906,7 @@ async function askChatGPTFastInternal(question, debug) {
|
|
|
1947
1906
|
console.error(`[ChatGPT] Response extracted: ${finalAnswer.slice(0, 100)}...`);
|
|
1948
1907
|
const finalUrl = await client.evaluate('location.href');
|
|
1949
1908
|
if (finalUrl && finalUrl.includes('chatgpt.com')) {
|
|
1950
|
-
await
|
|
1909
|
+
await saveAgentSession('chatgpt', finalUrl);
|
|
1951
1910
|
}
|
|
1952
1911
|
timings.waitResponseMs = nowMs() - tWaitResp;
|
|
1953
1912
|
timings.totalMs = nowMs() - t0;
|
|
@@ -2097,7 +2056,7 @@ async function askChatGPTViaDriver(question, debug) {
|
|
|
2097
2056
|
// セッション保存
|
|
2098
2057
|
const finalUrl = await driver.getCurrentUrl();
|
|
2099
2058
|
if (finalUrl.includes('chatgpt.com')) {
|
|
2100
|
-
await
|
|
2059
|
+
await saveAgentSession('chatgpt', finalUrl);
|
|
2101
2060
|
}
|
|
2102
2061
|
timings.totalMs = nowMs() - t0;
|
|
2103
2062
|
// 履歴保存
|
|
@@ -2164,7 +2123,7 @@ async function askGeminiViaDriver(question, debug) {
|
|
|
2164
2123
|
// セッション保存
|
|
2165
2124
|
const finalUrl = await driver.getCurrentUrl();
|
|
2166
2125
|
if (finalUrl.includes('gemini.google.com')) {
|
|
2167
|
-
await
|
|
2126
|
+
await saveAgentSession('gemini', finalUrl);
|
|
2168
2127
|
}
|
|
2169
2128
|
timings.totalMs = nowMs() - t0;
|
|
2170
2129
|
// 履歴保存
|
|
@@ -2218,11 +2177,11 @@ async function askGeminiFastInternal(question, debug) {
|
|
|
2218
2177
|
const tUrl = nowMs();
|
|
2219
2178
|
const currentUrl = await client.evaluate('location.href');
|
|
2220
2179
|
if (!currentUrl || !currentUrl.includes('gemini.google.com')) {
|
|
2221
|
-
const preferred = await
|
|
2180
|
+
const preferred = (await getPreferredSessionV2('gemini')).url;
|
|
2222
2181
|
await navigate(client, preferred || 'https://gemini.google.com/');
|
|
2223
2182
|
}
|
|
2224
2183
|
else {
|
|
2225
|
-
const preferred = await
|
|
2184
|
+
const preferred = (await getPreferredSessionV2('gemini')).url;
|
|
2226
2185
|
if (preferred && !currentUrl.startsWith(preferred)) {
|
|
2227
2186
|
await navigate(client, preferred);
|
|
2228
2187
|
}
|
|
@@ -2247,7 +2206,7 @@ async function askGeminiFastInternal(question, debug) {
|
|
|
2247
2206
|
if (stuckCheckResult.isStuck) {
|
|
2248
2207
|
console.error(`[Gemini] Existing chat appears stuck (stop button detected for ${stuckCheckResult.waitedMs}ms). Clearing session and retrying.`);
|
|
2249
2208
|
// セッションとクライアントをクリア
|
|
2250
|
-
await
|
|
2209
|
+
await clearAgentSession('gemini');
|
|
2251
2210
|
clearGeminiClient();
|
|
2252
2211
|
// エラーを投げて、呼び出し元でリトライを促す
|
|
2253
2212
|
throw new Error('GEMINI_STUCK_EXISTING_CHAT: Previous chat appears stuck (stop button visible). Session cleared, please retry.');
|
|
@@ -2517,7 +2476,7 @@ async function askGeminiFastInternal(question, debug) {
|
|
|
2517
2476
|
if (stopButtonConsecutiveCount >= forceNewChatThreshold) {
|
|
2518
2477
|
console.error(`[Gemini] Stop button detected for ${forceNewChatThreshold * 0.5}s - clearing session and forcing new chat`);
|
|
2519
2478
|
// セッションとクライアントをクリアして新規チャットを強制
|
|
2520
|
-
await
|
|
2479
|
+
await clearAgentSession('gemini');
|
|
2521
2480
|
clearGeminiClient();
|
|
2522
2481
|
// エラーを投げて再試行を促す
|
|
2523
2482
|
throw new Error('GEMINI_STUCK_STOP_BUTTON: Previous response appears stuck. Session cleared, please retry.');
|
|
@@ -2955,7 +2914,7 @@ async function askGeminiFastInternal(question, debug) {
|
|
|
2955
2914
|
console.error(`[Gemini] Response extracted: ${normalized.slice(0, 100)}...`);
|
|
2956
2915
|
const finalUrl = await client.evaluate('location.href');
|
|
2957
2916
|
if (finalUrl && finalUrl.includes('gemini.google.com')) {
|
|
2958
|
-
await
|
|
2917
|
+
await saveAgentSession('gemini', finalUrl);
|
|
2959
2918
|
}
|
|
2960
2919
|
timings.waitResponseMs = nowMs() - tWaitResp;
|
|
2961
2920
|
timings.totalMs = nowMs() - t0;
|
|
@@ -3076,7 +3035,7 @@ export async function takeCdpSnapshot(kind, options) {
|
|
|
3076
3035
|
connected: false,
|
|
3077
3036
|
timestamp: new Date().toISOString(),
|
|
3078
3037
|
};
|
|
3079
|
-
const existing = kind
|
|
3038
|
+
const existing = getClientFromAgent(kind);
|
|
3080
3039
|
if (!existing) {
|
|
3081
3040
|
result.error = `No ${kind} connection exists. Use ask_${kind}_web first to establish a connection.`;
|
|
3082
3041
|
return result;
|
|
@@ -3265,7 +3224,7 @@ export async function getPageDom(kind, selectors = []) {
|
|
|
3265
3224
|
connected: false,
|
|
3266
3225
|
selectors: {},
|
|
3267
3226
|
};
|
|
3268
|
-
const existing = kind
|
|
3227
|
+
const existing = getClientFromAgent(kind);
|
|
3269
3228
|
if (!existing) {
|
|
3270
3229
|
result.error = `No ${kind} connection exists. Use ask_${kind}_web first to establish a connection.`;
|
|
3271
3230
|
return result;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Session Manager for Agent Teams support.
|
|
8
|
+
*
|
|
9
|
+
* Manages per-agent sessions with:
|
|
10
|
+
* - V2 session format with agent isolation
|
|
11
|
+
* - Automatic migration from V1 format
|
|
12
|
+
* - TTL-based cleanup of stale sessions
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { getSessionConfig } from '../config.js';
|
|
17
|
+
import { getAgentId, hasAgentId } from './agent-context.js';
|
|
18
|
+
/**
|
|
19
|
+
* Get the session file path.
|
|
20
|
+
* Uses project-local .local/ directory.
|
|
21
|
+
*/
|
|
22
|
+
function getSessionPath() {
|
|
23
|
+
return path.join(process.cwd(), '.local', 'chrome-ai-bridge', 'sessions.json');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load raw sessions from file (any version).
|
|
27
|
+
*/
|
|
28
|
+
async function loadRawSessions() {
|
|
29
|
+
const sessionPath = getSessionPath();
|
|
30
|
+
try {
|
|
31
|
+
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
32
|
+
return JSON.parse(data);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const code = err.code;
|
|
36
|
+
if (code !== 'ENOENT') {
|
|
37
|
+
// File exists but is corrupted or unreadable
|
|
38
|
+
console.error(`[session-manager] Failed to load ${sessionPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
39
|
+
}
|
|
40
|
+
return { projects: {} };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Save sessions to file.
|
|
45
|
+
*/
|
|
46
|
+
async function saveRawSessions(sessions) {
|
|
47
|
+
const targetPath = getSessionPath();
|
|
48
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
49
|
+
await fs.writeFile(targetPath, JSON.stringify(sessions, null, 2), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if sessions are V2 format.
|
|
53
|
+
*/
|
|
54
|
+
function isV2Format(sessions) {
|
|
55
|
+
return 'version' in sessions && sessions.version === 2;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Migrate V1 sessions to V2 format.
|
|
59
|
+
* Creates a "legacy-default" agent from V1 project data.
|
|
60
|
+
*/
|
|
61
|
+
async function migrateToV2(v1) {
|
|
62
|
+
const config = getSessionConfig();
|
|
63
|
+
const v2 = {
|
|
64
|
+
version: 2,
|
|
65
|
+
agents: {},
|
|
66
|
+
config: {
|
|
67
|
+
sessionTtlMinutes: config.sessionTtlMinutes,
|
|
68
|
+
maxAgents: config.maxAgents,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
// Migrate each project as a legacy agent
|
|
72
|
+
for (const [projectName, projectSessions] of Object.entries(v1.projects)) {
|
|
73
|
+
// Sanitize project name for use as agent ID key
|
|
74
|
+
const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
75
|
+
const agentId = `legacy-${safeName}`;
|
|
76
|
+
v2.agents[agentId] = {
|
|
77
|
+
lastAccess: new Date().toISOString(),
|
|
78
|
+
chatgpt: projectSessions.chatgpt || null,
|
|
79
|
+
gemini: projectSessions.gemini || null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
console.error(`[session-manager] Migrated ${Object.keys(v1.projects).length} projects to V2 format`);
|
|
83
|
+
return v2;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Load sessions, auto-migrating if needed.
|
|
87
|
+
*/
|
|
88
|
+
export async function loadSessions() {
|
|
89
|
+
const raw = await loadRawSessions();
|
|
90
|
+
if (isV2Format(raw)) {
|
|
91
|
+
return raw;
|
|
92
|
+
}
|
|
93
|
+
// Migrate V1 to V2
|
|
94
|
+
const v2 = await migrateToV2(raw);
|
|
95
|
+
await saveRawSessions(v2);
|
|
96
|
+
return v2;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get or create session for the current agent.
|
|
100
|
+
* Always updates lastAccess to keep session alive for TTL.
|
|
101
|
+
*/
|
|
102
|
+
export async function getAgentSession() {
|
|
103
|
+
const agentId = hasAgentId() ? getAgentId() : 'default';
|
|
104
|
+
const sessions = await loadSessions();
|
|
105
|
+
let needsSave = false;
|
|
106
|
+
if (!sessions.agents[agentId]) {
|
|
107
|
+
sessions.agents[agentId] = {
|
|
108
|
+
lastAccess: new Date().toISOString(),
|
|
109
|
+
chatgpt: null,
|
|
110
|
+
gemini: null,
|
|
111
|
+
};
|
|
112
|
+
needsSave = true;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Update lastAccess for existing sessions (keeps TTL alive)
|
|
116
|
+
sessions.agents[agentId].lastAccess = new Date().toISOString();
|
|
117
|
+
needsSave = true;
|
|
118
|
+
}
|
|
119
|
+
if (needsSave) {
|
|
120
|
+
await saveRawSessions(sessions);
|
|
121
|
+
}
|
|
122
|
+
return sessions.agents[agentId];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Save session for the current agent.
|
|
126
|
+
*/
|
|
127
|
+
export async function saveAgentSession(kind, url, tabId) {
|
|
128
|
+
const agentId = hasAgentId() ? getAgentId() : 'default';
|
|
129
|
+
const sessions = await loadSessions();
|
|
130
|
+
if (!sessions.agents[agentId]) {
|
|
131
|
+
sessions.agents[agentId] = {
|
|
132
|
+
lastAccess: new Date().toISOString(),
|
|
133
|
+
chatgpt: null,
|
|
134
|
+
gemini: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const session = sessions.agents[agentId];
|
|
138
|
+
session.lastAccess = new Date().toISOString();
|
|
139
|
+
session[kind] = {
|
|
140
|
+
url,
|
|
141
|
+
tabId,
|
|
142
|
+
lastUsed: new Date().toISOString(),
|
|
143
|
+
};
|
|
144
|
+
await saveRawSessions(sessions);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Clear session for the current agent.
|
|
148
|
+
*/
|
|
149
|
+
export async function clearAgentSession(kind) {
|
|
150
|
+
const agentId = hasAgentId() ? getAgentId() : 'default';
|
|
151
|
+
const sessions = await loadSessions();
|
|
152
|
+
if (sessions.agents[agentId]) {
|
|
153
|
+
sessions.agents[agentId][kind] = null;
|
|
154
|
+
await saveRawSessions(sessions);
|
|
155
|
+
console.error(`[session-manager] Cleared ${kind} session for agent: ${agentId}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Remove stale sessions that exceed TTL.
|
|
160
|
+
*
|
|
161
|
+
* @returns Number of agents removed
|
|
162
|
+
*/
|
|
163
|
+
export async function cleanupStaleSessions() {
|
|
164
|
+
const config = getSessionConfig();
|
|
165
|
+
const sessions = await loadSessions();
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const ttlMs = config.sessionTtlMinutes * 60 * 1000;
|
|
168
|
+
let removedCount = 0;
|
|
169
|
+
for (const [agentId, session] of Object.entries(sessions.agents)) {
|
|
170
|
+
const lastAccess = new Date(session.lastAccess).getTime();
|
|
171
|
+
const age = now - lastAccess;
|
|
172
|
+
if (age > ttlMs) {
|
|
173
|
+
delete sessions.agents[agentId];
|
|
174
|
+
removedCount++;
|
|
175
|
+
console.error(`[session-manager] Removed stale agent: ${agentId} (${Math.round(age / 60000)}min old)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Enforce maxAgents limit
|
|
179
|
+
const agentIds = Object.keys(sessions.agents);
|
|
180
|
+
if (agentIds.length > config.maxAgents) {
|
|
181
|
+
// Sort by lastAccess (oldest first)
|
|
182
|
+
const sorted = agentIds.sort((a, b) => {
|
|
183
|
+
const aTime = new Date(sessions.agents[a].lastAccess).getTime();
|
|
184
|
+
const bTime = new Date(sessions.agents[b].lastAccess).getTime();
|
|
185
|
+
return aTime - bTime;
|
|
186
|
+
});
|
|
187
|
+
// Remove oldest until under limit
|
|
188
|
+
const toRemove = sorted.slice(0, agentIds.length - config.maxAgents);
|
|
189
|
+
for (const agentId of toRemove) {
|
|
190
|
+
delete sessions.agents[agentId];
|
|
191
|
+
removedCount++;
|
|
192
|
+
console.error(`[session-manager] Removed agent (over limit): ${agentId}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (removedCount > 0) {
|
|
196
|
+
await saveRawSessions(sessions);
|
|
197
|
+
}
|
|
198
|
+
return removedCount;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get preferred session (URL and tabId) for the current agent.
|
|
202
|
+
* Used by fast-chat.ts for connection reuse.
|
|
203
|
+
*/
|
|
204
|
+
export async function getPreferredSessionV2(kind) {
|
|
205
|
+
const session = await getAgentSession();
|
|
206
|
+
const entry = session[kind];
|
|
207
|
+
if (entry && typeof entry.url === 'string' && entry.url.length > 0) {
|
|
208
|
+
return {
|
|
209
|
+
url: entry.url,
|
|
210
|
+
tabId: typeof entry.tabId === 'number' ? entry.tabId : undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return { url: null };
|
|
214
|
+
}
|
package/build/src/main.js
CHANGED
|
@@ -26,6 +26,9 @@ import { ToolRegistry, PluginLoader } from './plugin-api.js';
|
|
|
26
26
|
import { registerOptionalTools, WEB_LLM_TOOLS_INFO, } from './tools/optional-tools.js';
|
|
27
27
|
import { getFastContext } from './fast-cdp/fast-context.js';
|
|
28
28
|
import { cleanupAllConnections } from './fast-cdp/fast-chat.js';
|
|
29
|
+
import { generateAgentId, setAgentId } from './fast-cdp/agent-context.js';
|
|
30
|
+
import { cleanupStaleSessions } from './fast-cdp/session-manager.js';
|
|
31
|
+
import { getSessionConfig } from './config.js';
|
|
29
32
|
function readPackageJson() {
|
|
30
33
|
const currentDir = import.meta.dirname;
|
|
31
34
|
const packageJsonPath = path.join(currentDir, '..', '..', 'package.json');
|
|
@@ -45,6 +48,23 @@ const version = readPackageJson().version ?? 'unknown';
|
|
|
45
48
|
export const args = parseArguments(version);
|
|
46
49
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
47
50
|
logger(`Starting Chrome AI Bridge v${version} (Extension-only mode)`);
|
|
51
|
+
// Initialize agent ID for Agent Teams support
|
|
52
|
+
const agentId = generateAgentId();
|
|
53
|
+
setAgentId(agentId);
|
|
54
|
+
// Start session cleanup timer
|
|
55
|
+
const sessionConfig = getSessionConfig();
|
|
56
|
+
const cleanupTimer = setInterval(async () => {
|
|
57
|
+
try {
|
|
58
|
+
const removed = await cleanupStaleSessions();
|
|
59
|
+
if (removed > 0) {
|
|
60
|
+
logger(`[session] Cleaned up ${removed} stale sessions`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger(`[session] Cleanup error: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
}, sessionConfig.cleanupIntervalMinutes * 60 * 1000);
|
|
67
|
+
cleanupTimer.unref(); // Don't keep process alive for cleanup
|
|
48
68
|
const server = new McpServer({
|
|
49
69
|
name: 'chrome-ai-bridge',
|
|
50
70
|
title: 'Chrome AI Bridge - ChatGPT/Gemini via Extension',
|
package/package.json
CHANGED