@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 +11 -1
- package/SKILL.md +1 -1
- package/index.ts +181 -50
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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.
|
|
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
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.
|
|
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
|
-
//
|
|
638
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)}` : "";
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-cli-bridge-elvatis",
|
|
3
3
|
"name": "OpenClaw CLI Bridge",
|
|
4
|
-
"version": "1.
|
|
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.
|
|
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": {
|