@elvatis_com/openclaw-cli-bridge-elvatis 1.2.0 → 1.3.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
4
4
 
5
- **Current version:** `1.2.0`
5
+ **Current version:** `1.3.1`
6
6
 
7
7
  ---
8
8
 
@@ -287,6 +287,16 @@ npm test # vitest run (45 tests)
287
287
 
288
288
  ## Changelog
289
289
 
290
+ ### v1.3.1
291
+ - **fix:** /claude-login, /gemini-login, /chatgpt-login now bake cookies into persistent profile dirs
292
+ - **fix:** After gateway restart, providers auto-reconnect from saved profile (no browser tabs needed)
293
+ - **fix:** Better debug logging when persistent headless context fails (Cloudflare etc.)
294
+
295
+ ### v1.3.0
296
+ - **fix:** Browser persistence after gateway restart — each provider launches its own persistent Chromium if OpenClaw browser is unavailable
297
+ - **feat:** `ensureAllProviderContexts()` — unified startup connect for all 4 providers
298
+ - **feat:** Lazy-connect fallback to persistent context when CDP unavailable
299
+
290
300
  ### v1.2.0
291
301
  - **fix:** Fresh page per request — no more message accumulation across calls
292
302
  - **feat:** ChatGPT model switching via URL param (?model=gpt-4o, o3, etc.)
package/SKILL.md CHANGED
@@ -53,4 +53,4 @@ Each command runs `openclaw models set <model>` atomically and replies with a co
53
53
 
54
54
  See `README.md` for full configuration reference and architecture diagram.
55
55
 
56
- **Version:** 1.2.0
56
+ **Version:** 1.3.1
package/index.ts CHANGED
@@ -322,6 +322,103 @@ async function cleanupBrowsers(log: (msg: string) => void): Promise<void> {
322
322
  log("[cli-bridge] browser resources cleaned up");
323
323
  }
324
324
 
325
+ /**
326
+ * Ensure all browser provider contexts are connected.
327
+ * 1. Try the shared OpenClaw browser (CDP 18800)
328
+ * 2. Fallback: launch a persistent headless Chromium per provider (saved profile with cookies)
329
+ */
330
+ async function ensureAllProviderContexts(log: (msg: string) => void): Promise<void> {
331
+ const { chromium } = await import("playwright");
332
+
333
+ // Try CDP first (OpenClaw browser)
334
+ let sharedCtx: BrowserContext | null = null;
335
+ try {
336
+ const b = await chromium.connectOverCDP("http://127.0.0.1:18800", { timeout: 2000 });
337
+ sharedCtx = b.contexts()[0] ?? null;
338
+ if (sharedCtx) log("[cli-bridge] using OpenClaw browser for all providers");
339
+ } catch { /* not available */ }
340
+
341
+ // For each provider: if no context yet, try shared ctx or launch own persistent context
342
+ const providerConfigs = [
343
+ {
344
+ name: "claude",
345
+ profileDir: join(homedir(), ".openclaw", "claude-profile"),
346
+ getCtx: () => claudeContext,
347
+ setCtx: (c: BrowserContext) => { claudeContext = c; },
348
+ homeUrl: "https://claude.ai/new",
349
+ verifySelector: ".ProseMirror",
350
+ },
351
+ {
352
+ name: "gemini",
353
+ profileDir: join(homedir(), ".openclaw", "gemini-profile"),
354
+ getCtx: () => geminiContext,
355
+ setCtx: (c: BrowserContext) => { geminiContext = c; },
356
+ homeUrl: "https://gemini.google.com/app",
357
+ verifySelector: ".ql-editor",
358
+ },
359
+ {
360
+ name: "chatgpt",
361
+ profileDir: join(homedir(), ".openclaw", "chatgpt-profile"),
362
+ getCtx: () => chatgptContext,
363
+ setCtx: (c: BrowserContext) => { chatgptContext = c; },
364
+ homeUrl: "https://chatgpt.com",
365
+ verifySelector: "#prompt-textarea",
366
+ },
367
+ ];
368
+
369
+ for (const cfg of providerConfigs) {
370
+ if (cfg.getCtx()) continue; // already connected
371
+
372
+ let ctx: BrowserContext | null = null;
373
+
374
+ // 1. Try shared OpenClaw browser context
375
+ if (sharedCtx) {
376
+ try {
377
+ const pages = sharedCtx.pages();
378
+ const existing = pages.find(p => p.url().includes(new URL(cfg.homeUrl).hostname));
379
+ const page = existing ?? await sharedCtx.newPage();
380
+ if (!existing) {
381
+ await page.goto(cfg.homeUrl, { waitUntil: "domcontentloaded", timeout: 10_000 });
382
+ await new Promise(r => setTimeout(r, 2000));
383
+ }
384
+ const visible = await page.locator(cfg.verifySelector).isVisible().catch(() => false);
385
+ if (visible) {
386
+ ctx = sharedCtx;
387
+ log(`[cli-bridge:${cfg.name}] connected via OpenClaw browser ✅`);
388
+ }
389
+ } catch { /* fall through */ }
390
+ }
391
+
392
+ // 2. Launch own persistent context (has saved cookies)
393
+ if (!ctx) {
394
+ try {
395
+ mkdirSync(cfg.profileDir, { recursive: true });
396
+ const pCtx = await chromium.launchPersistentContext(cfg.profileDir, {
397
+ headless: true,
398
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
399
+ });
400
+ const page = await pCtx.newPage();
401
+ await page.goto(cfg.homeUrl, { waitUntil: "domcontentloaded", timeout: 15_000 });
402
+ await new Promise(r => setTimeout(r, 4000));
403
+ const visible = await page.locator(cfg.verifySelector).isVisible().catch(() => false);
404
+ if (visible) {
405
+ ctx = pCtx;
406
+ log(`[cli-bridge:${cfg.name}] launched persistent context ✅`);
407
+ } else {
408
+ const title = await page.title().catch(() => "unknown");
409
+ const bodySnippet = await page.evaluate(() => document.body?.innerText?.substring(0, 100) ?? "").catch(() => "");
410
+ log(`[cli-bridge:${cfg.name}] persistent headless: editor not visible — title="${title}" body="${bodySnippet}"`);
411
+ await pCtx.close().catch(() => {});
412
+ }
413
+ } catch (err) {
414
+ log(`[cli-bridge:${cfg.name}] could not launch browser: ${(err as Error).message}`);
415
+ }
416
+ }
417
+
418
+ if (ctx) cfg.setCtx(ctx);
419
+ }
420
+ }
421
+
325
422
  async function tryRestoreGrokSession(
326
423
  _sessionPath: string,
327
424
  log: (msg: string) => void
@@ -613,7 +710,7 @@ function proxyTestRequest(
613
710
  const plugin = {
614
711
  id: "openclaw-cli-bridge-elvatis",
615
712
  name: "OpenClaw CLI Bridge",
616
- version: "1.2.0",
713
+ version: "1.3.1",
617
714
  description:
618
715
  "Phase 1: openai-codex auth bridge. " +
619
716
  "Phase 2: HTTP proxy for gemini/claude CLIs. " +
@@ -634,49 +731,8 @@ const plugin = {
634
731
 
635
732
  // ── Auto-connect all browser providers on startup (non-blocking) ──────────
636
733
  void (async () => {
637
- // Wait for proxy to start first
638
- await new Promise(r => setTimeout(r, 3000));
639
- const log = (msg: string) => api.logger.info(msg);
640
- const ctx = await connectToOpenClawBrowser(log);
641
- if (!ctx) { log("[cli-bridge] startup auto-connect: OpenClaw browser not available"); return; }
642
- const pages = ctx.pages().map(p => p.url());
643
- log(`[cli-bridge] startup auto-connect: ${pages.length} pages open`);
644
-
645
- // Claude
646
- if (pages.some(u => u.includes("claude.ai")) && !claudeContext) {
647
- try {
648
- const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
649
- const { page } = await getOrCreateClaudePage(ctx);
650
- if (await page.locator(".ProseMirror").isVisible().catch(() => false)) {
651
- claudeContext = ctx;
652
- log("[cli-bridge:claude] auto-connected ✅");
653
- }
654
- } catch { /* not available */ }
655
- }
656
-
657
- // Gemini
658
- if (pages.some(u => u.includes("gemini.google.com")) && !geminiContext) {
659
- try {
660
- const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
661
- const { page } = await getOrCreateGeminiPage(ctx);
662
- if (await page.locator(".ql-editor").isVisible().catch(() => false)) {
663
- geminiContext = ctx;
664
- log("[cli-bridge:gemini] auto-connected ✅");
665
- }
666
- } catch { /* not available */ }
667
- }
668
-
669
- // ChatGPT
670
- if (pages.some(u => u.includes("chatgpt.com")) && !chatgptContext) {
671
- try {
672
- const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
673
- const { page } = await getOrCreateChatGPTPage(ctx);
674
- if (await page.locator("#prompt-textarea").isVisible().catch(() => false)) {
675
- chatgptContext = ctx;
676
- log("[cli-bridge:chatgpt] auto-connected ✅");
677
- }
678
- } catch { /* not available */ }
679
- }
734
+ await new Promise(r => setTimeout(r, 3000)); // wait for proxy to start
735
+ await ensureAllProviderContexts((msg) => api.logger.info(msg));
680
736
  })();
681
737
 
682
738
  // ── Phase 1: openai-codex auth bridge ─────────────────────────────────────
@@ -792,7 +848,9 @@ const plugin = {
792
848
  const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
793
849
  if (editor) { claudeContext = ctx; return ctx; }
794
850
  }
795
- return null;
851
+ // Fallback: try persistent context
852
+ await ensureAllProviderContexts((msg) => api.logger.info(msg));
853
+ return claudeContext;
796
854
  },
797
855
  getGeminiContext: () => geminiContext,
798
856
  connectGeminiContext: async () => {
@@ -803,7 +861,9 @@ const plugin = {
803
861
  const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
804
862
  if (editor) { geminiContext = ctx; return ctx; }
805
863
  }
806
- return null;
864
+ // Fallback: try persistent context
865
+ await ensureAllProviderContexts((msg) => api.logger.info(msg));
866
+ return geminiContext;
807
867
  },
808
868
  getChatGPTContext: () => chatgptContext,
809
869
  connectChatGPTContext: async () => {
@@ -814,7 +874,9 @@ const plugin = {
814
874
  const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
815
875
  if (editor) { chatgptContext = ctx; return ctx; }
816
876
  }
817
- return null;
877
+ // Fallback: try persistent context
878
+ await ensureAllProviderContexts((msg) => api.logger.info(msg));
879
+ return chatgptContext;
818
880
  },
819
881
  });
820
882
  proxyServer = server;
@@ -857,7 +919,9 @@ const plugin = {
857
919
  const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
858
920
  if (editor) { claudeContext = ctx; return ctx; }
859
921
  }
860
- return null;
922
+ // Fallback: try persistent context
923
+ await ensureAllProviderContexts((msg) => api.logger.info(msg));
924
+ return claudeContext;
861
925
  },
862
926
  getGeminiContext: () => geminiContext,
863
927
  connectGeminiContext: async () => {
@@ -868,7 +932,9 @@ const plugin = {
868
932
  const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
869
933
  if (editor) { geminiContext = ctx; return ctx; }
870
934
  }
871
- return null;
935
+ // Fallback: try persistent context
936
+ await ensureAllProviderContexts((msg) => api.logger.info(msg));
937
+ return geminiContext;
872
938
  },
873
939
  getChatGPTContext: () => chatgptContext,
874
940
  connectChatGPTContext: async () => {
@@ -879,7 +945,9 @@ const plugin = {
879
945
  const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
880
946
  if (editor) { chatgptContext = ctx; return ctx; }
881
947
  }
882
- return null;
948
+ // Fallback: try persistent context
949
+ await ensureAllProviderContexts((msg) => api.logger.info(msg));
950
+ return chatgptContext;
883
951
  },
884
952
  });
885
953
  proxyServer = server;
@@ -1259,6 +1327,26 @@ const plugin = {
1259
1327
 
1260
1328
  claudeContext = ctx;
1261
1329
 
1330
+ // Export + bake cookies into persistent profile
1331
+ const claudeProfileDir = join(homedir(), ".openclaw", "claude-profile");
1332
+ mkdirSync(claudeProfileDir, { recursive: true });
1333
+ try {
1334
+ const allCookies = await ctx.cookies([
1335
+ "https://claude.ai",
1336
+ "https://anthropic.com",
1337
+ ]);
1338
+ const { chromium } = await import("playwright");
1339
+ const pCtx = await chromium.launchPersistentContext(claudeProfileDir, {
1340
+ headless: true,
1341
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
1342
+ });
1343
+ await pCtx.addCookies(allCookies);
1344
+ await pCtx.close();
1345
+ api.logger.info(`[cli-bridge:claude] cookies baked into ${claudeProfileDir}`);
1346
+ } catch (err) {
1347
+ api.logger.warn(`[cli-bridge:claude] cookie bake failed: ${(err as Error).message}`);
1348
+ }
1349
+
1262
1350
  // Scan cookie expiry
1263
1351
  const expiry = await scanClaudeCookieExpiry(ctx);
1264
1352
  if (expiry) {
@@ -1336,6 +1424,27 @@ const plugin = {
1336
1424
 
1337
1425
  geminiContext = ctx;
1338
1426
 
1427
+ // Export + bake cookies into persistent profile
1428
+ const geminiProfileDir = join(homedir(), ".openclaw", "gemini-profile");
1429
+ mkdirSync(geminiProfileDir, { recursive: true });
1430
+ try {
1431
+ const allCookies = await ctx.cookies([
1432
+ "https://gemini.google.com",
1433
+ "https://accounts.google.com",
1434
+ "https://google.com",
1435
+ ]);
1436
+ const { chromium } = await import("playwright");
1437
+ const pCtx = await chromium.launchPersistentContext(geminiProfileDir, {
1438
+ headless: true,
1439
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
1440
+ });
1441
+ await pCtx.addCookies(allCookies);
1442
+ await pCtx.close();
1443
+ api.logger.info(`[cli-bridge:gemini] cookies baked into ${geminiProfileDir}`);
1444
+ } catch (err) {
1445
+ api.logger.warn(`[cli-bridge:gemini] cookie bake failed: ${(err as Error).message}`);
1446
+ }
1447
+
1339
1448
  const expiry = await scanGeminiCookieExpiry(ctx);
1340
1449
  if (expiry) {
1341
1450
  saveGeminiExpiry(expiry);
@@ -1405,6 +1514,28 @@ const plugin = {
1405
1514
  return { text: "❌ ChatGPT editor not visible — are you logged in?\nOpen chatgpt.com in your browser and try again." };
1406
1515
 
1407
1516
  chatgptContext = ctx;
1517
+
1518
+ // Export + bake cookies into persistent profile
1519
+ const chatgptProfileDir = join(homedir(), ".openclaw", "chatgpt-profile");
1520
+ mkdirSync(chatgptProfileDir, { recursive: true });
1521
+ try {
1522
+ const allCookies = await ctx.cookies([
1523
+ "https://chatgpt.com",
1524
+ "https://openai.com",
1525
+ "https://auth.openai.com",
1526
+ ]);
1527
+ const { chromium } = await import("playwright");
1528
+ const pCtx = await chromium.launchPersistentContext(chatgptProfileDir, {
1529
+ headless: true,
1530
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
1531
+ });
1532
+ await pCtx.addCookies(allCookies);
1533
+ await pCtx.close();
1534
+ api.logger.info(`[cli-bridge:chatgpt] cookies baked into ${chatgptProfileDir}`);
1535
+ } catch (err) {
1536
+ api.logger.warn(`[cli-bridge:chatgpt] cookie bake failed: ${(err as Error).message}`);
1537
+ }
1538
+
1408
1539
  const expiry = await scanChatGPTCookieExpiry(ctx);
1409
1540
  if (expiry) { saveChatGPTExpiry(expiry); api.logger.info(`[cli-bridge:chatgpt] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`); }
1410
1541
  const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatChatGPTExpiry(expiry)}` : "";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-cli-bridge-elvatis",
3
3
  "name": "OpenClaw CLI Bridge",
4
- "version": "1.2.0",
4
+ "version": "1.3.1",
5
5
  "description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
6
6
  "providers": [
7
7
  "openai-codex"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvatis_com/openclaw-cli-bridge-elvatis",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Bridges gemini, claude, and codex CLI tools as OpenClaw model providers. Reads existing CLI auth without re-login.",
5
5
  "type": "module",
6
6
  "openclaw": {