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