@elvatis_com/openclaw-cli-bridge-elvatis 0.2.29 → 1.0.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/README.md +22 -1
- package/SKILL.md +1 -1
- package/index.ts +325 -54
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/chatgpt-browser.ts +253 -0
- package/src/gemini-browser.ts +242 -0
- package/src/proxy-server.ts +115 -0
- package/test/chatgpt-proxy.test.ts +107 -0
- package/test/gemini-proxy.test.ts +139 -0
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:** `0.
|
|
5
|
+
**Current version:** `1.0.0`
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -287,6 +287,27 @@ npm test # vitest run (45 tests)
|
|
|
287
287
|
|
|
288
288
|
## Changelog
|
|
289
289
|
|
|
290
|
+
## v1.0.0 — Full Headless Browser Bridge 🚀
|
|
291
|
+
|
|
292
|
+
All four major LLM providers are now available via browser automation.
|
|
293
|
+
No CLI binaries required — just authenticated browser sessions.
|
|
294
|
+
|
|
295
|
+
### v1.0.0
|
|
296
|
+
- **feat:** `chatgpt-browser.ts` — chatgpt.com DOM-automation (`#prompt-textarea` + `[data-message-author-role]`)
|
|
297
|
+
- **feat:** `web-chatgpt/*` models: gpt-4o, gpt-4o-mini, gpt-o3, gpt-o4-mini, gpt-5
|
|
298
|
+
- **feat:** `/chatgpt-login`, `/chatgpt-status`, `/chatgpt-logout` + cookie-expiry tracking
|
|
299
|
+
- **feat:** All 4 providers headless: Grok ✅ Claude ✅ Gemini ✅ ChatGPT ✅
|
|
300
|
+
- **test:** 96/96 tests green (8 test files)
|
|
301
|
+
- **fix:** Singleton CDP connection, cleanupBrowsers() on plugin stop
|
|
302
|
+
|
|
303
|
+
### v0.2.30
|
|
304
|
+
- **feat:** `gemini-browser.ts` — gemini.google.com DOM-automation (Quill editor + message-content polling)
|
|
305
|
+
- **feat:** `web-gemini/*` models in proxy (gemini-2-5-pro, gemini-2-5-flash, gemini-3-pro, gemini-3-flash)
|
|
306
|
+
- **feat:** `/gemini-login`, `/gemini-status`, `/gemini-logout` commands + cookie-expiry tracking
|
|
307
|
+
- **fix:** Singleton CDP connection — no more zombie Chromium processes
|
|
308
|
+
- **fix:** `cleanupBrowsers()` called on plugin stop — all browser resources released
|
|
309
|
+
- **test:** 90/90 tests green (+6 gemini-proxy tests)
|
|
310
|
+
|
|
290
311
|
### v0.2.29
|
|
291
312
|
- **feat:** `claude-browser.ts` — claude.ai DOM-automation (ProseMirror + `[data-test-render-count]` polling)
|
|
292
313
|
- **feat:** `web-claude/*` models in proxy (web-claude/claude-sonnet, claude-opus, claude-haiku)
|
package/SKILL.md
CHANGED
package/index.ts
CHANGED
|
@@ -89,6 +89,64 @@ let grokContext: BrowserContext | null = null;
|
|
|
89
89
|
// Persistent profile dir — survives gateway restarts, keeps cookies intact
|
|
90
90
|
const GROK_PROFILE_DIR = join(homedir(), ".openclaw", "grok-profile");
|
|
91
91
|
|
|
92
|
+
// ── Gemini web-session state ──────────────────────────────────────────────────
|
|
93
|
+
let geminiContext: BrowserContext | null = null;
|
|
94
|
+
const GEMINI_EXPIRY_FILE = join(homedir(), ".openclaw", "gemini-cookie-expiry.json");
|
|
95
|
+
|
|
96
|
+
interface GeminiExpiryInfo { expiresAt: number; loginAt: number; cookieName: string; }
|
|
97
|
+
|
|
98
|
+
function saveGeminiExpiry(info: GeminiExpiryInfo): void {
|
|
99
|
+
try { writeFileSync(GEMINI_EXPIRY_FILE, JSON.stringify(info, null, 2)); } catch { /* ignore */ }
|
|
100
|
+
}
|
|
101
|
+
function loadGeminiExpiry(): GeminiExpiryInfo | null {
|
|
102
|
+
try { return JSON.parse(readFileSync(GEMINI_EXPIRY_FILE, "utf-8")) as GeminiExpiryInfo; } catch { return null; }
|
|
103
|
+
}
|
|
104
|
+
function formatGeminiExpiry(info: GeminiExpiryInfo): string {
|
|
105
|
+
const daysLeft = Math.floor((info.expiresAt - Date.now()) / 86_400_000);
|
|
106
|
+
const dateStr = new Date(info.expiresAt).toISOString().substring(0, 10);
|
|
107
|
+
if (daysLeft < 0) return `⚠️ EXPIRED (${dateStr}) — run /gemini-login`;
|
|
108
|
+
if (daysLeft <= 7) return `🚨 expires in ${daysLeft}d (${dateStr}) — run /gemini-login NOW`;
|
|
109
|
+
if (daysLeft <= 14) return `⚠️ expires in ${daysLeft}d (${dateStr}) — run /gemini-login soon`;
|
|
110
|
+
return `✅ valid for ${daysLeft} more days (expires ${dateStr})`;
|
|
111
|
+
}
|
|
112
|
+
async function scanGeminiCookieExpiry(ctx: BrowserContext): Promise<GeminiExpiryInfo | null> {
|
|
113
|
+
try {
|
|
114
|
+
const cookies = await ctx.cookies(["https://gemini.google.com", "https://accounts.google.com"]);
|
|
115
|
+
const auth = cookies.filter(c => ["__Secure-1PSID", "__Secure-3PSID", "SID"].includes(c.name) && c.expires && c.expires > 0);
|
|
116
|
+
if (!auth.length) return null;
|
|
117
|
+
auth.sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
|
|
118
|
+
const earliest = auth[0];
|
|
119
|
+
return { expiresAt: (earliest.expires ?? 0) * 1000, loginAt: Date.now(), cookieName: earliest.name };
|
|
120
|
+
} catch { return null; }
|
|
121
|
+
}
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
// ── ChatGPT web-session state ─────────────────────────────────────────────────
|
|
125
|
+
let chatgptContext: BrowserContext | null = null;
|
|
126
|
+
const CHATGPT_EXPIRY_FILE = join(homedir(), ".openclaw", "chatgpt-cookie-expiry.json");
|
|
127
|
+
interface ChatGPTExpiryInfo { expiresAt: number; loginAt: number; cookieName: string; }
|
|
128
|
+
function saveChatGPTExpiry(i: ChatGPTExpiryInfo) { try { writeFileSync(CHATGPT_EXPIRY_FILE, JSON.stringify(i, null, 2)); } catch { /* ignore */ } }
|
|
129
|
+
function loadChatGPTExpiry(): ChatGPTExpiryInfo | null { try { return JSON.parse(readFileSync(CHATGPT_EXPIRY_FILE, "utf-8")); } catch { return null; } }
|
|
130
|
+
function formatChatGPTExpiry(i: ChatGPTExpiryInfo): string {
|
|
131
|
+
const d = Math.floor((i.expiresAt - Date.now()) / 86_400_000);
|
|
132
|
+
const dt = new Date(i.expiresAt).toISOString().substring(0, 10);
|
|
133
|
+
if (d < 0) return `⚠️ EXPIRED (${dt}) — run /chatgpt-login`;
|
|
134
|
+
if (d <= 7) return `🚨 expires in ${d}d (${dt}) — run /chatgpt-login NOW`;
|
|
135
|
+
if (d <= 14) return `⚠️ expires in ${d}d (${dt}) — run /chatgpt-login soon`;
|
|
136
|
+
return `✅ valid for ${d} more days (expires ${dt})`;
|
|
137
|
+
}
|
|
138
|
+
async function scanChatGPTCookieExpiry(ctx: BrowserContext): Promise<ChatGPTExpiryInfo | null> {
|
|
139
|
+
try {
|
|
140
|
+
const cookies = await ctx.cookies(["https://chatgpt.com", "https://openai.com"]);
|
|
141
|
+
const auth = cookies.filter(c => ["__Secure-next-auth.session-token", "cf_clearance", "__cf_bm"].includes(c.name) && c.expires && c.expires > 0);
|
|
142
|
+
if (!auth.length) return null;
|
|
143
|
+
auth.sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
|
|
144
|
+
const earliest = auth[0];
|
|
145
|
+
return { expiresAt: (earliest.expires ?? 0) * 1000, loginAt: Date.now(), cookieName: earliest.name };
|
|
146
|
+
} catch { return null; }
|
|
147
|
+
}
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
92
150
|
// ── Claude web-session state ──────────────────────────────────────────────────
|
|
93
151
|
let claudeContext: BrowserContext | null = null;
|
|
94
152
|
const CLAUDE_EXPIRY_FILE = join(homedir(), ".openclaw", "claude-cookie-expiry.json");
|
|
@@ -172,60 +230,97 @@ async function scanCookieExpiry(ctx: import("playwright").BrowserContext): Promi
|
|
|
172
230
|
} catch { return null; }
|
|
173
231
|
}
|
|
174
232
|
|
|
233
|
+
// Singleton CDP connection — one browser object shared across grok + claude
|
|
234
|
+
let _cdpBrowser: import("playwright").Browser | null = null;
|
|
235
|
+
let _cdpBrowserLaunchPromise: Promise<import("playwright").BrowserContext | null> | null = null;
|
|
236
|
+
|
|
175
237
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
238
|
+
* Connect to the OpenClaw managed browser (CDP port 18800).
|
|
239
|
+
* Singleton: reuses the same connection. Falls back to persistent Chromium for Grok only.
|
|
240
|
+
* NEVER launches a new browser for Claude — Claude requires the OpenClaw browser.
|
|
179
241
|
*/
|
|
180
|
-
async function
|
|
242
|
+
async function connectToOpenClawBrowser(
|
|
181
243
|
log: (msg: string) => void
|
|
182
244
|
): Promise<BrowserContext | null> {
|
|
183
|
-
//
|
|
184
|
-
if (
|
|
245
|
+
// Reuse existing CDP connection if still alive
|
|
246
|
+
if (_cdpBrowser) {
|
|
185
247
|
try {
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
return grokContext;
|
|
248
|
+
_cdpBrowser.contexts(); // ping
|
|
249
|
+
return _cdpBrowser.contexts()[0] ?? null;
|
|
189
250
|
} catch {
|
|
190
|
-
|
|
251
|
+
_cdpBrowser = null;
|
|
191
252
|
}
|
|
192
253
|
}
|
|
193
|
-
|
|
194
254
|
const { chromium } = await import("playwright");
|
|
195
|
-
|
|
196
|
-
// 1. Try connecting to the OpenClaw managed browser first (user may have grok.com open)
|
|
197
255
|
try {
|
|
198
|
-
const browser = await chromium.connectOverCDP("http://127.0.0.1:18800", { timeout:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return ctx;
|
|
204
|
-
}
|
|
256
|
+
const browser = await chromium.connectOverCDP("http://127.0.0.1:18800", { timeout: 3000 });
|
|
257
|
+
_cdpBrowser = browser;
|
|
258
|
+
browser.on("disconnected", () => { _cdpBrowser = null; log("[cli-bridge] OpenClaw browser disconnected"); });
|
|
259
|
+
log("[cli-bridge] connected to OpenClaw browser via CDP");
|
|
260
|
+
return browser.contexts()[0] ?? null;
|
|
205
261
|
} catch {
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// 2. Launch our own persistent headless Chromium with saved profile
|
|
210
|
-
log("[cli-bridge:grok] launching persistent Chromium…");
|
|
211
|
-
try {
|
|
212
|
-
const ctx = await chromium.launchPersistentContext(GROK_PROFILE_DIR, {
|
|
213
|
-
headless: true,
|
|
214
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
215
|
-
});
|
|
216
|
-
grokContext = ctx;
|
|
217
|
-
log("[cli-bridge:grok] persistent context ready");
|
|
218
|
-
return ctx;
|
|
219
|
-
} catch (err) {
|
|
220
|
-
log(`[cli-bridge:grok] failed to launch browser: ${(err as Error).message}`);
|
|
262
|
+
log("[cli-bridge] OpenClaw browser not available (CDP 18800)");
|
|
221
263
|
return null;
|
|
222
264
|
}
|
|
223
265
|
}
|
|
224
266
|
|
|
225
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Launch (or reuse) a persistent headless Chromium context for grok.com.
|
|
269
|
+
* ONLY used for Grok — Grok has a saved persistent profile with cookies.
|
|
270
|
+
* Claude does NOT use this — it requires the OpenClaw browser.
|
|
271
|
+
*/
|
|
272
|
+
async function getOrLaunchGrokContext(
|
|
226
273
|
log: (msg: string) => void
|
|
227
274
|
): Promise<BrowserContext | null> {
|
|
228
|
-
|
|
275
|
+
// Already have a live context?
|
|
276
|
+
if (grokContext) {
|
|
277
|
+
try { grokContext.pages(); return grokContext; } catch { grokContext = null; }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Try OpenClaw browser first (singleton CDP)
|
|
281
|
+
const cdpCtx = await connectToOpenClawBrowser(log);
|
|
282
|
+
if (cdpCtx) return cdpCtx;
|
|
283
|
+
|
|
284
|
+
// Coalesce concurrent launch requests into one
|
|
285
|
+
if (_cdpBrowserLaunchPromise) return _cdpBrowserLaunchPromise;
|
|
286
|
+
|
|
287
|
+
_cdpBrowserLaunchPromise = (async () => {
|
|
288
|
+
const { chromium } = await import("playwright");
|
|
289
|
+
log("[cli-bridge:grok] launching persistent Chromium…");
|
|
290
|
+
try {
|
|
291
|
+
const ctx = await chromium.launchPersistentContext(GROK_PROFILE_DIR, {
|
|
292
|
+
headless: true,
|
|
293
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
294
|
+
});
|
|
295
|
+
grokContext = ctx;
|
|
296
|
+
// Auto-cleanup on browser crash
|
|
297
|
+
ctx.on("close", () => { grokContext = null; log("[cli-bridge:grok] persistent context closed"); });
|
|
298
|
+
log("[cli-bridge:grok] persistent context ready");
|
|
299
|
+
return ctx;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
log(`[cli-bridge:grok] failed to launch browser: ${(err as Error).message}`);
|
|
302
|
+
return null;
|
|
303
|
+
} finally {
|
|
304
|
+
_cdpBrowserLaunchPromise = null;
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
return _cdpBrowserLaunchPromise;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Clean up all browser resources — call on plugin teardown */
|
|
311
|
+
async function cleanupBrowsers(log: (msg: string) => void): Promise<void> {
|
|
312
|
+
if (grokContext) {
|
|
313
|
+
try { await grokContext.close(); } catch { /* ignore */ }
|
|
314
|
+
grokContext = null;
|
|
315
|
+
}
|
|
316
|
+
if (_cdpBrowser) {
|
|
317
|
+
try { await _cdpBrowser.close(); } catch { /* ignore */ }
|
|
318
|
+
_cdpBrowser = null;
|
|
319
|
+
}
|
|
320
|
+
claudeContext = null;
|
|
321
|
+
geminiContext = null;
|
|
322
|
+
chatgptContext = null;
|
|
323
|
+
log("[cli-bridge] browser resources cleaned up");
|
|
229
324
|
}
|
|
230
325
|
|
|
231
326
|
async function tryRestoreGrokSession(
|
|
@@ -583,7 +678,7 @@ function proxyTestRequest(
|
|
|
583
678
|
const plugin = {
|
|
584
679
|
id: "openclaw-cli-bridge-elvatis",
|
|
585
680
|
name: "OpenClaw CLI Bridge",
|
|
586
|
-
version: "0.
|
|
681
|
+
version: "1.0.0",
|
|
587
682
|
description:
|
|
588
683
|
"Phase 1: openai-codex auth bridge. " +
|
|
589
684
|
"Phase 2: HTTP proxy for gemini/claude CLIs. " +
|
|
@@ -717,6 +812,28 @@ const plugin = {
|
|
|
717
812
|
}
|
|
718
813
|
return null;
|
|
719
814
|
},
|
|
815
|
+
getGeminiContext: () => geminiContext,
|
|
816
|
+
connectGeminiContext: async () => {
|
|
817
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
818
|
+
if (ctx) {
|
|
819
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
820
|
+
const { page } = await getOrCreateGeminiPage(ctx);
|
|
821
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
822
|
+
if (editor) { geminiContext = ctx; return ctx; }
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
},
|
|
826
|
+
getChatGPTContext: () => chatgptContext,
|
|
827
|
+
connectChatGPTContext: async () => {
|
|
828
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
829
|
+
if (ctx) {
|
|
830
|
+
const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
|
|
831
|
+
const { page } = await getOrCreateChatGPTPage(ctx);
|
|
832
|
+
const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
|
|
833
|
+
if (editor) { chatgptContext = ctx; return ctx; }
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
},
|
|
720
837
|
});
|
|
721
838
|
proxyServer = server;
|
|
722
839
|
api.logger.info(
|
|
@@ -760,6 +877,28 @@ const plugin = {
|
|
|
760
877
|
}
|
|
761
878
|
return null;
|
|
762
879
|
},
|
|
880
|
+
getGeminiContext: () => geminiContext,
|
|
881
|
+
connectGeminiContext: async () => {
|
|
882
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
883
|
+
if (ctx) {
|
|
884
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
885
|
+
const { page } = await getOrCreateGeminiPage(ctx);
|
|
886
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
887
|
+
if (editor) { geminiContext = ctx; return ctx; }
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
},
|
|
891
|
+
getChatGPTContext: () => chatgptContext,
|
|
892
|
+
connectChatGPTContext: async () => {
|
|
893
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
894
|
+
if (ctx) {
|
|
895
|
+
const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
|
|
896
|
+
const { page } = await getOrCreateChatGPTPage(ctx);
|
|
897
|
+
const editor = await page.locator("#prompt-textarea").isVisible().catch(() => false);
|
|
898
|
+
if (editor) { chatgptContext = ctx; return ctx; }
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
},
|
|
763
902
|
});
|
|
764
903
|
proxyServer = server;
|
|
765
904
|
api.logger.info(`[cli-bridge] proxy ready on :${port} (retry)`);
|
|
@@ -781,6 +920,9 @@ const plugin = {
|
|
|
781
920
|
id: "cli-bridge-proxy",
|
|
782
921
|
start: async () => { /* proxy already started above */ },
|
|
783
922
|
stop: async () => {
|
|
923
|
+
// Clean up browser resources first
|
|
924
|
+
await cleanupBrowsers((msg) => api.logger.info(msg));
|
|
925
|
+
|
|
784
926
|
if (proxyServer) {
|
|
785
927
|
// closeAllConnections() forcefully terminates keep-alive connections
|
|
786
928
|
// so that server.close() releases the port immediately rather than
|
|
@@ -1113,23 +1255,8 @@ const plugin = {
|
|
|
1113
1255
|
}
|
|
1114
1256
|
|
|
1115
1257
|
api.logger.info("[cli-bridge:claude] /claude-login: connecting to OpenClaw browser…");
|
|
1116
|
-
const { chromium } = await import("playwright");
|
|
1117
1258
|
|
|
1118
|
-
//
|
|
1119
|
-
let importedCookies: unknown[] = [];
|
|
1120
|
-
try {
|
|
1121
|
-
const ocBrowser = await chromium.connectOverCDP("http://127.0.0.1:18800", { timeout: 3000 });
|
|
1122
|
-
const ocCtx = ocBrowser.contexts()[0];
|
|
1123
|
-
if (ocCtx) {
|
|
1124
|
-
importedCookies = await ocCtx.cookies(["https://claude.ai", "https://anthropic.com"]);
|
|
1125
|
-
api.logger.info(`[cli-bridge:claude] imported ${importedCookies.length} cookies`);
|
|
1126
|
-
}
|
|
1127
|
-
await ocBrowser.close().catch(() => {});
|
|
1128
|
-
} catch {
|
|
1129
|
-
api.logger.info("[cli-bridge:claude] OpenClaw browser not available");
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Connect to OpenClaw browser context for session
|
|
1259
|
+
// Connect to OpenClaw browser context for session (singleton CDP)
|
|
1133
1260
|
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
1134
1261
|
if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure claude.ai is open in your browser." };
|
|
1135
1262
|
|
|
@@ -1192,6 +1319,144 @@ const plugin = {
|
|
|
1192
1319
|
return { text: "✅ Disconnected from claude.ai. Run `/claude-login` to reconnect." };
|
|
1193
1320
|
},
|
|
1194
1321
|
} satisfies OpenClawPluginCommandDefinition);
|
|
1322
|
+
|
|
1323
|
+
// ── Gemini web-session commands ───────────────────────────────────────────
|
|
1324
|
+
api.registerCommand({
|
|
1325
|
+
name: "gemini-login",
|
|
1326
|
+
description: "Authenticate gemini.google.com: imports session from OpenClaw browser",
|
|
1327
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1328
|
+
if (geminiContext) {
|
|
1329
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
1330
|
+
try {
|
|
1331
|
+
const { page } = await getOrCreateGeminiPage(geminiContext);
|
|
1332
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
1333
|
+
if (editor) return { text: "✅ Already connected to gemini.google.com. Use `/gemini-logout` first to reset." };
|
|
1334
|
+
} catch { /* fall through */ }
|
|
1335
|
+
geminiContext = null;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
api.logger.info("[cli-bridge:gemini] /gemini-login: connecting to OpenClaw browser…");
|
|
1339
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
1340
|
+
if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure gemini.google.com is open in your browser." };
|
|
1341
|
+
|
|
1342
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
1343
|
+
let page;
|
|
1344
|
+
try {
|
|
1345
|
+
({ page } = await getOrCreateGeminiPage(ctx));
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
return { text: `❌ Failed to open gemini.google.com: ${(err as Error).message}` };
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
1351
|
+
if (!editor) {
|
|
1352
|
+
return { text: "❌ Gemini editor not visible — are you logged in?\nOpen gemini.google.com in your browser and try again." };
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
geminiContext = ctx;
|
|
1356
|
+
|
|
1357
|
+
const expiry = await scanGeminiCookieExpiry(ctx);
|
|
1358
|
+
if (expiry) {
|
|
1359
|
+
saveGeminiExpiry(expiry);
|
|
1360
|
+
api.logger.info(`[cli-bridge:gemini] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`);
|
|
1361
|
+
}
|
|
1362
|
+
const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatGeminiExpiry(expiry)}` : "";
|
|
1363
|
+
|
|
1364
|
+
return { text: `✅ Gemini session ready!\n\nModels available:\n• \`vllm/web-gemini/gemini-2-5-pro\`\n• \`vllm/web-gemini/gemini-2-5-flash\`\n• \`vllm/web-gemini/gemini-3-pro\`\n• \`vllm/web-gemini/gemini-3-flash\`${expiryLine}` };
|
|
1365
|
+
},
|
|
1366
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1367
|
+
|
|
1368
|
+
api.registerCommand({
|
|
1369
|
+
name: "gemini-status",
|
|
1370
|
+
description: "Check gemini.google.com session status",
|
|
1371
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1372
|
+
if (!geminiContext) {
|
|
1373
|
+
return { text: "❌ No active gemini.google.com session\nRun `/gemini-login` to authenticate." };
|
|
1374
|
+
}
|
|
1375
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
1376
|
+
try {
|
|
1377
|
+
const { page } = await getOrCreateGeminiPage(geminiContext);
|
|
1378
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
1379
|
+
if (editor) {
|
|
1380
|
+
const expiry = loadGeminiExpiry();
|
|
1381
|
+
const expiryLine = expiry ? `\n🕐 ${formatGeminiExpiry(expiry)}` : "";
|
|
1382
|
+
return { text: `✅ gemini.google.com session active\nProxy: \`127.0.0.1:${port}\`\nModels: web-gemini/gemini-2-5-pro, gemini-2-5-flash, gemini-3-pro, gemini-3-flash${expiryLine}` };
|
|
1383
|
+
}
|
|
1384
|
+
} catch { /* fall through */ }
|
|
1385
|
+
geminiContext = null;
|
|
1386
|
+
return { text: "❌ Session lost — run `/gemini-login` to re-authenticate." };
|
|
1387
|
+
},
|
|
1388
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1389
|
+
|
|
1390
|
+
api.registerCommand({
|
|
1391
|
+
name: "gemini-logout",
|
|
1392
|
+
description: "Disconnect from gemini.google.com session",
|
|
1393
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1394
|
+
geminiContext = null;
|
|
1395
|
+
return { text: "✅ Disconnected from gemini.google.com. Run `/gemini-login` to reconnect." };
|
|
1396
|
+
},
|
|
1397
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1398
|
+
|
|
1399
|
+
// ── ChatGPT web-session commands ──────────────────────────────────────────
|
|
1400
|
+
api.registerCommand({
|
|
1401
|
+
name: "chatgpt-login",
|
|
1402
|
+
description: "Authenticate chatgpt.com: imports session from OpenClaw browser",
|
|
1403
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1404
|
+
if (chatgptContext) {
|
|
1405
|
+
const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
|
|
1406
|
+
try {
|
|
1407
|
+
const { page } = await getOrCreateChatGPTPage(chatgptContext);
|
|
1408
|
+
if (await page.locator("#prompt-textarea").isVisible().catch(() => false))
|
|
1409
|
+
return { text: "✅ Already connected to chatgpt.com. Use `/chatgpt-logout` first to reset." };
|
|
1410
|
+
} catch { /* fall through */ }
|
|
1411
|
+
chatgptContext = null;
|
|
1412
|
+
}
|
|
1413
|
+
api.logger.info("[cli-bridge:chatgpt] /chatgpt-login: connecting to OpenClaw browser…");
|
|
1414
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
1415
|
+
if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure chatgpt.com is open in your browser." };
|
|
1416
|
+
|
|
1417
|
+
const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
|
|
1418
|
+
let page;
|
|
1419
|
+
try { ({ page } = await getOrCreateChatGPTPage(ctx)); }
|
|
1420
|
+
catch (err) { return { text: `❌ Failed to open chatgpt.com: ${(err as Error).message}` }; }
|
|
1421
|
+
|
|
1422
|
+
if (!await page.locator("#prompt-textarea").isVisible().catch(() => false))
|
|
1423
|
+
return { text: "❌ ChatGPT editor not visible — are you logged in?\nOpen chatgpt.com in your browser and try again." };
|
|
1424
|
+
|
|
1425
|
+
chatgptContext = ctx;
|
|
1426
|
+
const expiry = await scanChatGPTCookieExpiry(ctx);
|
|
1427
|
+
if (expiry) { saveChatGPTExpiry(expiry); api.logger.info(`[cli-bridge:chatgpt] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`); }
|
|
1428
|
+
const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatChatGPTExpiry(expiry)}` : "";
|
|
1429
|
+
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}` };
|
|
1430
|
+
},
|
|
1431
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1432
|
+
|
|
1433
|
+
api.registerCommand({
|
|
1434
|
+
name: "chatgpt-status",
|
|
1435
|
+
description: "Check chatgpt.com session status",
|
|
1436
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1437
|
+
if (!chatgptContext) return { text: "❌ No active chatgpt.com session\nRun `/chatgpt-login` to authenticate." };
|
|
1438
|
+
const { getOrCreateChatGPTPage } = await import("./src/chatgpt-browser.js");
|
|
1439
|
+
try {
|
|
1440
|
+
const { page } = await getOrCreateChatGPTPage(chatgptContext);
|
|
1441
|
+
if (await page.locator("#prompt-textarea").isVisible().catch(() => false)) {
|
|
1442
|
+
const expiry = loadChatGPTExpiry();
|
|
1443
|
+
const expiryLine = expiry ? `\n🕐 ${formatChatGPTExpiry(expiry)}` : "";
|
|
1444
|
+
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}` };
|
|
1445
|
+
}
|
|
1446
|
+
} catch { /* fall through */ }
|
|
1447
|
+
chatgptContext = null;
|
|
1448
|
+
return { text: "❌ Session lost — run `/chatgpt-login` to re-authenticate." };
|
|
1449
|
+
},
|
|
1450
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1451
|
+
|
|
1452
|
+
api.registerCommand({
|
|
1453
|
+
name: "chatgpt-logout",
|
|
1454
|
+
description: "Disconnect from chatgpt.com session",
|
|
1455
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1456
|
+
chatgptContext = null;
|
|
1457
|
+
return { text: "✅ Disconnected from chatgpt.com. Run `/chatgpt-login` to reconnect." };
|
|
1458
|
+
},
|
|
1459
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1195
1460
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1196
1461
|
|
|
1197
1462
|
const allCommands = [
|
|
@@ -1205,6 +1470,12 @@ const plugin = {
|
|
|
1205
1470
|
"/claude-login",
|
|
1206
1471
|
"/claude-status",
|
|
1207
1472
|
"/claude-logout",
|
|
1473
|
+
"/gemini-login",
|
|
1474
|
+
"/gemini-status",
|
|
1475
|
+
"/gemini-logout",
|
|
1476
|
+
"/chatgpt-login",
|
|
1477
|
+
"/chatgpt-status",
|
|
1478
|
+
"/chatgpt-logout",
|
|
1208
1479
|
];
|
|
1209
1480
|
api.logger.info(`[cli-bridge] registered ${allCommands.length} commands: ${allCommands.join(", ")}`);
|
|
1210
1481
|
},
|
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": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
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": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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": {
|