@elvatis_com/openclaw-cli-bridge-elvatis 0.2.28 → 0.2.30
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/HEADLESS_ROADMAP.md +81 -0
- package/.ai/handoff/STATUS.md +54 -78
- package/README.md +16 -1
- package/SKILL.md +1 -1
- package/index.ts +353 -39
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/claude-browser.ts +233 -0
- package/src/gemini-browser.ts +242 -0
- package/src/proxy-server.ts +125 -0
- package/test/claude-browser.test.ts +93 -0
- package/test/claude-proxy.test.ts +235 -0
- package/test/cli-runner.test.ts +27 -11
- package/test/gemini-proxy.test.ts +139 -0
package/index.ts
CHANGED
|
@@ -89,6 +89,74 @@ 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
|
+
// ── Claude web-session state ──────────────────────────────────────────────────
|
|
125
|
+
let claudeContext: BrowserContext | null = null;
|
|
126
|
+
const CLAUDE_EXPIRY_FILE = join(homedir(), ".openclaw", "claude-cookie-expiry.json");
|
|
127
|
+
|
|
128
|
+
interface ClaudeExpiryInfo {
|
|
129
|
+
expiresAt: number;
|
|
130
|
+
loginAt: number;
|
|
131
|
+
cookieName: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function saveClaudeExpiry(info: ClaudeExpiryInfo): void {
|
|
135
|
+
try { writeFileSync(CLAUDE_EXPIRY_FILE, JSON.stringify(info, null, 2)); } catch { /* ignore */ }
|
|
136
|
+
}
|
|
137
|
+
function loadClaudeExpiry(): ClaudeExpiryInfo | null {
|
|
138
|
+
try { return JSON.parse(readFileSync(CLAUDE_EXPIRY_FILE, "utf-8")) as ClaudeExpiryInfo; } catch { return null; }
|
|
139
|
+
}
|
|
140
|
+
function formatClaudeExpiry(info: ClaudeExpiryInfo): string {
|
|
141
|
+
const daysLeft = Math.floor((info.expiresAt - Date.now()) / 86_400_000);
|
|
142
|
+
const dateStr = new Date(info.expiresAt).toISOString().substring(0, 10);
|
|
143
|
+
if (daysLeft < 0) return `⚠️ EXPIRED (${dateStr}) — run /claude-login`;
|
|
144
|
+
if (daysLeft <= 7) return `🚨 expires in ${daysLeft}d (${dateStr}) — run /claude-login NOW`;
|
|
145
|
+
if (daysLeft <= 14) return `⚠️ expires in ${daysLeft}d (${dateStr}) — run /claude-login soon`;
|
|
146
|
+
return `✅ valid for ${daysLeft} more days (expires ${dateStr})`;
|
|
147
|
+
}
|
|
148
|
+
async function scanClaudeCookieExpiry(ctx: BrowserContext): Promise<ClaudeExpiryInfo | null> {
|
|
149
|
+
try {
|
|
150
|
+
const cookies = await ctx.cookies(["https://claude.ai", "https://anthropic.com"]);
|
|
151
|
+
const authCookies = cookies.filter(c => ["sessionKey", "intercom-session-igviqkfk"].includes(c.name) && c.expires && c.expires > 0);
|
|
152
|
+
if (!authCookies.length) return null;
|
|
153
|
+
authCookies.sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
|
|
154
|
+
const earliest = authCookies[0];
|
|
155
|
+
return { expiresAt: (earliest.expires ?? 0) * 1000, loginAt: Date.now(), cookieName: earliest.name };
|
|
156
|
+
} catch { return null; }
|
|
157
|
+
}
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
92
160
|
// Cookie expiry tracking file — written on /grok-login, read on startup
|
|
93
161
|
const GROK_EXPIRY_FILE = join(homedir(), ".openclaw", "grok-cookie-expiry.json");
|
|
94
162
|
|
|
@@ -136,60 +204,96 @@ async function scanCookieExpiry(ctx: import("playwright").BrowserContext): Promi
|
|
|
136
204
|
} catch { return null; }
|
|
137
205
|
}
|
|
138
206
|
|
|
207
|
+
// Singleton CDP connection — one browser object shared across grok + claude
|
|
208
|
+
let _cdpBrowser: import("playwright").Browser | null = null;
|
|
209
|
+
let _cdpBrowserLaunchPromise: Promise<import("playwright").BrowserContext | null> | null = null;
|
|
210
|
+
|
|
139
211
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
212
|
+
* Connect to the OpenClaw managed browser (CDP port 18800).
|
|
213
|
+
* Singleton: reuses the same connection. Falls back to persistent Chromium for Grok only.
|
|
214
|
+
* NEVER launches a new browser for Claude — Claude requires the OpenClaw browser.
|
|
143
215
|
*/
|
|
144
|
-
async function
|
|
216
|
+
async function connectToOpenClawBrowser(
|
|
145
217
|
log: (msg: string) => void
|
|
146
218
|
): Promise<BrowserContext | null> {
|
|
147
|
-
//
|
|
148
|
-
if (
|
|
219
|
+
// Reuse existing CDP connection if still alive
|
|
220
|
+
if (_cdpBrowser) {
|
|
149
221
|
try {
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
return grokContext;
|
|
222
|
+
_cdpBrowser.contexts(); // ping
|
|
223
|
+
return _cdpBrowser.contexts()[0] ?? null;
|
|
153
224
|
} catch {
|
|
154
|
-
|
|
225
|
+
_cdpBrowser = null;
|
|
155
226
|
}
|
|
156
227
|
}
|
|
157
|
-
|
|
158
228
|
const { chromium } = await import("playwright");
|
|
159
|
-
|
|
160
|
-
// 1. Try connecting to the OpenClaw managed browser first (user may have grok.com open)
|
|
161
229
|
try {
|
|
162
|
-
const browser = await chromium.connectOverCDP("http://127.0.0.1:18800", { timeout:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return ctx;
|
|
168
|
-
}
|
|
230
|
+
const browser = await chromium.connectOverCDP("http://127.0.0.1:18800", { timeout: 3000 });
|
|
231
|
+
_cdpBrowser = browser;
|
|
232
|
+
browser.on("disconnected", () => { _cdpBrowser = null; log("[cli-bridge] OpenClaw browser disconnected"); });
|
|
233
|
+
log("[cli-bridge] connected to OpenClaw browser via CDP");
|
|
234
|
+
return browser.contexts()[0] ?? null;
|
|
169
235
|
} catch {
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 2. Launch our own persistent headless Chromium with saved profile
|
|
174
|
-
log("[cli-bridge:grok] launching persistent Chromium…");
|
|
175
|
-
try {
|
|
176
|
-
const ctx = await chromium.launchPersistentContext(GROK_PROFILE_DIR, {
|
|
177
|
-
headless: true,
|
|
178
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
179
|
-
});
|
|
180
|
-
grokContext = ctx;
|
|
181
|
-
log("[cli-bridge:grok] persistent context ready");
|
|
182
|
-
return ctx;
|
|
183
|
-
} catch (err) {
|
|
184
|
-
log(`[cli-bridge:grok] failed to launch browser: ${(err as Error).message}`);
|
|
236
|
+
log("[cli-bridge] OpenClaw browser not available (CDP 18800)");
|
|
185
237
|
return null;
|
|
186
238
|
}
|
|
187
239
|
}
|
|
188
240
|
|
|
189
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Launch (or reuse) a persistent headless Chromium context for grok.com.
|
|
243
|
+
* ONLY used for Grok — Grok has a saved persistent profile with cookies.
|
|
244
|
+
* Claude does NOT use this — it requires the OpenClaw browser.
|
|
245
|
+
*/
|
|
246
|
+
async function getOrLaunchGrokContext(
|
|
190
247
|
log: (msg: string) => void
|
|
191
248
|
): Promise<BrowserContext | null> {
|
|
192
|
-
|
|
249
|
+
// Already have a live context?
|
|
250
|
+
if (grokContext) {
|
|
251
|
+
try { grokContext.pages(); return grokContext; } catch { grokContext = null; }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Try OpenClaw browser first (singleton CDP)
|
|
255
|
+
const cdpCtx = await connectToOpenClawBrowser(log);
|
|
256
|
+
if (cdpCtx) return cdpCtx;
|
|
257
|
+
|
|
258
|
+
// Coalesce concurrent launch requests into one
|
|
259
|
+
if (_cdpBrowserLaunchPromise) return _cdpBrowserLaunchPromise;
|
|
260
|
+
|
|
261
|
+
_cdpBrowserLaunchPromise = (async () => {
|
|
262
|
+
const { chromium } = await import("playwright");
|
|
263
|
+
log("[cli-bridge:grok] launching persistent Chromium…");
|
|
264
|
+
try {
|
|
265
|
+
const ctx = await chromium.launchPersistentContext(GROK_PROFILE_DIR, {
|
|
266
|
+
headless: true,
|
|
267
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
268
|
+
});
|
|
269
|
+
grokContext = ctx;
|
|
270
|
+
// Auto-cleanup on browser crash
|
|
271
|
+
ctx.on("close", () => { grokContext = null; log("[cli-bridge:grok] persistent context closed"); });
|
|
272
|
+
log("[cli-bridge:grok] persistent context ready");
|
|
273
|
+
return ctx;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
log(`[cli-bridge:grok] failed to launch browser: ${(err as Error).message}`);
|
|
276
|
+
return null;
|
|
277
|
+
} finally {
|
|
278
|
+
_cdpBrowserLaunchPromise = null;
|
|
279
|
+
}
|
|
280
|
+
})();
|
|
281
|
+
return _cdpBrowserLaunchPromise;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Clean up all browser resources — call on plugin teardown */
|
|
285
|
+
async function cleanupBrowsers(log: (msg: string) => void): Promise<void> {
|
|
286
|
+
if (grokContext) {
|
|
287
|
+
try { await grokContext.close(); } catch { /* ignore */ }
|
|
288
|
+
grokContext = null;
|
|
289
|
+
}
|
|
290
|
+
if (_cdpBrowser) {
|
|
291
|
+
try { await _cdpBrowser.close(); } catch { /* ignore */ }
|
|
292
|
+
_cdpBrowser = null;
|
|
293
|
+
}
|
|
294
|
+
claudeContext = null;
|
|
295
|
+
geminiContext = null;
|
|
296
|
+
log("[cli-bridge] browser resources cleaned up");
|
|
193
297
|
}
|
|
194
298
|
|
|
195
299
|
async function tryRestoreGrokSession(
|
|
@@ -547,7 +651,7 @@ function proxyTestRequest(
|
|
|
547
651
|
const plugin = {
|
|
548
652
|
id: "openclaw-cli-bridge-elvatis",
|
|
549
653
|
name: "OpenClaw CLI Bridge",
|
|
550
|
-
version: "0.2.
|
|
654
|
+
version: "0.2.30",
|
|
551
655
|
description:
|
|
552
656
|
"Phase 1: openai-codex auth bridge. " +
|
|
553
657
|
"Phase 2: HTTP proxy for gemini/claude CLIs. " +
|
|
@@ -670,6 +774,28 @@ const plugin = {
|
|
|
670
774
|
}
|
|
671
775
|
return null;
|
|
672
776
|
},
|
|
777
|
+
getClaudeContext: () => claudeContext,
|
|
778
|
+
connectClaudeContext: async () => {
|
|
779
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
780
|
+
if (ctx) {
|
|
781
|
+
const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
|
|
782
|
+
const { page } = await getOrCreateClaudePage(ctx);
|
|
783
|
+
const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
|
|
784
|
+
if (editor) { claudeContext = ctx; return ctx; }
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
},
|
|
788
|
+
getGeminiContext: () => geminiContext,
|
|
789
|
+
connectGeminiContext: async () => {
|
|
790
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
791
|
+
if (ctx) {
|
|
792
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
793
|
+
const { page } = await getOrCreateGeminiPage(ctx);
|
|
794
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
795
|
+
if (editor) { geminiContext = ctx; return ctx; }
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
},
|
|
673
799
|
});
|
|
674
800
|
proxyServer = server;
|
|
675
801
|
api.logger.info(
|
|
@@ -702,6 +828,28 @@ const plugin = {
|
|
|
702
828
|
}
|
|
703
829
|
return null;
|
|
704
830
|
},
|
|
831
|
+
getClaudeContext: () => claudeContext,
|
|
832
|
+
connectClaudeContext: async () => {
|
|
833
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
834
|
+
if (ctx) {
|
|
835
|
+
const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
|
|
836
|
+
const { page } = await getOrCreateClaudePage(ctx);
|
|
837
|
+
const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
|
|
838
|
+
if (editor) { claudeContext = ctx; return ctx; }
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
},
|
|
842
|
+
getGeminiContext: () => geminiContext,
|
|
843
|
+
connectGeminiContext: async () => {
|
|
844
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
845
|
+
if (ctx) {
|
|
846
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
847
|
+
const { page } = await getOrCreateGeminiPage(ctx);
|
|
848
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
849
|
+
if (editor) { geminiContext = ctx; return ctx; }
|
|
850
|
+
}
|
|
851
|
+
return null;
|
|
852
|
+
},
|
|
705
853
|
});
|
|
706
854
|
proxyServer = server;
|
|
707
855
|
api.logger.info(`[cli-bridge] proxy ready on :${port} (retry)`);
|
|
@@ -723,6 +871,9 @@ const plugin = {
|
|
|
723
871
|
id: "cli-bridge-proxy",
|
|
724
872
|
start: async () => { /* proxy already started above */ },
|
|
725
873
|
stop: async () => {
|
|
874
|
+
// Clean up browser resources first
|
|
875
|
+
await cleanupBrowsers((msg) => api.logger.info(msg));
|
|
876
|
+
|
|
726
877
|
if (proxyServer) {
|
|
727
878
|
// closeAllConnections() forcefully terminates keep-alive connections
|
|
728
879
|
// so that server.close() releases the port immediately rather than
|
|
@@ -1033,13 +1184,170 @@ const plugin = {
|
|
|
1033
1184
|
name: "grok-logout",
|
|
1034
1185
|
description: "Disconnect from grok.com session (does not close the browser)",
|
|
1035
1186
|
handler: async (): Promise<PluginCommandResult> => {
|
|
1036
|
-
// Don't close the context — it belongs to the OpenClaw browser, not us
|
|
1037
1187
|
grokContext = null;
|
|
1038
1188
|
deleteSession(grokSessionPath);
|
|
1039
1189
|
return { text: "✅ Disconnected from grok.com. Run `/grok-login` to reconnect." };
|
|
1040
1190
|
},
|
|
1041
1191
|
} satisfies OpenClawPluginCommandDefinition);
|
|
1042
1192
|
|
|
1193
|
+
// ── Claude web-session commands ───────────────────────────────────────────
|
|
1194
|
+
api.registerCommand({
|
|
1195
|
+
name: "claude-login",
|
|
1196
|
+
description: "Authenticate claude.ai: imports session from OpenClaw browser",
|
|
1197
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1198
|
+
if (claudeContext) {
|
|
1199
|
+
const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
|
|
1200
|
+
try {
|
|
1201
|
+
const { page } = await getOrCreateClaudePage(claudeContext);
|
|
1202
|
+
const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
|
|
1203
|
+
if (editor) return { text: "✅ Already connected to claude.ai. Use `/claude-logout` first to reset." };
|
|
1204
|
+
} catch { /* fall through */ }
|
|
1205
|
+
claudeContext = null;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
api.logger.info("[cli-bridge:claude] /claude-login: connecting to OpenClaw browser…");
|
|
1209
|
+
|
|
1210
|
+
// Connect to OpenClaw browser context for session (singleton CDP)
|
|
1211
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
1212
|
+
if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure claude.ai is open in your browser." };
|
|
1213
|
+
|
|
1214
|
+
// Navigate to claude.ai/new if not already there
|
|
1215
|
+
const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
|
|
1216
|
+
let page;
|
|
1217
|
+
try {
|
|
1218
|
+
({ page } = await getOrCreateClaudePage(ctx));
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
return { text: `❌ Failed to open claude.ai: ${(err as Error).message}` };
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Verify editor is visible
|
|
1224
|
+
const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
|
|
1225
|
+
if (!editor) {
|
|
1226
|
+
return { text: "❌ claude.ai editor not visible — are you logged in?\nOpen claude.ai in your browser and try again." };
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
claudeContext = ctx;
|
|
1230
|
+
|
|
1231
|
+
// Scan cookie expiry
|
|
1232
|
+
const expiry = await scanClaudeCookieExpiry(ctx);
|
|
1233
|
+
if (expiry) {
|
|
1234
|
+
saveClaudeExpiry(expiry);
|
|
1235
|
+
api.logger.info(`[cli-bridge:claude] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`);
|
|
1236
|
+
}
|
|
1237
|
+
const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatClaudeExpiry(expiry)}` : "";
|
|
1238
|
+
|
|
1239
|
+
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}` };
|
|
1240
|
+
},
|
|
1241
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1242
|
+
|
|
1243
|
+
api.registerCommand({
|
|
1244
|
+
name: "claude-status",
|
|
1245
|
+
description: "Check claude.ai session status",
|
|
1246
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1247
|
+
if (!claudeContext) {
|
|
1248
|
+
return { text: "❌ No active claude.ai session\nRun `/claude-login` to authenticate." };
|
|
1249
|
+
}
|
|
1250
|
+
const { getOrCreateClaudePage } = await import("./src/claude-browser.js");
|
|
1251
|
+
try {
|
|
1252
|
+
const { page } = await getOrCreateClaudePage(claudeContext);
|
|
1253
|
+
const editor = await page.locator(".ProseMirror").isVisible().catch(() => false);
|
|
1254
|
+
if (editor) {
|
|
1255
|
+
const expiry = loadClaudeExpiry();
|
|
1256
|
+
const expiryLine = expiry ? `\n🕐 ${formatClaudeExpiry(expiry)}` : "";
|
|
1257
|
+
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}` };
|
|
1258
|
+
}
|
|
1259
|
+
} catch { /* fall through */ }
|
|
1260
|
+
claudeContext = null;
|
|
1261
|
+
return { text: "❌ Session lost — run `/claude-login` to re-authenticate." };
|
|
1262
|
+
},
|
|
1263
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1264
|
+
|
|
1265
|
+
api.registerCommand({
|
|
1266
|
+
name: "claude-logout",
|
|
1267
|
+
description: "Disconnect from claude.ai session",
|
|
1268
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1269
|
+
claudeContext = null;
|
|
1270
|
+
return { text: "✅ Disconnected from claude.ai. Run `/claude-login` to reconnect." };
|
|
1271
|
+
},
|
|
1272
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1273
|
+
|
|
1274
|
+
// ── Gemini web-session commands ───────────────────────────────────────────
|
|
1275
|
+
api.registerCommand({
|
|
1276
|
+
name: "gemini-login",
|
|
1277
|
+
description: "Authenticate gemini.google.com: imports session from OpenClaw browser",
|
|
1278
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1279
|
+
if (geminiContext) {
|
|
1280
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
1281
|
+
try {
|
|
1282
|
+
const { page } = await getOrCreateGeminiPage(geminiContext);
|
|
1283
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
1284
|
+
if (editor) return { text: "✅ Already connected to gemini.google.com. Use `/gemini-logout` first to reset." };
|
|
1285
|
+
} catch { /* fall through */ }
|
|
1286
|
+
geminiContext = null;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
api.logger.info("[cli-bridge:gemini] /gemini-login: connecting to OpenClaw browser…");
|
|
1290
|
+
const ctx = await connectToOpenClawBrowser((msg) => api.logger.info(msg));
|
|
1291
|
+
if (!ctx) return { text: "❌ Could not connect to OpenClaw browser.\nMake sure gemini.google.com is open in your browser." };
|
|
1292
|
+
|
|
1293
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
1294
|
+
let page;
|
|
1295
|
+
try {
|
|
1296
|
+
({ page } = await getOrCreateGeminiPage(ctx));
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
return { text: `❌ Failed to open gemini.google.com: ${(err as Error).message}` };
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
1302
|
+
if (!editor) {
|
|
1303
|
+
return { text: "❌ Gemini editor not visible — are you logged in?\nOpen gemini.google.com in your browser and try again." };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
geminiContext = ctx;
|
|
1307
|
+
|
|
1308
|
+
const expiry = await scanGeminiCookieExpiry(ctx);
|
|
1309
|
+
if (expiry) {
|
|
1310
|
+
saveGeminiExpiry(expiry);
|
|
1311
|
+
api.logger.info(`[cli-bridge:gemini] cookie expiry: ${new Date(expiry.expiresAt).toISOString()}`);
|
|
1312
|
+
}
|
|
1313
|
+
const expiryLine = expiry ? `\n\n🕐 Cookie expiry: ${formatGeminiExpiry(expiry)}` : "";
|
|
1314
|
+
|
|
1315
|
+
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}` };
|
|
1316
|
+
},
|
|
1317
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1318
|
+
|
|
1319
|
+
api.registerCommand({
|
|
1320
|
+
name: "gemini-status",
|
|
1321
|
+
description: "Check gemini.google.com session status",
|
|
1322
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1323
|
+
if (!geminiContext) {
|
|
1324
|
+
return { text: "❌ No active gemini.google.com session\nRun `/gemini-login` to authenticate." };
|
|
1325
|
+
}
|
|
1326
|
+
const { getOrCreateGeminiPage } = await import("./src/gemini-browser.js");
|
|
1327
|
+
try {
|
|
1328
|
+
const { page } = await getOrCreateGeminiPage(geminiContext);
|
|
1329
|
+
const editor = await page.locator(".ql-editor").isVisible().catch(() => false);
|
|
1330
|
+
if (editor) {
|
|
1331
|
+
const expiry = loadGeminiExpiry();
|
|
1332
|
+
const expiryLine = expiry ? `\n🕐 ${formatGeminiExpiry(expiry)}` : "";
|
|
1333
|
+
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}` };
|
|
1334
|
+
}
|
|
1335
|
+
} catch { /* fall through */ }
|
|
1336
|
+
geminiContext = null;
|
|
1337
|
+
return { text: "❌ Session lost — run `/gemini-login` to re-authenticate." };
|
|
1338
|
+
},
|
|
1339
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1340
|
+
|
|
1341
|
+
api.registerCommand({
|
|
1342
|
+
name: "gemini-logout",
|
|
1343
|
+
description: "Disconnect from gemini.google.com session",
|
|
1344
|
+
handler: async (): Promise<PluginCommandResult> => {
|
|
1345
|
+
geminiContext = null;
|
|
1346
|
+
return { text: "✅ Disconnected from gemini.google.com. Run `/gemini-login` to reconnect." };
|
|
1347
|
+
},
|
|
1348
|
+
} satisfies OpenClawPluginCommandDefinition);
|
|
1349
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1350
|
+
|
|
1043
1351
|
const allCommands = [
|
|
1044
1352
|
...CLI_MODEL_COMMANDS.map((c) => `/${c.name}`),
|
|
1045
1353
|
"/cli-back",
|
|
@@ -1048,6 +1356,12 @@ const plugin = {
|
|
|
1048
1356
|
"/grok-login",
|
|
1049
1357
|
"/grok-status",
|
|
1050
1358
|
"/grok-logout",
|
|
1359
|
+
"/claude-login",
|
|
1360
|
+
"/claude-status",
|
|
1361
|
+
"/claude-logout",
|
|
1362
|
+
"/gemini-login",
|
|
1363
|
+
"/gemini-status",
|
|
1364
|
+
"/gemini-logout",
|
|
1051
1365
|
];
|
|
1052
1366
|
api.logger.info(`[cli-bridge] registered ${allCommands.length} commands: ${allCommands.join(", ")}`);
|
|
1053
1367
|
},
|
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.2.
|
|
4
|
+
"version": "0.2.30",
|
|
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.2.
|
|
3
|
+
"version": "0.2.30",
|
|
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": {
|