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.
@@ -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
- let chatgptClient = null;
10
- let geminiClient = null;
11
- // RelayServer参照を保持(接続失敗時のクリーンアップ用)
12
- let chatgptRelay = null;
13
- let geminiRelay = null;
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
- if (geminiClient) {
179
- geminiClient = null;
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 (geminiRelay) {
176
+ if (relay) {
183
177
  try {
184
- geminiRelay.stop();
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
- // ChatGPT
198
- if (chatgptRelay) {
199
- try {
200
- await chatgptRelay.stop();
201
- }
202
- catch {
203
- // ignore stop errors
204
- }
205
- chatgptRelay = null;
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
- catch {
214
- // ignore stop errors
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
- geminiClient = null;
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 getPreferredSession(kind);
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
- if (kind === 'chatgpt') {
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
- if (kind === 'chatgpt') {
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
- logInfo('fast-chat', `getClient called`, { kind, hasExisting: kind === 'chatgpt' ? !!chatgptClient : !!geminiClient });
473
- const existing = kind === 'chatgpt' ? chatgptClient : geminiClient;
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 === 'chatgpt' ? chatgptRelay : geminiRelay;
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
- if (kind === 'chatgpt') {
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 saveSession('chatgpt', finalUrl);
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 saveSession('chatgpt', finalUrl);
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 saveSession('gemini', finalUrl);
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 getPreferredUrl('gemini');
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 getPreferredUrl('gemini');
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 clearGeminiSession();
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 clearGeminiSession();
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 saveSession('gemini', finalUrl);
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 === 'chatgpt' ? chatgptClient : geminiClient;
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 === 'chatgpt' ? chatgptClient : geminiClient;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "2.0.19",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server bridging Chrome extension and AI assistants (ChatGPT, Gemini). Extension-only mode - no Puppeteer.",
5
5
  "type": "module",
6
6
  "bin": "./scripts/cli.mjs",