@elvatis_com/openclaw-cli-bridge-elvatis 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/SKILL.md +3 -1
- package/index.ts +50 -37
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/proxy-server.ts +114 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
|
|
4
4
|
|
|
5
|
-
**Current version:** `1.7.
|
|
5
|
+
**Current version:** `1.7.2`
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -362,6 +362,13 @@ npm test # vitest run (83 tests)
|
|
|
362
362
|
|
|
363
363
|
## Changelog
|
|
364
364
|
|
|
365
|
+
### v1.7.2
|
|
366
|
+
- **fix:** Startup restore now uses cookie expiry file as primary check — if cookies are still valid (>1h left), the persistent context is launched immediately without a fragile browser selector check. This eliminates false "not logged in" errors for Grok/Claude/ChatGPT caused by slow page loads or DOM selector changes.
|
|
367
|
+
- **fix:** Grok cookie file path corrected to `grok-cookie-expiry.json` (was `grok-session.json`).
|
|
368
|
+
|
|
369
|
+
### v1.7.1
|
|
370
|
+
- **feat:** `/status` HTML dashboard — browser-accessible health page at `http://127.0.0.1:31337/status`. Shows all 4 web providers with live status badge (Connected / Logged in / Expired / Never logged in), cookie expiry per provider, CLI and web model list. Auto-refreshes every 30s.
|
|
371
|
+
|
|
365
372
|
### v1.7.0
|
|
366
373
|
- **fix:** Startup restore timeout 3s → 6s with one retry, eliminates false "not logged in" for slow-loading pages (Grok)
|
|
367
374
|
- **feat:** Auto-relogin on startup — if cookies truly expired, attempt headless relogin before sending WhatsApp alert
|
package/SKILL.md
CHANGED
|
@@ -56,6 +56,8 @@ Sessions survive gateway restarts. `/bridge-status` shows all 4 at a glance.
|
|
|
56
56
|
|
|
57
57
|
On gateway restart, if any session has expired, a **WhatsApp alert** is sent automatically with the exact `/xxx-login` commands needed — no guessing required.
|
|
58
58
|
|
|
59
|
+
**Browser health dashboard:** `http://127.0.0.1:31337/status` — live overview of all 4 providers, cookie expiry, and model list. Auto-refreshes every 30s.
|
|
60
|
+
|
|
59
61
|
## Setup
|
|
60
62
|
|
|
61
63
|
1. Enable plugin + restart gateway
|
|
@@ -64,4 +66,4 @@ On gateway restart, if any session has expired, a **WhatsApp alert** is sent aut
|
|
|
64
66
|
|
|
65
67
|
See `README.md` for full configuration reference and architecture diagram.
|
|
66
68
|
|
|
67
|
-
**Version:** 1.7.
|
|
69
|
+
**Version:** 1.7.2
|
package/index.ts
CHANGED
|
@@ -912,7 +912,7 @@ function proxyTestRequest(
|
|
|
912
912
|
const plugin = {
|
|
913
913
|
id: "openclaw-cli-bridge-elvatis",
|
|
914
914
|
name: "OpenClaw CLI Bridge",
|
|
915
|
-
version: "1.7.
|
|
915
|
+
version: "1.7.2",
|
|
916
916
|
description:
|
|
917
917
|
"Phase 1: openai-codex auth bridge. " +
|
|
918
918
|
"Phase 2: HTTP proxy for gemini/claude CLIs. " +
|
|
@@ -959,7 +959,7 @@ const plugin = {
|
|
|
959
959
|
{
|
|
960
960
|
name: "grok",
|
|
961
961
|
profileDir: GROK_PROFILE_DIR,
|
|
962
|
-
cookieFile:
|
|
962
|
+
cookieFile: GROK_EXPIRY_FILE,
|
|
963
963
|
verifySelector: "textarea",
|
|
964
964
|
homeUrl: "https://grok.com",
|
|
965
965
|
loginCmd: "/grok-login",
|
|
@@ -1005,8 +1005,44 @@ const plugin = {
|
|
|
1005
1005
|
}
|
|
1006
1006
|
if (p.getCtx()) continue; // already connected
|
|
1007
1007
|
|
|
1008
|
+
// ── Cookie-first check ────────────────────────────────────────────
|
|
1009
|
+
// If a cookie expiry file exists and is still valid (>1h left),
|
|
1010
|
+
// launch the persistent context immediately without a browser-based
|
|
1011
|
+
// selector check. Selector checks are fragile (slow pages, DOM changes).
|
|
1012
|
+
// The keep-alive (20h) will verify the session properly later.
|
|
1013
|
+
let cookiesValid = false;
|
|
1014
|
+
if (existsSync(p.cookieFile)) {
|
|
1015
|
+
try {
|
|
1016
|
+
const expInfo = JSON.parse(readFileSync(p.cookieFile, "utf-8")) as { expiresAt: number };
|
|
1017
|
+
const msLeft = expInfo.expiresAt - Date.now();
|
|
1018
|
+
if (msLeft > 3_600_000) { // >1h remaining
|
|
1019
|
+
cookiesValid = true;
|
|
1020
|
+
api.logger.info(`[cli-bridge:${p.name}] cookie valid (${Math.floor(msLeft / 86_400_000)}d left) — restoring context without browser check`);
|
|
1021
|
+
}
|
|
1022
|
+
} catch { /* ignore parse errors */ }
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (cookiesValid) {
|
|
1026
|
+
try {
|
|
1027
|
+
const ctx = await chromium.launchPersistentContext(p.profileDir, {
|
|
1028
|
+
headless: true,
|
|
1029
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
1030
|
+
});
|
|
1031
|
+
p.setCtx(ctx);
|
|
1032
|
+
ctx.on("close", () => { p.setCtx(null as unknown as BrowserContext); });
|
|
1033
|
+
api.logger.info(`[cli-bridge:${p.name}] session restored from profile ✅`);
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
api.logger.warn(`[cli-bridge:${p.name}] context launch failed: ${(err as Error).message}`);
|
|
1036
|
+
needsLogin.push(p.loginCmd);
|
|
1037
|
+
}
|
|
1038
|
+
// Sequential — never spawn all 4 Chromium instances at once
|
|
1039
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ── Fallback: cookies expired or missing — try browser check ─────
|
|
1008
1044
|
try {
|
|
1009
|
-
api.logger.info(`[cli-bridge:${p.name}]
|
|
1045
|
+
api.logger.info(`[cli-bridge:${p.name}] cookies expired/missing — verifying via browser…`);
|
|
1010
1046
|
const ctx = await chromium.launchPersistentContext(p.profileDir, {
|
|
1011
1047
|
headless: true,
|
|
1012
1048
|
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
@@ -1014,46 +1050,16 @@ const plugin = {
|
|
|
1014
1050
|
const page = await ctx.newPage();
|
|
1015
1051
|
await page.goto(p.homeUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
1016
1052
|
await new Promise(r => setTimeout(r, 6000));
|
|
1017
|
-
|
|
1018
|
-
// Retry once — some pages (Grok) load slowly
|
|
1019
|
-
if (!ok) {
|
|
1020
|
-
api.logger.info(`[cli-bridge:${p.name}] verifySelector not visible after 6s, retrying (3s)…`);
|
|
1021
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
1022
|
-
ok = await page.locator(p.verifySelector).isVisible().catch(() => false);
|
|
1023
|
-
}
|
|
1053
|
+
const ok = await page.locator(p.verifySelector).isVisible().catch(() => false);
|
|
1024
1054
|
await page.close().catch(() => {});
|
|
1025
1055
|
if (ok) {
|
|
1026
1056
|
p.setCtx(ctx);
|
|
1027
1057
|
ctx.on("close", () => { p.setCtx(null as unknown as BrowserContext); });
|
|
1028
1058
|
api.logger.info(`[cli-bridge:${p.name}] session restored from profile ✅`);
|
|
1029
1059
|
} else {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
try {
|
|
1034
|
-
const reloginPage = await ctx.newPage();
|
|
1035
|
-
await reloginPage.goto(p.homeUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
|
|
1036
|
-
await new Promise(r => setTimeout(r, 6000));
|
|
1037
|
-
let reloginOk = await reloginPage.locator(p.verifySelector).isVisible().catch(() => false);
|
|
1038
|
-
if (!reloginOk) {
|
|
1039
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
1040
|
-
reloginOk = await reloginPage.locator(p.verifySelector).isVisible().catch(() => false);
|
|
1041
|
-
}
|
|
1042
|
-
await reloginPage.close().catch(() => {});
|
|
1043
|
-
if (reloginOk) {
|
|
1044
|
-
p.setCtx(ctx);
|
|
1045
|
-
ctx.on("close", () => { p.setCtx(null as unknown as BrowserContext); });
|
|
1046
|
-
api.logger.info(`[cli-bridge:${p.name}] session restored (slow load) ✅`);
|
|
1047
|
-
} else {
|
|
1048
|
-
await ctx.close().catch(() => {});
|
|
1049
|
-
api.logger.info(`[cli-bridge:${p.name}] auto-relogin failed, needs manual ${p.loginCmd}`);
|
|
1050
|
-
needsLogin.push(p.loginCmd);
|
|
1051
|
-
}
|
|
1052
|
-
} catch (reloginErr) {
|
|
1053
|
-
await ctx.close().catch(() => {});
|
|
1054
|
-
api.logger.warn(`[cli-bridge:${p.name}] auto-relogin error: ${(reloginErr as Error).message}`);
|
|
1055
|
-
needsLogin.push(p.loginCmd);
|
|
1056
|
-
}
|
|
1060
|
+
await ctx.close().catch(() => {});
|
|
1061
|
+
api.logger.info(`[cli-bridge:${p.name}] session expired — needs ${p.loginCmd}`);
|
|
1062
|
+
needsLogin.push(p.loginCmd);
|
|
1057
1063
|
}
|
|
1058
1064
|
} catch (err) {
|
|
1059
1065
|
api.logger.warn(`[cli-bridge:${p.name}] startup restore failed: ${(err as Error).message}`);
|
|
@@ -1240,6 +1246,13 @@ const plugin = {
|
|
|
1240
1246
|
}
|
|
1241
1247
|
return chatgptContext;
|
|
1242
1248
|
},
|
|
1249
|
+
version: plugin.version,
|
|
1250
|
+
getExpiryInfo: () => ({
|
|
1251
|
+
grok: (() => { const e = loadGrokExpiry(); return e ? formatExpiryInfo(e) : null; })(),
|
|
1252
|
+
gemini: (() => { const e = loadGeminiExpiry(); return e ? formatGeminiExpiry(e) : null; })(),
|
|
1253
|
+
claude: (() => { const e = loadClaudeExpiry(); return e ? formatClaudeExpiry(e) : null; })(),
|
|
1254
|
+
chatgpt: (() => { const e = loadChatGPTExpiry(); return e ? formatChatGPTExpiry(e) : null; })(),
|
|
1255
|
+
}),
|
|
1243
1256
|
});
|
|
1244
1257
|
proxyServer = server;
|
|
1245
1258
|
api.logger.info(
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-cli-bridge-elvatis",
|
|
3
3
|
"name": "OpenClaw CLI Bridge",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.2",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
|
|
7
7
|
"providers": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elvatis_com/openclaw-cli-bridge-elvatis",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
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": {
|
package/src/proxy-server.ts
CHANGED
|
@@ -60,6 +60,15 @@ export interface ProxyServerOptions {
|
|
|
60
60
|
_chatgptComplete?: typeof chatgptComplete;
|
|
61
61
|
/** Override for testing — replaces chatgptCompleteStream */
|
|
62
62
|
_chatgptCompleteStream?: typeof chatgptCompleteStream;
|
|
63
|
+
/** Returns human-readable expiry string for each web provider (null = no login yet) */
|
|
64
|
+
getExpiryInfo?: () => {
|
|
65
|
+
grok: string | null;
|
|
66
|
+
gemini: string | null;
|
|
67
|
+
claude: string | null;
|
|
68
|
+
chatgpt: string | null;
|
|
69
|
+
};
|
|
70
|
+
/** Plugin version string for the status page */
|
|
71
|
+
version?: string;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/** Available CLI bridge models for GET /v1/models */
|
|
@@ -156,6 +165,111 @@ async function handleRequest(
|
|
|
156
165
|
return;
|
|
157
166
|
}
|
|
158
167
|
|
|
168
|
+
// Browser status page — human-readable HTML dashboard
|
|
169
|
+
if ((url === "/status" || url === "/") && req.method === "GET") {
|
|
170
|
+
const expiry = opts.getExpiryInfo?.() ?? { grok: null, gemini: null, claude: null, chatgpt: null };
|
|
171
|
+
const version = opts.version ?? "?";
|
|
172
|
+
|
|
173
|
+
const providers = [
|
|
174
|
+
{ name: "Grok", icon: "𝕏", expiry: expiry.grok, loginCmd: "/grok-login", ctx: opts.getGrokContext?.() ?? null },
|
|
175
|
+
{ name: "Gemini", icon: "✦", expiry: expiry.gemini, loginCmd: "/gemini-login", ctx: opts.getGeminiContext?.() ?? null },
|
|
176
|
+
{ name: "Claude", icon: "◆", expiry: expiry.claude, loginCmd: "/claude-login", ctx: opts.getClaudeContext?.() ?? null },
|
|
177
|
+
{ name: "ChatGPT", icon: "◉", expiry: expiry.chatgpt, loginCmd: "/chatgpt-login", ctx: opts.getChatGPTContext?.() ?? null },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
function statusBadge(p: typeof providers[0]): { label: string; color: string; dot: string } {
|
|
181
|
+
if (p.ctx !== null) return { label: "Connected", color: "#22c55e", dot: "🟢" };
|
|
182
|
+
if (!p.expiry) return { label: "Never logged in", color: "#6b7280", dot: "⚪" };
|
|
183
|
+
if (p.expiry.startsWith("⚠️ EXPIRED")) return { label: "Expired", color: "#ef4444", dot: "🔴" };
|
|
184
|
+
if (p.expiry.startsWith("🚨")) return { label: "Expiring soon", color: "#f59e0b", dot: "🟡" };
|
|
185
|
+
return { label: "Logged in", color: "#3b82f6", dot: "🔵" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rows = providers.map(p => {
|
|
189
|
+
const badge = statusBadge(p);
|
|
190
|
+
const expiryText = p.expiry
|
|
191
|
+
? p.expiry.replace(/[⚠️🚨✅🕐]/gu, "").trim()
|
|
192
|
+
: `Not logged in — run <code>${p.loginCmd}</code>`;
|
|
193
|
+
return `
|
|
194
|
+
<tr>
|
|
195
|
+
<td style="padding:12px 16px;font-weight:600;font-size:15px">${p.icon} ${p.name}</td>
|
|
196
|
+
<td style="padding:12px 16px">
|
|
197
|
+
<span style="background:${badge.color}22;color:${badge.color};border:1px solid ${badge.color}44;
|
|
198
|
+
border-radius:6px;padding:3px 10px;font-size:13px;font-weight:600">
|
|
199
|
+
${badge.dot} ${badge.label}
|
|
200
|
+
</span>
|
|
201
|
+
</td>
|
|
202
|
+
<td style="padding:12px 16px;color:#9ca3af;font-size:13px">${expiryText}</td>
|
|
203
|
+
<td style="padding:12px 16px;color:#6b7280;font-size:12px;font-family:monospace">${p.loginCmd}</td>
|
|
204
|
+
</tr>`;
|
|
205
|
+
}).join("");
|
|
206
|
+
|
|
207
|
+
const cliModels = CLI_MODELS.filter(m => m.id.startsWith("cli-"));
|
|
208
|
+
const webModels = CLI_MODELS.filter(m => m.id.startsWith("web-"));
|
|
209
|
+
const modelList = (models: typeof CLI_MODELS) =>
|
|
210
|
+
models.map(m => `<li style="margin:2px 0;font-size:13px;color:#d1d5db"><code style="color:#93c5fd">${m.id}</code></li>`).join("");
|
|
211
|
+
|
|
212
|
+
const html = `<!DOCTYPE html>
|
|
213
|
+
<html lang="en">
|
|
214
|
+
<head>
|
|
215
|
+
<meta charset="UTF-8">
|
|
216
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
217
|
+
<title>CLI Bridge Status</title>
|
|
218
|
+
<meta http-equiv="refresh" content="30">
|
|
219
|
+
<style>
|
|
220
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
221
|
+
body { background: #0f1117; color: #e5e7eb; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; min-height: 100vh; padding: 32px 24px; }
|
|
222
|
+
h1 { font-size: 22px; font-weight: 700; color: #f9fafb; margin-bottom: 4px; }
|
|
223
|
+
.subtitle { color: #6b7280; font-size: 13px; margin-bottom: 28px; }
|
|
224
|
+
.card { background: #1a1d27; border: 1px solid #2d3148; border-radius: 12px; overflow: hidden; margin-bottom: 24px; }
|
|
225
|
+
.card-header { padding: 14px 16px; border-bottom: 1px solid #2d3148; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
226
|
+
table { width: 100%; border-collapse: collapse; }
|
|
227
|
+
tr:not(:last-child) td { border-bottom: 1px solid #1f2335; }
|
|
228
|
+
.models { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
229
|
+
ul { list-style: none; padding: 12px 16px; }
|
|
230
|
+
.footer { color: #374151; font-size: 12px; text-align: center; margin-top: 16px; }
|
|
231
|
+
code { background: #1e2130; padding: 1px 5px; border-radius: 4px; }
|
|
232
|
+
</style>
|
|
233
|
+
</head>
|
|
234
|
+
<body>
|
|
235
|
+
<h1>🌉 CLI Bridge</h1>
|
|
236
|
+
<p class="subtitle">v${version} · Port ${opts.port} · Auto-refreshes every 30s</p>
|
|
237
|
+
|
|
238
|
+
<div class="card">
|
|
239
|
+
<div class="card-header">Web Session Providers</div>
|
|
240
|
+
<table>
|
|
241
|
+
<thead>
|
|
242
|
+
<tr style="background:#13151f">
|
|
243
|
+
<th style="padding:10px 16px;text-align:left;font-size:12px;color:#4b5563;font-weight:600">Provider</th>
|
|
244
|
+
<th style="padding:10px 16px;text-align:left;font-size:12px;color:#4b5563;font-weight:600">Status</th>
|
|
245
|
+
<th style="padding:10px 16px;text-align:left;font-size:12px;color:#4b5563;font-weight:600">Session</th>
|
|
246
|
+
<th style="padding:10px 16px;text-align:left;font-size:12px;color:#4b5563;font-weight:600">Login</th>
|
|
247
|
+
</tr>
|
|
248
|
+
</thead>
|
|
249
|
+
<tbody>${rows}</tbody>
|
|
250
|
+
</table>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div class="models">
|
|
254
|
+
<div class="card">
|
|
255
|
+
<div class="card-header">CLI Models (${cliModels.length})</div>
|
|
256
|
+
<ul>${modelList(cliModels)}</ul>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="card">
|
|
259
|
+
<div class="card-header">Web Session Models (${webModels.length})</div>
|
|
260
|
+
<ul>${modelList(webModels)}</ul>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<p class="footer">openclaw-cli-bridge-elvatis v${version} · <a href="/v1/models" style="color:#4b5563">/v1/models</a> · <a href="/health" style="color:#4b5563">/health</a></p>
|
|
265
|
+
</body>
|
|
266
|
+
</html>`;
|
|
267
|
+
|
|
268
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
269
|
+
res.end(html);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
159
273
|
// Model list
|
|
160
274
|
if (url === "/v1/models" && req.method === "GET") {
|
|
161
275
|
const now = Math.floor(Date.now() / 1000);
|