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