@elvatis_com/openclaw-cli-bridge-elvatis 1.3.4 → 1.5.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/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
 
@@ -233,6 +181,10 @@ async function scanCookieExpiry(ctx: import("playwright").BrowserContext): Promi
233
181
  let _cdpBrowser: import("playwright").Browser | null = null;
234
182
  let _cdpBrowserLaunchPromise: Promise<import("playwright").BrowserContext | null> | null = null;
235
183
 
184
+ // Startup restore guard — module-level so it survives hot-reloads (SIGUSR1).
185
+ // Set to true after first run; hot-reloads see true and skip the restore loop.
186
+ let _startupRestoreDone = false;
187
+
236
188
  /**
237
189
  * Connect to the OpenClaw managed browser (CDP port 18800).
238
190
  * Singleton: reuses the same connection. Falls back to persistent Chromium for Grok only.
@@ -289,7 +241,9 @@ async function getOrLaunchGrokContext(
289
241
  try {
290
242
  const ctx = await chromium.launchPersistentContext(GROK_PROFILE_DIR, {
291
243
  headless: true,
292
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
244
+ channel: "chrome",
245
+ args: STEALTH_ARGS,
246
+ ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULTS],
293
247
  });
294
248
  grokContext = ctx;
295
249
  // Auto-cleanup on browser crash
@@ -306,19 +260,57 @@ async function getOrLaunchGrokContext(
306
260
  return _cdpBrowserLaunchPromise;
307
261
  }
308
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
+
309
300
  /** Clean up all browser resources — call on plugin teardown */
310
301
  async function cleanupBrowsers(log: (msg: string) => void): Promise<void> {
311
302
  if (grokContext) {
312
303
  try { await grokContext.close(); } catch { /* ignore */ }
313
304
  grokContext = null;
314
305
  }
306
+ if (geminiContext) {
307
+ try { await geminiContext.close(); } catch { /* ignore */ }
308
+ geminiContext = null;
309
+ }
315
310
  if (_cdpBrowser) {
316
311
  try { await _cdpBrowser.close(); } catch { /* ignore */ }
317
312
  _cdpBrowser = null;
318
313
  }
319
- claudeContext = null;
320
- geminiContext = null;
321
- chatgptContext = null;
322
314
  log("[cli-bridge] browser resources cleaned up");
323
315
  }
324
316
 
@@ -360,14 +352,6 @@ async function _doEnsureAllProviderContexts(log: (msg: string) => void): Promise
360
352
 
361
353
  // For each provider: if no context yet, try shared ctx or launch own persistent context
362
354
  const providerConfigs = [
363
- {
364
- name: "claude",
365
- profileDir: join(homedir(), ".openclaw", "claude-profile"),
366
- getCtx: () => claudeContext,
367
- setCtx: (c: BrowserContext) => { claudeContext = c; },
368
- homeUrl: "https://claude.ai/new",
369
- verifySelector: ".ProseMirror",
370
- },
371
355
  {
372
356
  name: "gemini",
373
357
  profileDir: join(homedir(), ".openclaw", "gemini-profile"),
@@ -376,14 +360,6 @@ async function _doEnsureAllProviderContexts(log: (msg: string) => void): Promise
376
360
  homeUrl: "https://gemini.google.com/app",
377
361
  verifySelector: ".ql-editor",
378
362
  },
379
- {
380
- name: "chatgpt",
381
- profileDir: join(homedir(), ".openclaw", "chatgpt-profile"),
382
- getCtx: () => chatgptContext,
383
- setCtx: (c: BrowserContext) => { chatgptContext = c; },
384
- homeUrl: "https://chatgpt.com",
385
- verifySelector: "#prompt-textarea",
386
- },
387
363
  ];
388
364
 
389
365
  for (const cfg of providerConfigs) {
@@ -415,7 +391,9 @@ async function _doEnsureAllProviderContexts(log: (msg: string) => void): Promise
415
391
  mkdirSync(cfg.profileDir, { recursive: true });
416
392
  const pCtx = await chromium.launchPersistentContext(cfg.profileDir, {
417
393
  headless: true,
418
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
394
+ channel: "chrome",
395
+ args: STEALTH_ARGS,
396
+ ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULTS],
419
397
  });
420
398
  const page = await pCtx.newPage();
421
399
  await page.goto(cfg.homeUrl, { waitUntil: "domcontentloaded", timeout: 15_000 });
@@ -746,97 +724,84 @@ const plugin = {
746
724
  const codexAuthPath = cfg.codexAuthPath ?? DEFAULT_CODEX_AUTH_PATH;
747
725
  const grokSessionPath = cfg.grokSessionPath ?? DEFAULT_SESSION_PATH;
748
726
 
749
- // ── Safe session restore on startup (sequential, profile-gated, non-blocking) ──
750
- // Restores provider sessions from saved persistent profiles but ONLY if the
751
- // profile directory already exists (i.e. user has logged in before).
752
- // Providers are launched one at a time with a delay to avoid OOM.
753
- void (async () => {
754
- await new Promise(r => setTimeout(r, 5000)); // wait for proxy + gateway to settle
755
- const { chromium } = await import("playwright");
756
- const { existsSync } = await import("node:fs");
757
-
758
- const profileProviders: Array<{
759
- name: string;
760
- profileDir: string;
761
- cookieFile: string;
762
- verifySelector: string;
763
- homeUrl: string;
764
- setCtx: (c: BrowserContext) => void;
765
- getCtx: () => BrowserContext | null;
766
- }> = [
767
- {
768
- name: "grok",
769
- profileDir: GROK_PROFILE_DIR,
770
- cookieFile: join(homedir(), ".openclaw", "grok-session.json"),
771
- verifySelector: "textarea",
772
- homeUrl: "https://grok.com",
773
- getCtx: () => grokContext,
774
- setCtx: (c) => { grokContext = c; },
775
- },
776
- {
777
- name: "claude",
778
- profileDir: join(homedir(), ".openclaw", "claude-profile"),
779
- cookieFile: join(homedir(), ".openclaw", "claude-cookie-expiry.json"),
780
- verifySelector: ".ProseMirror",
781
- homeUrl: "https://claude.ai/new",
782
- getCtx: () => claudeContext,
783
- setCtx: (c) => { claudeContext = c; },
784
- },
785
- {
786
- name: "gemini",
787
- profileDir: join(homedir(), ".openclaw", "gemini-profile"),
788
- cookieFile: join(homedir(), ".openclaw", "gemini-cookie-expiry.json"),
789
- verifySelector: ".ql-editor",
790
- homeUrl: "https://gemini.google.com/app",
791
- getCtx: () => geminiContext,
792
- setCtx: (c) => { geminiContext = c; },
793
- },
794
- {
795
- name: "chatgpt",
796
- profileDir: join(homedir(), ".openclaw", "chatgpt-profile"),
797
- cookieFile: join(homedir(), ".openclaw", "chatgpt-cookie-expiry.json"),
798
- verifySelector: "#prompt-textarea",
799
- homeUrl: "https://chatgpt.com",
800
- getCtx: () => chatgptContext,
801
- setCtx: (c) => { chatgptContext = c; },
802
- },
803
- ];
727
+ // ── Session restore: only on first plugin load (not on hot-reloads) ──────
728
+ // The gateway polls every ~60s via openclaw status, which triggers a hot-reload
729
+ // (SIGUSR1 + hybrid mode). Module-level contexts (grokContext etc.) survive
730
+ // hot-reloads because Node keeps the module in memory so we only need to
731
+ // restore once, on the very first load (when all contexts are null).
732
+ //
733
+ // Guard: _startupRestoreDone is module-level and persists across hot-reloads.
734
+ if (!_startupRestoreDone) {
735
+ _startupRestoreDone = true;
736
+ void (async () => {
737
+ await new Promise(r => setTimeout(r, 5000)); // wait for proxy + gateway to settle
738
+ const { chromium } = await import("playwright");
739
+ const { existsSync } = await import("node:fs");
740
+
741
+ const profileProviders: Array<{
742
+ name: string;
743
+ profileDir: string;
744
+ cookieFile: string;
745
+ verifySelector: string;
746
+ homeUrl: string;
747
+ setCtx: (c: BrowserContext) => void;
748
+ getCtx: () => BrowserContext | null;
749
+ }> = [
750
+ {
751
+ name: "grok",
752
+ profileDir: GROK_PROFILE_DIR,
753
+ cookieFile: join(homedir(), ".openclaw", "grok-session.json"),
754
+ verifySelector: "textarea",
755
+ homeUrl: "https://grok.com",
756
+ getCtx: () => grokContext,
757
+ setCtx: (c) => { grokContext = c; },
758
+ },
759
+ {
760
+ name: "gemini",
761
+ profileDir: join(homedir(), ".openclaw", "gemini-profile"),
762
+ cookieFile: join(homedir(), ".openclaw", "gemini-cookie-expiry.json"),
763
+ verifySelector: ".ql-editor",
764
+ homeUrl: "https://gemini.google.com/app",
765
+ getCtx: () => geminiContext,
766
+ setCtx: (c) => { geminiContext = c; },
767
+ },
768
+ ];
804
769
 
805
- for (const p of profileProviders) {
806
- // Skip if no saved profile/session exists
807
- if (!existsSync(p.profileDir) && !existsSync(p.cookieFile)) {
808
- api.logger.info(`[cli-bridge:${p.name}] no saved profile — skipping startup restore`);
809
- continue;
810
- }
811
- if (p.getCtx()) continue; // already connected
770
+ for (const p of profileProviders) {
771
+ if (!existsSync(p.profileDir) && !existsSync(p.cookieFile)) {
772
+ api.logger.info(`[cli-bridge:${p.name}] no saved profile — skipping startup restore`);
773
+ continue;
774
+ }
775
+ if (p.getCtx()) continue; // already connected
812
776
 
813
- try {
814
- api.logger.info(`[cli-bridge:${p.name}] restoring session from profile…`);
815
- const ctx = await chromium.launchPersistentContext(p.profileDir, {
816
- headless: true,
817
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
818
- });
819
- const page = await ctx.newPage();
820
- await page.goto(p.homeUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
821
- await new Promise(r => setTimeout(r, 3000));
822
- const ok = await page.locator(p.verifySelector).isVisible().catch(() => false);
823
- await page.close().catch(() => {});
824
- if (ok) {
825
- p.setCtx(ctx);
826
- ctx.on("close", () => { p.setCtx(null as unknown as BrowserContext); });
827
- api.logger.info(`[cli-bridge:${p.name}] session restored from profile ✅`);
828
- } else {
829
- await ctx.close().catch(() => {});
830
- api.logger.info(`[cli-bridge:${p.name}] profile exists but not logged in — skipping`);
777
+ try {
778
+ api.logger.info(`[cli-bridge:${p.name}] restoring session from profile…`);
779
+ const ctx = await chromium.launchPersistentContext(p.profileDir, {
780
+ headless: true,
781
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
782
+ });
783
+ const page = await ctx.newPage();
784
+ await page.goto(p.homeUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
785
+ await new Promise(r => setTimeout(r, 3000));
786
+ const ok = await page.locator(p.verifySelector).isVisible().catch(() => false);
787
+ await page.close().catch(() => {});
788
+ if (ok) {
789
+ p.setCtx(ctx);
790
+ ctx.on("close", () => { p.setCtx(null as unknown as BrowserContext); });
791
+ api.logger.info(`[cli-bridge:${p.name}] session restored from profile ✅`);
792
+ } else {
793
+ await ctx.close().catch(() => {});
794
+ api.logger.info(`[cli-bridge:${p.name}] profile exists but not logged in — needs /xxx-login`);
795
+ }
796
+ } catch (err) {
797
+ api.logger.warn(`[cli-bridge:${p.name}] startup restore failed: ${(err as Error).message}`);
831
798
  }
832
- } catch (err) {
833
- api.logger.warn(`[cli-bridge:${p.name}] startup restore failed: ${(err as Error).message}`);
834
- }
835
799
 
836
- // Sequential delay avoid spawning all 4 Chromium instances at once
837
- await new Promise(r => setTimeout(r, 3000));
838
- }
839
- })();
800
+ // Sequential — never spawn all 4 Chromium instances at once
801
+ await new Promise(r => setTimeout(r, 3000));
802
+ }
803
+ })();
804
+ }
840
805
 
841
806
  // ── Phase 1: openai-codex auth bridge ─────────────────────────────────────
842
807
  if (enableCodex) {
@@ -935,49 +900,24 @@ const plugin = {
935
900
  warn: (msg) => api.logger.warn(msg),
936
901
  getGrokContext: () => grokContext,
937
902
  connectGrokContext: async () => {
938
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
903
+ const ctx = await getOrLaunchGrokContext((msg) => api.logger.info(msg));
939
904
  if (ctx) {
940
905
  const check = await verifySession(ctx, (msg) => api.logger.info(msg));
941
906
  if (check.valid) { grokContext = ctx; return ctx; }
942
907
  }
943
908
  return null;
944
909
  },
945
- getClaudeContext: () => claudeContext,
946
- connectClaudeContext: async () => {
947
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
948
- if (ctx) {
949
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
950
- const { page } = await getOrCreateClaudePage(ctx);
951
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
952
- if (editor) { claudeContext = ctx; return ctx; }
953
- }
954
- // No fallback spawn — return existing context or null to avoid Chromium leak
955
- return claudeContext;
956
- },
957
910
  getGeminiContext: () => geminiContext,
958
911
  connectGeminiContext: async () => {
959
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
912
+ const ctx = await getOrLaunchGeminiContext((msg) => api.logger.info(msg));
960
913
  if (ctx) {
961
914
  const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
962
915
  const { page } = await getOrCreateGeminiPage(ctx);
963
916
  const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
964
917
  if (editor) { geminiContext = ctx; return ctx; }
965
918
  }
966
- // No fallback spawn — return existing context or null to avoid Chromium leak
967
919
  return geminiContext;
968
920
  },
969
- getChatGPTContext: () => chatgptContext,
970
- connectChatGPTContext: async () => {
971
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
972
- if (ctx) {
973
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
974
- const { page } = await getOrCreateChatGPTPage(ctx);
975
- const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
976
- if (editor) { chatgptContext = ctx; return ctx; }
977
- }
978
- // No fallback spawn — return existing context or null to avoid Chromium leak
979
- return chatgptContext;
980
- },
981
921
  });
982
922
  proxyServer = server;
983
923
  api.logger.info(
@@ -1010,18 +950,6 @@ const plugin = {
1010
950
  }
1011
951
  return null;
1012
952
  },
1013
- getClaudeContext: () => claudeContext,
1014
- connectClaudeContext: async () => {
1015
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1016
- if (ctx) {
1017
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1018
- const { page } = await getOrCreateClaudePage(ctx);
1019
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1020
- if (editor) { claudeContext = ctx; return ctx; }
1021
- }
1022
- // No fallback spawn — return existing context or null to avoid Chromium leak
1023
- return claudeContext;
1024
- },
1025
953
  getGeminiContext: () => geminiContext,
1026
954
  connectGeminiContext: async () => {
1027
955
  const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
@@ -1034,18 +962,6 @@ const plugin = {
1034
962
  // No fallback spawn — return existing context or null to avoid Chromium leak
1035
963
  return geminiContext;
1036
964
  },
1037
- getChatGPTContext: () => chatgptContext,
1038
- connectChatGPTContext: async () => {
1039
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1040
- if (ctx) {
1041
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1042
- const { page } = await getOrCreateChatGPTPage(ctx);
1043
- const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
1044
- if (editor) { chatgptContext = ctx; return ctx; }
1045
- }
1046
- // No fallback spawn — return existing context or null to avoid Chromium leak
1047
- return chatgptContext;
1048
- },
1049
965
  });
1050
966
  proxyServer = server;
1051
967
  api.logger.info(`[cli-bridge] proxy ready on :${port} (retry)`);
@@ -1386,107 +1302,6 @@ const plugin = {
1386
1302
  },
1387
1303
  } satisfies OpenClawPluginCommandDefinition);
1388
1304
 
1389
- // ── Claude web-session commands ───────────────────────────────────────────
1390
- api.registerCommand({
1391
- name: "claude-login",
1392
- description: "Authenticate claude.ai: imports session from OpenClaw browser",
1393
- handler: async (): Promise<PluginCommandResult> => {
1394
- if (claudeContext) {
1395
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1396
- try {
1397
- const { page } = await getOrCreateClaudePage(claudeContext);
1398
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1399
- if (editor) return { text: "✅ Already connected to claude.ai. Use `/claude-logout` first to reset." };
1400
- } catch { /* fall through */ }
1401
- claudeContext = null;
1402
- }
1403
-
1404
- api.logger.info("[cli-bridge:claude] /claude-login: connecting to OpenClaw browser…");
1405
-
1406
- // Connect to OpenClaw browser context for session (singleton CDP)
1407
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1408
- if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure claude.ai is open in your browser." };
1409
-
1410
- // Navigate to claude.ai/new if not already there
1411
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1412
- let page;
1413
- try {
1414
- ({ page } = await getOrCreateClaudePage(ctx));
1415
- } catch (err) {
1416
- return { text: `❌ Failed to open claude.ai: ${(err as Error).message}` };
1417
- }
1418
-
1419
- // Verify editor is visible
1420
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1421
- if (!editor) {
1422
- return { text: "❌ claude.ai editor not visible — are you logged in?\nOpen claude.ai in your browser and try again." };
1423
- }
1424
-
1425
- claudeContext = ctx;
1426
-
1427
- // Export + bake cookies into persistent profile
1428
- const claudeProfileDir = join(homedir(), ".openclaw", "claude-profile");
1429
- mkdirSync(claudeProfileDir, { recursive: true });
1430
- try {
1431
- const allCookies = await ctx.cookies([
1432
- "https://claude.ai",
1433
- "https://anthropic.com",
1434
- ]);
1435
- const { chromium } = await import("playwright");
1436
- const pCtx = await chromium.launchPersistentContext(claudeProfileDir, {
1437
- headless: true,
1438
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
1439
- });
1440
- await pCtx.addCookies(allCookies);
1441
- await pCtx.close();
1442
- api.logger.info(`[cli-bridge:claude] cookies baked into ${claudeProfileDir}`);
1443
- } catch (err) {
1444
- api.logger.warn(`[cli-bridge:claude] cookie bake failed: ${(err as Error).message}`);
1445
- }
1446
-
1447
- // Scan cookie expiry
1448
- const expiry = await scanClaudeCookieExpiry(ctx);
1449
- if (expiry) {
1450
- saveClaudeExpiry(expiry);
1451
- api.logger.info(`[cli-bridge:claude] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`);
1452
- }
1453
- const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatClaudeExpiry(expiry)}` : "";
1454
-
1455
- 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}` };
1456
- },
1457
- } satisfies OpenClawPluginCommandDefinition);
1458
-
1459
- api.registerCommand({
1460
- name: "claude-status",
1461
- description: "Check claude.ai session status",
1462
- handler: async (): Promise<PluginCommandResult> => {
1463
- if (!claudeContext) {
1464
- return { text: "❌ No active claude.ai session\nRun `/claude-login` to authenticate." };
1465
- }
1466
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1467
- try {
1468
- const { page } = await getOrCreateClaudePage(claudeContext);
1469
- const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
1470
- if (editor) {
1471
- const expiry = loadClaudeExpiry();
1472
- const expiryLine = expiry ? `\n🕐 ${formatClaudeExpiry(expiry)}` : "";
1473
- 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}` };
1474
- }
1475
- } catch { /* fall through */ }
1476
- claudeContext = null;
1477
- return { text: "❌ Session lost — run `/claude-login` to re-authenticate." };
1478
- },
1479
- } satisfies OpenClawPluginCommandDefinition);
1480
-
1481
- api.registerCommand({
1482
- name: "claude-logout",
1483
- description: "Disconnect from claude.ai session",
1484
- handler: async (): Promise<PluginCommandResult> => {
1485
- claudeContext = null;
1486
- return { text: "✅ Disconnected from claude.ai. Run `/claude-login` to reconnect." };
1487
- },
1488
- } satisfies OpenClawPluginCommandDefinition);
1489
-
1490
1305
  // ── Gemini web-session commands ───────────────────────────────────────────
1491
1306
  api.registerCommand({
1492
1307
  name: "gemini-login",
@@ -1502,10 +1317,34 @@ const plugin = {
1502
1317
  geminiContext = null;
1503
1318
  }
1504
1319
 
1505
- api.logger.info("[cli-bridge:gemini] /gemini-login: connecting to OpenClaw browser…");
1506
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1507
- 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." };
1508
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
1509
1348
  const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
1510
1349
  let page;
1511
1350
  try {
@@ -1514,35 +1353,40 @@ const plugin = {
1514
1353
  return { text: `❌ Failed to open gemini.google.com: ${(err as Error).message}` };
1515
1354
  }
1516
1355
 
1517
- const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
1356
+ let editor = await page.locator(".ql-editor").isVisible().catch(() => false);
1518
1357
  if (!editor) {
1519
- return { text: "❌ Gemini editor not visible are you logged in?\nOpen gemini.google.com in your browser and try again." };
1520
- }
1521
-
1522
- 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;
1523
1362
 
1524
- // Export + bake cookies into persistent profile
1525
- const geminiProfileDir = join(homedir(), ".openclaw", "gemini-profile");
1526
- mkdirSync(geminiProfileDir, { recursive: true });
1527
- try {
1528
- const allCookies = await ctx.cookies([
1529
- "https://gemini.google.com",
1530
- "https://accounts.google.com",
1531
- "https://google.com",
1532
- ]);
1533
1363
  const { chromium } = await import("playwright");
1534
- const pCtx = await chromium.launchPersistentContext(geminiProfileDir, {
1535
- headless: true,
1536
- 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],
1537
1369
  });
1538
- await pCtx.addCookies(allCookies);
1539
- await pCtx.close();
1540
- api.logger.info(`[cli-bridge:gemini] cookies baked into ${geminiProfileDir}`);
1541
- } catch (err) {
1542
- 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;
1543
1387
  }
1544
1388
 
1545
- const expiry = await scanGeminiCookieExpiry(ctx);
1389
+ const expiry = await scanGeminiCookieExpiry(geminiContext!);
1546
1390
  if (expiry) {
1547
1391
  saveGeminiExpiry(expiry);
1548
1392
  api.logger.info(`[cli-bridge:gemini] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`);
@@ -1584,94 +1428,10 @@ const plugin = {
1584
1428
  },
1585
1429
  } satisfies OpenClawPluginCommandDefinition);
1586
1430
 
1587
- // ── ChatGPT web-session commands ──────────────────────────────────────────
1588
- api.registerCommand({
1589
- name: "chatgpt-login",
1590
- description: "Authenticate chatgpt.com: imports session from OpenClaw browser",
1591
- handler: async (): Promise<PluginCommandResult> => {
1592
- if (chatgptContext) {
1593
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1594
- try {
1595
- const { page } = await getOrCreateChatGPTPage(chatgptContext);
1596
- if (await page.locator("#prompt-textarea").isVisible().catch(() => false))
1597
- return { text: "✅ Already connected to chatgpt.com. Use `/chatgpt-logout` first to reset." };
1598
- } catch { /* fall through */ }
1599
- chatgptContext = null;
1600
- }
1601
- api.logger.info("[cli-bridge:chatgpt] /chatgpt-login: connecting to OpenClaw browser…");
1602
- const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
1603
- if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure chatgpt.com is open in your browser." };
1604
-
1605
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1606
- let page;
1607
- try { ({ page } = await getOrCreateChatGPTPage(ctx)); }
1608
- catch (err) { return { text: `❌ Failed to open chatgpt.com: ${(err as Error).message}` }; }
1609
-
1610
- if (!await page.locator("#prompt-textarea").isVisible().catch(() => false))
1611
- return { text: "❌ ChatGPT editor not visible — are you logged in?\nOpen chatgpt.com in your browser and try again." };
1612
-
1613
- chatgptContext = ctx;
1614
-
1615
- // Export + bake cookies into persistent profile
1616
- const chatgptProfileDir = join(homedir(), ".openclaw", "chatgpt-profile");
1617
- mkdirSync(chatgptProfileDir, { recursive: true });
1618
- try {
1619
- const allCookies = await ctx.cookies([
1620
- "https://chatgpt.com",
1621
- "https://openai.com",
1622
- "https://auth.openai.com",
1623
- ]);
1624
- const { chromium } = await import("playwright");
1625
- const pCtx = await chromium.launchPersistentContext(chatgptProfileDir, {
1626
- headless: true,
1627
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
1628
- });
1629
- await pCtx.addCookies(allCookies);
1630
- await pCtx.close();
1631
- api.logger.info(`[cli-bridge:chatgpt] cookies baked into ${chatgptProfileDir}`);
1632
- } catch (err) {
1633
- api.logger.warn(`[cli-bridge:chatgpt] cookie bake failed: ${(err as Error).message}`);
1634
- }
1635
-
1636
- const expiry = await scanChatGPTCookieExpiry(ctx);
1637
- if (expiry) { saveChatGPTExpiry(expiry); api.logger.info(`[cli-bridge:chatgpt] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`); }
1638
- const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatChatGPTExpiry(expiry)}` : "";
1639
- 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}` };
1640
- },
1641
- } satisfies OpenClawPluginCommandDefinition);
1642
-
1643
- api.registerCommand({
1644
- name: "chatgpt-status",
1645
- description: "Check chatgpt.com session status",
1646
- handler: async (): Promise<PluginCommandResult> => {
1647
- if (!chatgptContext) return { text: "❌ No active chatgpt.com session\nRun `/chatgpt-login` to authenticate." };
1648
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1649
- try {
1650
- const { page } = await getOrCreateChatGPTPage(chatgptContext);
1651
- if (await page.locator("#prompt-textarea").isVisible().catch(() => false)) {
1652
- const expiry = loadChatGPTExpiry();
1653
- const expiryLine = expiry ? `\n🕐 ${formatChatGPTExpiry(expiry)}` : "";
1654
- 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}` };
1655
- }
1656
- } catch { /* fall through */ }
1657
- chatgptContext = null;
1658
- return { text: "❌ Session lost — run `/chatgpt-login` to re-authenticate." };
1659
- },
1660
- } satisfies OpenClawPluginCommandDefinition);
1661
-
1662
- api.registerCommand({
1663
- name: "chatgpt-logout",
1664
- description: "Disconnect from chatgpt.com session",
1665
- handler: async (): Promise<PluginCommandResult> => {
1666
- chatgptContext = null;
1667
- return { text: "✅ Disconnected from chatgpt.com. Run `/chatgpt-login` to reconnect." };
1668
- },
1669
- } satisfies OpenClawPluginCommandDefinition);
1670
-
1671
1431
  // ── /bridge-status — all providers at a glance ───────────────────────────
1672
1432
  api.registerCommand({
1673
1433
  name: "bridge-status",
1674
- description: "Show status of all headless browser providers (Grok, Claude, Gemini, ChatGPT)",
1434
+ description: "Show status of all headless browser providers (Grok, Gemini)",
1675
1435
  handler: async (): Promise<PluginCommandResult> => {
1676
1436
  const lines: string[] = [`🌉 *CLI Bridge v${plugin.version} — Provider Status*\n`];
1677
1437
 
@@ -1689,21 +1449,6 @@ const plugin = {
1689
1449
  loginCmd: "/grok-login",
1690
1450
  expiry: () => { const e = loadGrokExpiry(); return e ? formatExpiryInfo(e) : null; },
1691
1451
  },
1692
- {
1693
- name: "Claude",
1694
- ctx: claudeContext,
1695
- check: async () => {
1696
- if (!claudeContext) return false;
1697
- try {
1698
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
1699
- const { page } = await getOrCreateClaudePage(claudeContext);
1700
- return page.locator(".ProseMirror").isVisible().catch(() => false);
1701
- } catch { claudeContext = null; return false; }
1702
- },
1703
- models: "web-claude/claude-sonnet, claude-opus, claude-haiku",
1704
- loginCmd: "/claude-login",
1705
- expiry: () => { const e = loadClaudeExpiry(); return e ? formatClaudeExpiry(e) : null; },
1706
- },
1707
1452
  {
1708
1453
  name: "Gemini",
1709
1454
  ctx: geminiContext,
@@ -1719,21 +1464,6 @@ const plugin = {
1719
1464
  loginCmd: "/gemini-login",
1720
1465
  expiry: () => { const e = loadGeminiExpiry(); return e ? formatGeminiExpiry(e) : null; },
1721
1466
  },
1722
- {
1723
- name: "ChatGPT",
1724
- ctx: chatgptContext,
1725
- check: async () => {
1726
- if (!chatgptContext) return false;
1727
- try {
1728
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
1729
- const { page } = await getOrCreateChatGPTPage(chatgptContext);
1730
- return page.locator("#prompt-textarea").isVisible().catch(() => false);
1731
- } catch { chatgptContext = null; return false; }
1732
- },
1733
- models: "web-chatgpt/gpt-4o, gpt-o3, gpt-o4-mini, gpt-5",
1734
- loginCmd: "/chatgpt-login",
1735
- expiry: () => { const e = loadChatGPTExpiry(); return e ? formatChatGPTExpiry(e) : null; },
1736
- },
1737
1467
  ];
1738
1468
 
1739
1469
  for (const c of checks) {
@@ -1750,7 +1480,7 @@ const plugin = {
1750
1480
  lines.push("");
1751
1481
  }
1752
1482
 
1753
- lines.push(`🔌 Proxy: \`127.0.0.1:${port}\` | 22 models total`);
1483
+ lines.push(`🔌 Proxy: \`127.0.0.1:${port}\``);
1754
1484
  return { text: lines.join("\n") };
1755
1485
  },
1756
1486
  } satisfies OpenClawPluginCommandDefinition);
@@ -1764,15 +1494,9 @@ const plugin = {
1764
1494
  "/grok-login",
1765
1495
  "/grok-status",
1766
1496
  "/grok-logout",
1767
- "/claude-login",
1768
- "/claude-status",
1769
- "/claude-logout",
1770
1497
  "/gemini-login",
1771
1498
  "/gemini-status",
1772
1499
  "/gemini-logout",
1773
- "/chatgpt-login",
1774
- "/chatgpt-status",
1775
- "/chatgpt-logout",
1776
1500
  "/bridge-status",
1777
1501
  ];
1778
1502
  api.logger.info(`[cli-bridge] registered ${allCommands.length} commands: ${allCommands.join(", ")}`);