@elvatis_com/openclaw-cli-bridge-elvatis 1.3.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -85,8 +85,18 @@ interface CliPluginConfig {
85
85
  let grokBrowser: Browser | null = null;
86
86
  let grokContext: BrowserContext | null = null;
87
87
 
88
- // Persistent profile dirsurvives gateway restarts, keeps cookies intact
88
+ // Persistent profile dirssurvive gateway restarts, keep cookies intact
89
89
  const GROK_PROFILE_DIR = join(homedir(), ".openclaw", "grok-profile");
90
+ const GEMINI_PROFILE_DIR = join(homedir(), ".openclaw", "gemini-profile");
91
+
92
+ // Stealth launch options — prevent Cloudflare/bot detection from flagging the browser
93
+ const STEALTH_ARGS = [
94
+ "--no-sandbox",
95
+ "--disable-setuid-sandbox",
96
+ "--disable-blink-features=AutomationControlled",
97
+ "--disable-infobars",
98
+ ];
99
+ const STEALTH_IGNORE_DEFAULTS = ["--enable-automation"] as const;
90
100
 
91
101
  // ── Gemini web-session state ──────────────────────────────────────────────────
92
102
  let geminiContext: BrowserContext | null = null;
@@ -120,68 +130,6 @@ async function scanGeminiCookieExpiry(ctx: BrowserContext): Promise<GeminiExpiry
120
130
  }
121
131
  // ─────────────────────────────────────────────────────────────────────────────
122
132
 
123
- // ── ChatGPT web-session state ─────────────────────────────────────────────────
124
- let chatgptContext: BrowserContext | null = null;
125
- const CHATGPT_EXPIRY_FILE = join(homedir(), ".openclaw", "chatgpt-cookie-expiry.json");
126
- interface ChatGPTExpiryInfo { expiresAt: number; loginAt: number; cookieName: string; }
127
- function saveChatGPTExpiry(i: ChatGPTExpiryInfo) { try { writeFileSync(CHATGPT_EXPIRY_FILE, JSON.stringify(i, null, 2)); } catch { /* ignore */ } }
128
- function loadChatGPTExpiry(): ChatGPTExpiryInfo | null { try { return JSON.parse(readFileSync(CHATGPT_EXPIRY_FILE, "utf-8")); } catch { return null; } }
129
- function formatChatGPTExpiry(i: ChatGPTExpiryInfo): string {
130
- const d = Math.floor((i.expiresAt - Date.now()) / 86_400_000);
131
- const dt = new Date(i.expiresAt).toISOString().substring(0, 10);
132
- if (d < 0) return `⚠️ EXPIRED (${dt}) — run /chatgpt-login`;
133
- if (d <= 7) return `🚨 expires in ${d}d (${dt}) — run /chatgpt-login NOW`;
134
- if (d <= 14) return `⚠️ expires in ${d}d (${dt}) — run /chatgpt-login soon`;
135
- return `✅ valid for ${d} more days (expires ${dt})`;
136
- }
137
- async function scanChatGPTCookieExpiry(ctx: BrowserContext): Promise<ChatGPTExpiryInfo | null> {
138
- try {
139
- const cookies = await ctx.cookies(["https://chatgpt.com", "https://openai.com"]);
140
- const auth = cookies.filter(c => ["__Secure-next-auth.session-token", "cf_clearance", "__cf_bm"].includes(c.name) && c.expires && c.expires > 0);
141
- if (!auth.length) return null;
142
- auth.sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
143
- const earliest = auth[0];
144
- return { expiresAt: (earliest.expires ?? 0) * 1000, loginAt: Date.now(), cookieName: earliest.name };
145
- } catch { return null; }
146
- }
147
- // ─────────────────────────────────────────────────────────────────────────────
148
-
149
- // ── Claude web-session state ──────────────────────────────────────────────────
150
- let claudeContext: BrowserContext | null = null;
151
- const CLAUDE_EXPIRY_FILE = join(homedir(), ".openclaw", "claude-cookie-expiry.json");
152
-
153
- interface ClaudeExpiryInfo {
154
- expiresAt: number;
155
- loginAt: number;
156
- cookieName: string;
157
- }
158
-
159
- function saveClaudeExpiry(info: ClaudeExpiryInfo): void {
160
- try { writeFileSync(CLAUDE_EXPIRY_FILE, JSON.stringify(info, null, 2)); } catch { /* ignore */ }
161
- }
162
- function loadClaudeExpiry(): ClaudeExpiryInfo | null {
163
- try { return JSON.parse(readFileSync(CLAUDE_EXPIRY_FILE, "utf-8")) as ClaudeExpiryInfo; } catch { return null; }
164
- }
165
- function formatClaudeExpiry(info: ClaudeExpiryInfo): string {
166
- const daysLeft = Math.floor((info.expiresAt - Date.now()) / 86_400_000);
167
- const dateStr = new Date(info.expiresAt).toISOString().substring(0, 10);
168
- if (daysLeft < 0) return `⚠️ EXPIRED (${dateStr}) — run /claude-login`;
169
- if (daysLeft <= 7) return `🚨 expires in ${daysLeft}d (${dateStr}) — run /claude-login NOW`;
170
- if (daysLeft <= 14) return `⚠️ expires in ${daysLeft}d (${dateStr}) — run /claude-login soon`;
171
- return `✅ valid for ${daysLeft} more days (expires ${dateStr})`;
172
- }
173
- async function scanClaudeCookieExpiry(ctx: BrowserContext): Promise<ClaudeExpiryInfo | null> {
174
- try {
175
- const cookies = await ctx.cookies(["https://claude.ai", "https://anthropic.com"]);
176
- const authCookies = cookies.filter(c => ["sessionKey", "intercom-session-igviqkfk"].includes(c.name) && c.expires && c.expires > 0);
177
- if (!authCookies.length) return null;
178
- authCookies.sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
179
- const earliest = authCookies[0];
180
- return { expiresAt: (earliest.expires ?? 0) * 1000, loginAt: Date.now(), cookieName: earliest.name };
181
- } catch { return null; }
182
- }
183
- // ─────────────────────────────────────────────────────────────────────────────
184
-
185
133
  // Cookie expiry tracking file — written on /grok-login, read on startup
186
134
  const GROK_EXPIRY_FILE = join(homedir(), ".openclaw", "grok-cookie-expiry.json");
187
135
 
@@ -293,7 +241,9 @@ async function getOrLaunchGrokContext(
293
241
  try {
294
242
  const ctx = await chromium.launchPersistentContext(GROK_PROFILE_DIR, {
295
243
  headless: true,
296
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
244
+ channel: "chrome",
245
+ args: STEALTH_ARGS,
246
+ ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULTS],
297
247
  });
298
248
  grokContext = ctx;
299
249
  // Auto-cleanup on browser crash
@@ -310,19 +260,57 @@ async function getOrLaunchGrokContext(
310
260
  return _cdpBrowserLaunchPromise;
311
261
  }
312
262
 
263
+ // ── Per-provider persistent context launch promises (coalesce concurrent calls) ──
264
+ let _geminiLaunchPromise: Promise<BrowserContext | null> | null = null;
265
+
266
+ async function getOrLaunchGeminiContext(
267
+ log: (msg: string) => void
268
+ ): Promise<BrowserContext | null> {
269
+ if (geminiContext) {
270
+ try { geminiContext.pages(); return geminiContext; } catch { geminiContext = null; }
271
+ }
272
+ const cdpCtx = await connectToOpenClawBrowser(log);
273
+ if (cdpCtx) return cdpCtx;
274
+ if (_geminiLaunchPromise) return _geminiLaunchPromise;
275
+ _geminiLaunchPromise = (async () => {
276
+ const { chromium } = await import("playwright");
277
+ log("[cli-bridge:gemini] launching persistent Chromium…");
278
+ try {
279
+ mkdirSync(GEMINI_PROFILE_DIR, { recursive: true });
280
+ const ctx = await chromium.launchPersistentContext(GEMINI_PROFILE_DIR, {
281
+ headless: true,
282
+ channel: "chrome",
283
+ args: STEALTH_ARGS,
284
+ ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULTS],
285
+ });
286
+ geminiContext = ctx;
287
+ ctx.on("close", () => { geminiContext = null; log("[cli-bridge:gemini] persistent context closed"); });
288
+ log("[cli-bridge:gemini] persistent context ready");
289
+ return ctx;
290
+ } catch (err) {
291
+ log(`[cli-bridge:gemini] failed to launch browser: ${(err as Error).message}`);
292
+ return null;
293
+ } finally {
294
+ _geminiLaunchPromise = null;
295
+ }
296
+ })();
297
+ return _geminiLaunchPromise;
298
+ }
299
+
313
300
  /** Clean up all browser resources — call on plugin teardown */
314
301
  async function cleanupBrowsers(log: (msg: string) => void): Promise<void> {
315
302
  if (grokContext) {
316
303
  try { await grokContext.close(); } catch { /* ignore */ }
317
304
  grokContext = null;
318
305
  }
306
+ if (geminiContext) {
307
+ try { await geminiContext.close(); } catch { /* ignore */ }
308
+ geminiContext = null;
309
+ }
319
310
  if (_cdpBrowser) {
320
311
  try { await _cdpBrowser.close(); } catch { /* ignore */ }
321
312
  _cdpBrowser = null;
322
313
  }
323
- claudeContext = null;
324
- geminiContext = null;
325
- chatgptContext = null;
326
314
  log("[cli-bridge] browser resources cleaned up");
327
315
  }
328
316
 
@@ -364,14 +352,6 @@ async function _doEnsureAllProviderContexts(log: (msg: string) => void): Promise
364
352
 
365
353
  // For each provider: if no context yet, try shared ctx or launch own persistent context
366
354
  const providerConfigs = [
367
- {
368
- name: "claude",
369
- profileDir: join(homedir(), ".openclaw", "claude-profile"),
370
- getCtx: () => claudeContext,
371
- setCtx: (c: BrowserContext) => { claudeContext = c; },
372
- homeUrl: "https://claude.ai/new",
373
- verifySelector: ".ProseMirror",
374
- },
375
355
  {
376
356
  name: "gemini",
377
357
  profileDir: join(homedir(), ".openclaw", "gemini-profile"),
@@ -380,14 +360,6 @@ async function _doEnsureAllProviderContexts(log: (msg: string) => void): Promise
380
360
  homeUrl: "https://gemini.google.com/app",
381
361
  verifySelector: ".ql-editor",
382
362
  },
383
- {
384
- name: "chatgpt",
385
- profileDir: join(homedir(), ".openclaw", "chatgpt-profile"),
386
- getCtx: () => chatgptContext,
387
- setCtx: (c: BrowserContext) => { chatgptContext = c; },
388
- homeUrl: "https://chatgpt.com",
389
- verifySelector: "#prompt-textarea",
390
- },
391
363
  ];
392
364
 
393
365
  for (const cfg of providerConfigs) {
@@ -419,7 +391,9 @@ async function _doEnsureAllProviderContexts(log: (msg: string) => void): Promise
419
391
  mkdirSync(cfg.profileDir, { recursive: true });
420
392
  const pCtx = await chromium.launchPersistentContext(cfg.profileDir, {
421
393
  headless: true,
422
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
394
+ channel: "chrome",
395
+ args: STEALTH_ARGS,
396
+ ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULTS],
423
397
  });
424
398
  const page = await pCtx.newPage();
425
399
  await page.goto(cfg.homeUrl, { waitUntil: "domcontentloaded", timeout: 15_000 });
@@ -782,15 +756,6 @@ const plugin = {
782
756
  getCtx: () => grokContext,
783
757
  setCtx: (c) => { grokContext = c; },
784
758
  },
785
- {
786
- name: "claude",
787
- profileDir: join(homedir(), ".openclaw", "claude-profile"),
788
- cookieFile: join(homedir(), ".openclaw", "claude-cookie-expiry.json"),
789
- verifySelector: ".ProseMirror",
790
- homeUrl: "https://claude.ai/new",
791
- getCtx: () => claudeContext,
792
- setCtx: (c) => { claudeContext = c; },
793
- },
794
759
  {
795
760
  name: "gemini",
796
761
  profileDir: join(homedir(), ".openclaw", "gemini-profile"),
@@ -800,15 +765,6 @@ const plugin = {
800
765
  getCtx: () => geminiContext,
801
766
  setCtx: (c) => { geminiContext = c; },
802
767
  },
803
- {
804
- name: "chatgpt",
805
- profileDir: join(homedir(), ".openclaw", "chatgpt-profile"),
806
- cookieFile: join(homedir(), ".openclaw", "chatgpt-cookie-expiry.json"),
807
- verifySelector: "#prompt-textarea",
808
- homeUrl: "https://chatgpt.com",
809
- getCtx: () => chatgptContext,
810
- setCtx: (c) => { chatgptContext = c; },
811
- },
812
768
  ];
813
769
 
814
770
  for (const p of profileProviders) {
@@ -944,49 +900,24 @@ const plugin = {
944
900
  warn: (msg) => api.logger.warn(msg),
945
901
  getGrokContext: () => grokContext,
946
902
  connectGrokContext: async () => {
947
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
903
+ const ctx = await getOrLaunchGrokContext((msg) => api.logger.info(msg));
948
904
  if (ctx) {
949
905
  const check = await verifySession(ctx, (msg) => api.logger.info(msg));
950
906
  if (check.valid) { grokContext = ctx; return ctx; }
951
907
  }
952
908
  return null;
953
909
  },
954
- getClaudeContext: () => claudeContext,
955
- connectClaudeContext: async () => {
956
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
957
- if (ctx) {
958
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
959
- const { page } = await getOrCreateClaudePage(ctx);
960
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
961
- if (editor) { claudeContext = ctx; return ctx; }
962
- }
963
- // No fallback spawn — return existing context or null to avoid Chromium leak
964
- return claudeContext;
965
- },
966
910
  getGeminiContext: () => geminiContext,
967
911
  connectGeminiContext: async () => {
968
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
912
+ const ctx = await getOrLaunchGeminiContext((msg) => api.logger.info(msg));
969
913
  if (ctx) {
970
914
  const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
971
915
  const { page } = await getOrCreateGeminiPage(ctx);
972
916
  const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
973
917
  if (editor) { geminiContext = ctx; return ctx; }
974
918
  }
975
- // No fallback spawn — return existing context or null to avoid Chromium leak
976
919
  return geminiContext;
977
920
  },
978
- getChatGPTContext: () => chatgptContext,
979
- connectChatGPTContext: async () => {
980
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
981
- if (ctx) {
982
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
983
- const { page } = await getOrCreateChatGPTPage(ctx);
984
- const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
985
- if (editor) { chatgptContext = ctx; return ctx; }
986
- }
987
- // No fallback spawn — return existing context or null to avoid Chromium leak
988
- return chatgptContext;
989
- },
990
921
  });
991
922
  proxyServer = server;
992
923
  api.logger.info(
@@ -1019,18 +950,6 @@ const plugin = {
1019
950
  }
1020
951
  return null;
1021
952
  },
1022
- getClaudeContext: () => claudeContext,
1023
- connectClaudeContext: async () => {
1024
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1025
- if (ctx) {
1026
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1027
- const { page } = await getOrCreateClaudePage(ctx);
1028
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1029
- if (editor) { claudeContext = ctx; return ctx; }
1030
- }
1031
- // No fallback spawn — return existing context or null to avoid Chromium leak
1032
- return claudeContext;
1033
- },
1034
953
  getGeminiContext: () => geminiContext,
1035
954
  connectGeminiContext: async () => {
1036
955
  const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
@@ -1043,18 +962,6 @@ const plugin = {
1043
962
  // No fallback spawn — return existing context or null to avoid Chromium leak
1044
963
  return geminiContext;
1045
964
  },
1046
- getChatGPTContext: () => chatgptContext,
1047
- connectChatGPTContext: async () => {
1048
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1049
- if (ctx) {
1050
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1051
- const { page } = await getOrCreateChatGPTPage(ctx);
1052
- const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
1053
- if (editor) { chatgptContext = ctx; return ctx; }
1054
- }
1055
- // No fallback spawn — return existing context or null to avoid Chromium leak
1056
- return chatgptContext;
1057
- },
1058
965
  });
1059
966
  proxyServer = server;
1060
967
  api.logger.info(`[cli-bridge] proxy ready on :${port} (retry)`);
@@ -1395,107 +1302,6 @@ const plugin = {
1395
1302
  },
1396
1303
  } satisfies OpenClawPluginCommandDefinition);
1397
1304
 
1398
- // ── Claude web-session commands ───────────────────────────────────────────
1399
- api.registerCommand({
1400
- name: "claude-login",
1401
- description: "Authenticate claude.ai: imports session from OpenClaw browser",
1402
- handler: async (): Promise<PluginCommandResult> => {
1403
- if (claudeContext) {
1404
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1405
- try {
1406
- const { page } = await getOrCreateClaudePage(claudeContext);
1407
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1408
- if (editor) return { text: "✅ Already connected to claude.ai. Use `/claude-logout` first to reset." };
1409
- } catch { /* fall through */ }
1410
- claudeContext = null;
1411
- }
1412
-
1413
- api.logger.info("[cli-bridge:claude] /claude-login: connecting to OpenClaw browser…");
1414
-
1415
- // Connect to OpenClaw browser context for session (singleton CDP)
1416
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1417
- if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure claude.ai is open in your browser." };
1418
-
1419
- // Navigate to claude.ai/new if not already there
1420
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1421
- let page;
1422
- try {
1423
- ({ page } = await getOrCreateClaudePage(ctx));
1424
- } catch (err) {
1425
- return { text: `❌ Failed to open claude.ai: ${(err as Error).message}` };
1426
- }
1427
-
1428
- // Verify editor is visible
1429
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1430
- if (!editor) {
1431
- return { text: "❌ claude.ai editor not visible — are you logged in?\nOpen claude.ai in your browser and try again." };
1432
- }
1433
-
1434
- claudeContext = ctx;
1435
-
1436
- // Export + bake cookies into persistent profile
1437
- const claudeProfileDir = join(homedir(), ".openclaw", "claude-profile");
1438
- mkdirSync(claudeProfileDir, { recursive: true });
1439
- try {
1440
- const allCookies = await ctx.cookies([
1441
- "https://claude.ai",
1442
- "https://anthropic.com",
1443
- ]);
1444
- const { chromium } = await import("playwright");
1445
- const pCtx = await chromium.launchPersistentContext(claudeProfileDir, {
1446
- headless: true,
1447
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
1448
- });
1449
- await pCtx.addCookies(allCookies);
1450
- await pCtx.close();
1451
- api.logger.info(`[cli-bridge:claude] cookies baked into ${claudeProfileDir}`);
1452
- } catch (err) {
1453
- api.logger.warn(`[cli-bridge:claude] cookie bake failed: ${(err as Error).message}`);
1454
- }
1455
-
1456
- // Scan cookie expiry
1457
- const expiry = await scanClaudeCookieExpiry(ctx);
1458
- if (expiry) {
1459
- saveClaudeExpiry(expiry);
1460
- api.logger.info(`[cli-bridge:claude] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`);
1461
- }
1462
- const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatClaudeExpiry(expiry)}` : "";
1463
-
1464
- return { text: `✅ claude.ai session ready!\n\nModels available:\n• \`vllm/web-claude/claude-sonnet\`\n• \`vllm/web-claude/claude-opus\`\n• \`vllm/web-claude/claude-haiku\`${expiryLine}` };
1465
- },
1466
- } satisfies OpenClawPluginCommandDefinition);
1467
-
1468
- api.registerCommand({
1469
- name: "claude-status",
1470
- description: "Check claude.ai session status",
1471
- handler: async (): Promise<PluginCommandResult> => {
1472
- if (!claudeContext) {
1473
- return { text: "❌ No active claude.ai session\nRun `/claude-login` to authenticate." };
1474
- }
1475
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1476
- try {
1477
- const { page } = await getOrCreateClaudePage(claudeContext);
1478
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1479
- if (editor) {
1480
- const expiry = loadClaudeExpiry();
1481
- const expiryLine = expiry ? `\n🕐 ${formatClaudeExpiry(expiry)}` : "";
1482
- return { text: `✅ claude.ai session active\nProxy: \`127.0.0.1:${port}\`\nModels: web-claude/claude-sonnet, web-claude/claude-opus, web-claude/claude-haiku${expiryLine}` };
1483
- }
1484
- } catch { /* fall through */ }
1485
- claudeContext = null;
1486
- return { text: "❌ Session lost — run `/claude-login` to re-authenticate." };
1487
- },
1488
- } satisfies OpenClawPluginCommandDefinition);
1489
-
1490
- api.registerCommand({
1491
- name: "claude-logout",
1492
- description: "Disconnect from claude.ai session",
1493
- handler: async (): Promise<PluginCommandResult> => {
1494
- claudeContext = null;
1495
- return { text: "✅ Disconnected from claude.ai. Run `/claude-login` to reconnect." };
1496
- },
1497
- } satisfies OpenClawPluginCommandDefinition);
1498
-
1499
1305
  // ── Gemini web-session commands ───────────────────────────────────────────
1500
1306
  api.registerCommand({
1501
1307
  name: "gemini-login",
@@ -1511,10 +1317,34 @@ const plugin = {
1511
1317
  geminiContext = null;
1512
1318
  }
1513
1319
 
1514
- api.logger.info("[cli-bridge:gemini] /gemini-login: connecting to OpenClaw browser…");
1515
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1516
- if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure gemini.google.com is open in your browser." };
1320
+ api.logger.info("[cli-bridge:gemini] /gemini-login: connecting…");
1321
+
1322
+ // Step 1: try to grab cookies from OpenClaw browser (CDP) if available
1323
+ let importedCookies: unknown[] = [];
1324
+ try {
1325
+ const { chromium } = await import("playwright");
1326
+ const ocBrowser = await chromium.connectOverCDP("http://127.0.0.1:18800", { timeout: 3000 });
1327
+ const ocCtx = ocBrowser.contexts()[0];
1328
+ if (ocCtx) {
1329
+ importedCookies = await ocCtx.cookies(["https://gemini.google.com", "https://accounts.google.com", "https://google.com"]);
1330
+ api.logger.info(`[cli-bridge:gemini] imported ${importedCookies.length} cookies from OpenClaw browser`);
1331
+ }
1332
+ await ocBrowser.close().catch(() => {});
1333
+ } catch {
1334
+ api.logger.info("[cli-bridge:gemini] OpenClaw browser not available — using saved profile");
1335
+ }
1336
+
1337
+ // Step 2: get or launch persistent context
1338
+ const ctx = await getOrLaunchGeminiContext((msg) => api.logger.info(msg));
1339
+ if (!ctx) return { text: "❌ Could not launch browser. Check server logs." };
1517
1340
 
1341
+ // Step 3: inject imported cookies if available
1342
+ if (importedCookies.length > 0) {
1343
+ await ctx.addCookies(importedCookies as Parameters<typeof ctx.addCookies>[0]);
1344
+ api.logger.info("[cli-bridge:gemini] cookies injected into persistent profile");
1345
+ }
1346
+
1347
+ // Step 4: navigate and verify
1518
1348
  const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
1519
1349
  let page;
1520
1350
  try {
@@ -1523,35 +1353,40 @@ const plugin = {
1523
1353
  return { text: `❌ Failed to open gemini.google.com: ${(err as Error).message}` };
1524
1354
  }
1525
1355
 
1526
- const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
1356
+ let editor = await page.locator(".ql-editor").isVisible().catch(() => false);
1527
1357
  if (!editor) {
1528
- return { text: "❌ Gemini editor not visible are you logged in?\nOpen gemini.google.com in your browser and try again." };
1529
- }
1530
-
1531
- geminiContext = ctx;
1358
+ // Headless failedlaunch headed browser for interactive login
1359
+ api.logger.info("[cli-bridge:gemini] headless login failed — launching headed browser for manual login…");
1360
+ try { await ctx.close(); } catch { /* ignore */ }
1361
+ geminiContext = null;
1532
1362
 
1533
- // Export + bake cookies into persistent profile
1534
- const geminiProfileDir = join(homedir(), ".openclaw", "gemini-profile");
1535
- mkdirSync(geminiProfileDir, { recursive: true });
1536
- try {
1537
- const allCookies = await ctx.cookies([
1538
- "https://gemini.google.com",
1539
- "https://accounts.google.com",
1540
- "https://google.com",
1541
- ]);
1542
1363
  const { chromium } = await import("playwright");
1543
- const pCtx = await chromium.launchPersistentContext(geminiProfileDir, {
1544
- headless: true,
1545
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
1364
+ const headedCtx = await chromium.launchPersistentContext(GEMINI_PROFILE_DIR, {
1365
+ headless: false,
1366
+ channel: "chrome",
1367
+ args: STEALTH_ARGS,
1368
+ ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULTS],
1546
1369
  });
1547
- await pCtx.addCookies(allCookies);
1548
- await pCtx.close();
1549
- api.logger.info(`[cli-bridge:gemini] cookies baked into ${geminiProfileDir}`);
1550
- } catch (err) {
1551
- api.logger.warn(`[cli-bridge:gemini] cookie bake failed: ${(err as Error).message}`);
1370
+ const loginPage = await headedCtx.newPage();
1371
+ await loginPage.goto("https://gemini.google.com/app", { waitUntil: "domcontentloaded", timeout: 15_000 });
1372
+
1373
+ api.logger.info("[cli-bridge:gemini] waiting for manual login (5 min timeout)…");
1374
+ try {
1375
+ await loginPage.waitForSelector(".ql-editor", { timeout: 300_000 });
1376
+ } catch {
1377
+ await headedCtx.close().catch(() => {});
1378
+ return { text: "❌ Login timeout — Gemini editor did not appear within 5 minutes." };
1379
+ }
1380
+
1381
+ geminiContext = headedCtx;
1382
+ headedCtx.on("close", () => { geminiContext = null; });
1383
+ editor = true;
1384
+ page = loginPage;
1385
+ } else {
1386
+ geminiContext = ctx;
1552
1387
  }
1553
1388
 
1554
- const expiry = await scanGeminiCookieExpiry(ctx);
1389
+ const expiry = await scanGeminiCookieExpiry(geminiContext!);
1555
1390
  if (expiry) {
1556
1391
  saveGeminiExpiry(expiry);
1557
1392
  api.logger.info(`[cli-bridge:gemini] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`);
@@ -1593,94 +1428,10 @@ const plugin = {
1593
1428
  },
1594
1429
  } satisfies OpenClawPluginCommandDefinition);
1595
1430
 
1596
- // ── ChatGPT web-session commands ──────────────────────────────────────────
1597
- api.registerCommand({
1598
- name: "chatgpt-login",
1599
- description: "Authenticate chatgpt.com: imports session from OpenClaw browser",
1600
- handler: async (): Promise<PluginCommandResult> => {
1601
- if (chatgptContext) {
1602
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1603
- try {
1604
- const { page } = await getOrCreateChatGPTPage(chatgptContext);
1605
- if (await page.locator("#prompt-textarea").isVisible().catch(() => false))
1606
- return { text: "✅ Already connected to chatgpt.com. Use `/chatgpt-logout` first to reset." };
1607
- } catch { /* fall through */ }
1608
- chatgptContext = null;
1609
- }
1610
- api.logger.info("[cli-bridge:chatgpt] /chatgpt-login: connecting to OpenClaw browser…");
1611
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1612
- if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure chatgpt.com is open in your browser." };
1613
-
1614
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1615
- let page;
1616
- try { ({ page } = await getOrCreateChatGPTPage(ctx)); }
1617
- catch (err) { return { text: `❌ Failed to open chatgpt.com: ${(err as Error).message}` }; }
1618
-
1619
- if (!await page.locator("#prompt-textarea").isVisible().catch(() => false))
1620
- return { text: "❌ ChatGPT editor not visible — are you logged in?\nOpen chatgpt.com in your browser and try again." };
1621
-
1622
- chatgptContext = ctx;
1623
-
1624
- // Export + bake cookies into persistent profile
1625
- const chatgptProfileDir = join(homedir(), ".openclaw", "chatgpt-profile");
1626
- mkdirSync(chatgptProfileDir, { recursive: true });
1627
- try {
1628
- const allCookies = await ctx.cookies([
1629
- "https://chatgpt.com",
1630
- "https://openai.com",
1631
- "https://auth.openai.com",
1632
- ]);
1633
- const { chromium } = await import("playwright");
1634
- const pCtx = await chromium.launchPersistentContext(chatgptProfileDir, {
1635
- headless: true,
1636
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
1637
- });
1638
- await pCtx.addCookies(allCookies);
1639
- await pCtx.close();
1640
- api.logger.info(`[cli-bridge:chatgpt] cookies baked into ${chatgptProfileDir}`);
1641
- } catch (err) {
1642
- api.logger.warn(`[cli-bridge:chatgpt] cookie bake failed: ${(err as Error).message}`);
1643
- }
1644
-
1645
- const expiry = await scanChatGPTCookieExpiry(ctx);
1646
- if (expiry) { saveChatGPTExpiry(expiry); api.logger.info(`[cli-bridge:chatgpt] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`); }
1647
- const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatChatGPTExpiry(expiry)}` : "";
1648
- return { text: `✅ ChatGPT session ready!\n\nModels available:\n• \`vllm/web-chatgpt/gpt-4o\`\n• \`vllm/web-chatgpt/gpt-4o-mini\`\n• \`vllm/web-chatgpt/gpt-o3\`\n• \`vllm/web-chatgpt/gpt-o4-mini\`\n• \`vllm/web-chatgpt/gpt-5\`${expiryLine}` };
1649
- },
1650
- } satisfies OpenClawPluginCommandDefinition);
1651
-
1652
- api.registerCommand({
1653
- name: "chatgpt-status",
1654
- description: "Check chatgpt.com session status",
1655
- handler: async (): Promise<PluginCommandResult> => {
1656
- if (!chatgptContext) return { text: "❌ No active chatgpt.com session\nRun `/chatgpt-login` to authenticate." };
1657
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1658
- try {
1659
- const { page } = await getOrCreateChatGPTPage(chatgptContext);
1660
- if (await page.locator("#prompt-textarea").isVisible().catch(() => false)) {
1661
- const expiry = loadChatGPTExpiry();
1662
- const expiryLine = expiry ? `\n🕐 ${formatChatGPTExpiry(expiry)}` : "";
1663
- return { text: `✅ chatgpt.com session active\nProxy: \`127.0.0.1:${port}\`\nModels: web-chatgpt/gpt-4o, gpt-4o-mini, gpt-o3, gpt-o4-mini, gpt-5${expiryLine}` };
1664
- }
1665
- } catch { /* fall through */ }
1666
- chatgptContext = null;
1667
- return { text: "❌ Session lost — run `/chatgpt-login` to re-authenticate." };
1668
- },
1669
- } satisfies OpenClawPluginCommandDefinition);
1670
-
1671
- api.registerCommand({
1672
- name: "chatgpt-logout",
1673
- description: "Disconnect from chatgpt.com session",
1674
- handler: async (): Promise<PluginCommandResult> => {
1675
- chatgptContext = null;
1676
- return { text: "✅ Disconnected from chatgpt.com. Run `/chatgpt-login` to reconnect." };
1677
- },
1678
- } satisfies OpenClawPluginCommandDefinition);
1679
-
1680
1431
  // ── /bridge-status — all providers at a glance ───────────────────────────
1681
1432
  api.registerCommand({
1682
1433
  name: "bridge-status",
1683
- description: "Show status of all headless browser providers (Grok, Claude, Gemini, ChatGPT)",
1434
+ description: "Show status of all headless browser providers (Grok, Gemini)",
1684
1435
  handler: async (): Promise<PluginCommandResult> => {
1685
1436
  const lines: string[] = [`🌉 *CLI Bridge v${plugin.version} — Provider Status*\n`];
1686
1437
 
@@ -1698,21 +1449,6 @@ const plugin = {
1698
1449
  loginCmd: "/grok-login",
1699
1450
  expiry: () => { const e = loadGrokExpiry(); return e ? formatExpiryInfo(e) : null; },
1700
1451
  },
1701
- {
1702
- name: "Claude",
1703
- ctx: claudeContext,
1704
- check: async () => {
1705
- if (!claudeContext) return false;
1706
- try {
1707
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1708
- const { page } = await getOrCreateClaudePage(claudeContext);
1709
- return page.locator(".ProseMirror").isVisible().catch(() => false);
1710
- } catch { claudeContext = null; return false; }
1711
- },
1712
- models: "web-claude/claude-sonnet, claude-opus, claude-haiku",
1713
- loginCmd: "/claude-login",
1714
- expiry: () => { const e = loadClaudeExpiry(); return e ? formatClaudeExpiry(e) : null; },
1715
- },
1716
1452
  {
1717
1453
  name: "Gemini",
1718
1454
  ctx: geminiContext,
@@ -1728,21 +1464,6 @@ const plugin = {
1728
1464
  loginCmd: "/gemini-login",
1729
1465
  expiry: () => { const e = loadGeminiExpiry(); return e ? formatGeminiExpiry(e) : null; },
1730
1466
  },
1731
- {
1732
- name: "ChatGPT",
1733
- ctx: chatgptContext,
1734
- check: async () => {
1735
- if (!chatgptContext) return false;
1736
- try {
1737
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1738
- const { page } = await getOrCreateChatGPTPage(chatgptContext);
1739
- return page.locator("#prompt-textarea").isVisible().catch(() => false);
1740
- } catch { chatgptContext = null; return false; }
1741
- },
1742
- models: "web-chatgpt/gpt-4o, gpt-o3, gpt-o4-mini, gpt-5",
1743
- loginCmd: "/chatgpt-login",
1744
- expiry: () => { const e = loadChatGPTExpiry(); return e ? formatChatGPTExpiry(e) : null; },
1745
- },
1746
1467
  ];
1747
1468
 
1748
1469
  for (const c of checks) {
@@ -1759,7 +1480,7 @@ const plugin = {
1759
1480
  lines.push("");
1760
1481
  }
1761
1482
 
1762
- lines.push(`🔌 Proxy: \`127.0.0.1:${port}\` | 22 models total`);
1483
+ lines.push(`🔌 Proxy: \`127.0.0.1:${port}\``);
1763
1484
  return { text: lines.join("\n") };
1764
1485
  },
1765
1486
  } satisfies OpenClawPluginCommandDefinition);
@@ -1773,15 +1494,9 @@ const plugin = {
1773
1494
  "/grok-login",
1774
1495
  "/grok-status",
1775
1496
  "/grok-logout",
1776
- "/claude-login",
1777
- "/claude-status",
1778
- "/claude-logout",
1779
1497
  "/gemini-login",
1780
1498
  "/gemini-status",
1781
1499
  "/gemini-logout",
1782
- "/chatgpt-login",
1783
- "/chatgpt-status",
1784
- "/chatgpt-logout",
1785
1500
  "/bridge-status",
1786
1501
  ];
1787
1502
  api.logger.info(`[cli-bridge] registered ${allCommands.length} commands: ${allCommands.join(", ")}`);