@elvatis_com/openclaw-cli-bridge-elvatis 1.3.4 → 1.3.5

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 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.3.4`
5
+ **Current version:** `1.3.5`
6
6
 
7
7
  ---
8
8
 
@@ -81,6 +81,77 @@ All commands use gateway-level `commands.allowFrom` for authorization (`requireA
81
81
 
82
82
  ---
83
83
 
84
+ ### Phase 4 — Web Browser Providers (headless, no API key needed)
85
+
86
+ Routes requests through real browser sessions on the provider's web UI. Requires a valid login session (free or paid tier). Uses persistent Chromium profiles — sessions survive gateway restarts.
87
+
88
+ **Grok** (grok.com — SuperGrok subscription):
89
+
90
+ | Model | Notes |
91
+ |---|---|
92
+ | `web-grok/grok-3` | Full model |
93
+ | `web-grok/grok-3-fast` | Faster variant |
94
+ | `web-grok/grok-3-mini` | Lightweight |
95
+ | `web-grok/grok-3-mini-fast` | Fastest |
96
+
97
+ | Command | What it does |
98
+ |---|---|
99
+ | `/grok-login` | Authenticate via X.com OAuth, save session to `~/.openclaw/grok-profile/` |
100
+ | `/grok-status` | Show session validity + cookie expiry |
101
+ | `/grok-logout` | Clear session |
102
+
103
+ **Claude** (claude.ai):
104
+
105
+ | Model | Notes |
106
+ |---|---|
107
+ | `web-claude/claude-sonnet` | Sonnet |
108
+ | `web-claude/claude-opus` | Opus |
109
+ | `web-claude/claude-haiku` | Haiku |
110
+
111
+ | Command | What it does |
112
+ |---|---|
113
+ | `/claude-login` | Authenticate, save cookies to `~/.openclaw/claude-profile/` |
114
+ | `/claude-status` | Show session validity + cookie expiry |
115
+ | `/claude-logout` | Clear session |
116
+
117
+ **Gemini** (gemini.google.com):
118
+
119
+ | Model | Notes |
120
+ |---|---|
121
+ | `web-gemini/gemini-2-5-pro` | Gemini 2.5 Pro |
122
+ | `web-gemini/gemini-2-5-flash` | Gemini 2.5 Flash |
123
+ | `web-gemini/gemini-3-pro` | Gemini 3 Pro |
124
+ | `web-gemini/gemini-3-flash` | Gemini 3 Flash |
125
+
126
+ | Command | What it does |
127
+ |---|---|
128
+ | `/gemini-login` | Authenticate, save cookies to `~/.openclaw/gemini-profile/` |
129
+ | `/gemini-status` | Show session validity + cookie expiry |
130
+ | `/gemini-logout` | Clear session |
131
+
132
+ **ChatGPT** (chatgpt.com):
133
+
134
+ | Model | Notes |
135
+ |---|---|
136
+ | `web-chatgpt/gpt-4o` | GPT-4o |
137
+ | `web-chatgpt/gpt-4o-mini` | GPT-4o Mini |
138
+ | `web-chatgpt/gpt-o3` | o3 |
139
+ | `web-chatgpt/gpt-o4-mini` | o4-mini |
140
+ | `web-chatgpt/gpt-5` | GPT-5 |
141
+
142
+ | Command | What it does |
143
+ |---|---|
144
+ | `/chatgpt-login` | Authenticate, save cookies to `~/.openclaw/chatgpt-profile/` |
145
+ | `/chatgpt-status` | Show session validity + cookie expiry |
146
+ | `/chatgpt-logout` | Clear session |
147
+
148
+ **Session lifecycle:**
149
+ - First use: run `/xxx-login` once (opens Chromium, authenticate in browser)
150
+ - After gateway restart: sessions are **automatically restored** from saved profiles on startup (sequential, ~25s after start)
151
+ - `/bridge-status` — shows all 4 providers at a glance with login state + expiry info
152
+
153
+ ---
154
+
84
155
  ## Requirements
85
156
 
86
157
  - [OpenClaw](https://openclaw.ai) gateway (tested with `2026.3.x`)
@@ -287,6 +358,11 @@ npm test # vitest run (45 tests)
287
358
 
288
359
  ## Changelog
289
360
 
361
+ ### v1.3.5
362
+ - **fix:** Startup session restore now runs only once per process lifetime — `_startupRestoreDone` module-level guard prevents re-running on every hot-reload (SIGUSR1), which was triggered every ~60s by the openclaw-control-ui dashboard poll
363
+ - **root cause:** Gateway `reload.mode=hybrid` + dashboard status polling caused plugin to reinitialize every 60s → each reload spawned a new Gemini Chromium instance → RAM/CPU OOM loop
364
+ - **behavior:** First load after gateway start: sequential profile restore runs once. All subsequent hot-reloads: skip restore, reuse existing in-memory contexts
365
+
290
366
  ### v1.3.4
291
367
  - **feat:** Safe sequential session restore on startup — if a saved profile exists, providers are reconnected automatically after gateway restart (one at a time, 3s delay between each, headless)
292
368
  - **fix:** No manual `/xxx-login` needed after reboot if profile is already saved
package/index.ts CHANGED
@@ -233,6 +233,10 @@ async function scanCookieExpiry(ctx: import("playwright").BrowserContext): Promi
233
233
  let _cdpBrowser: import("playwright").Browser | null = null;
234
234
  let _cdpBrowserLaunchPromise: Promise<import("playwright").BrowserContext | null> | null = null;
235
235
 
236
+ // Startup restore guard — module-level so it survives hot-reloads (SIGUSR1).
237
+ // Set to true after first run; hot-reloads see true and skip the restore loop.
238
+ let _startupRestoreDone = false;
239
+
236
240
  /**
237
241
  * Connect to the OpenClaw managed browser (CDP port 18800).
238
242
  * Singleton: reuses the same connection. Falls back to persistent Chromium for Grok only.
@@ -746,97 +750,102 @@ const plugin = {
746
750
  const codexAuthPath = cfg.codexAuthPath ?? DEFAULT_CODEX_AUTH_PATH;
747
751
  const grokSessionPath = cfg.grokSessionPath ?? DEFAULT_SESSION_PATH;
748
752
 
749
- // ── Safe session restore on startup (sequential, profile-gated, non-blocking) ──
750
- // Restores provider sessions from saved persistent profiles but ONLY if the
751
- // profile directory already exists (i.e. user has logged in before).
752
- // Providers are launched one at a time with a delay to avoid OOM.
753
- void (async () => {
754
- await new Promise(r => setTimeout(r, 5000)); // wait for proxy + gateway to settle
755
- const { chromium } = await import("playwright");
756
- const { existsSync } = await import("node:fs");
757
-
758
- const profileProviders: Array<{
759
- name: string;
760
- profileDir: string;
761
- cookieFile: string;
762
- verifySelector: string;
763
- homeUrl: string;
764
- setCtx: (c: BrowserContext) => void;
765
- getCtx: () => BrowserContext | null;
766
- }> = [
767
- {
768
- name: "grok",
769
- profileDir: GROK_PROFILE_DIR,
770
- cookieFile: join(homedir(), ".openclaw", "grok-session.json"),
771
- verifySelector: "textarea",
772
- homeUrl: "https://grok.com",
773
- getCtx: () => grokContext,
774
- setCtx: (c) => { grokContext = c; },
775
- },
776
- {
777
- name: "claude",
778
- profileDir: join(homedir(), ".openclaw", "claude-profile"),
779
- cookieFile: join(homedir(), ".openclaw", "claude-cookie-expiry.json"),
780
- verifySelector: ".ProseMirror",
781
- homeUrl: "https://claude.ai/new",
782
- getCtx: () => claudeContext,
783
- setCtx: (c) => { claudeContext = c; },
784
- },
785
- {
786
- name: "gemini",
787
- profileDir: join(homedir(), ".openclaw", "gemini-profile"),
788
- cookieFile: join(homedir(), ".openclaw", "gemini-cookie-expiry.json"),
789
- verifySelector: ".ql-editor",
790
- homeUrl: "https://gemini.google.com/app",
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
- ];
753
+ // ── Session restore: only on first plugin load (not on hot-reloads) ──────
754
+ // The gateway polls every ~60s via openclaw status, which triggers a hot-reload
755
+ // (SIGUSR1 + hybrid mode). Module-level contexts (grokContext etc.) survive
756
+ // hot-reloads because Node keeps the module in memory so we only need to
757
+ // restore once, on the very first load (when all contexts are null).
758
+ //
759
+ // Guard: _startupRestoreDone is module-level and persists across hot-reloads.
760
+ if (!_startupRestoreDone) {
761
+ _startupRestoreDone = true;
762
+ void (async () => {
763
+ await new Promise(r => setTimeout(r, 5000)); // wait for proxy + gateway to settle
764
+ const { chromium } = await import("playwright");
765
+ const { existsSync } = await import("node:fs");
766
+
767
+ const profileProviders: Array<{
768
+ name: string;
769
+ profileDir: string;
770
+ cookieFile: string;
771
+ verifySelector: string;
772
+ homeUrl: string;
773
+ setCtx: (c: BrowserContext) => void;
774
+ getCtx: () => BrowserContext | null;
775
+ }> = [
776
+ {
777
+ name: "grok",
778
+ profileDir: GROK_PROFILE_DIR,
779
+ cookieFile: join(homedir(), ".openclaw", "grok-session.json"),
780
+ verifySelector: "textarea",
781
+ homeUrl: "https://grok.com",
782
+ getCtx: () => grokContext,
783
+ setCtx: (c) => { grokContext = c; },
784
+ },
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
+ {
795
+ name: "gemini",
796
+ profileDir: join(homedir(), ".openclaw", "gemini-profile"),
797
+ cookieFile: join(homedir(), ".openclaw", "gemini-cookie-expiry.json"),
798
+ verifySelector: ".ql-editor",
799
+ homeUrl: "https://gemini.google.com/app",
800
+ getCtx: () => geminiContext,
801
+ setCtx: (c) => { geminiContext = c; },
802
+ },
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
+ ];
804
813
 
805
- for (const p of profileProviders) {
806
- // Skip if no saved profile/session exists
807
- if (!existsSync(p.profileDir) && !existsSync(p.cookieFile)) {
808
- api.logger.info(`[cli-bridge:${p.name}] no saved profile — skipping startup restore`);
809
- continue;
810
- }
811
- if (p.getCtx()) continue; // already connected
814
+ for (const p of profileProviders) {
815
+ if (!existsSync(p.profileDir) && !existsSync(p.cookieFile)) {
816
+ api.logger.info(`[cli-bridge:${p.name}] no saved profile — skipping startup restore`);
817
+ continue;
818
+ }
819
+ if (p.getCtx()) continue; // already connected
812
820
 
813
- try {
814
- api.logger.info(`[cli-bridge:${p.name}] restoring session from profile…`);
815
- const ctx = await chromium.launchPersistentContext(p.profileDir, {
816
- headless: true,
817
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
818
- });
819
- const page = await ctx.newPage();
820
- await page.goto(p.homeUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
821
- await new Promise(r => setTimeout(r, 3000));
822
- const ok = await page.locator(p.verifySelector).isVisible().catch(() => false);
823
- await page.close().catch(() => {});
824
- if (ok) {
825
- p.setCtx(ctx);
826
- ctx.on("close", () => { p.setCtx(null as unknown as BrowserContext); });
827
- api.logger.info(`[cli-bridge:${p.name}] session restored from profile ✅`);
828
- } else {
829
- await ctx.close().catch(() => {});
830
- api.logger.info(`[cli-bridge:${p.name}] profile exists but not logged in — skipping`);
821
+ try {
822
+ api.logger.info(`[cli-bridge:${p.name}] restoring session from profile…`);
823
+ const ctx = await chromium.launchPersistentContext(p.profileDir, {
824
+ headless: true,
825
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
826
+ });
827
+ const page = await ctx.newPage();
828
+ await page.goto(p.homeUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
829
+ await new Promise(r => setTimeout(r, 3000));
830
+ const ok = await page.locator(p.verifySelector).isVisible().catch(() => false);
831
+ await page.close().catch(() => {});
832
+ if (ok) {
833
+ p.setCtx(ctx);
834
+ ctx.on("close", () => { p.setCtx(null as unknown as BrowserContext); });
835
+ api.logger.info(`[cli-bridge:${p.name}] session restored from profile ✅`);
836
+ } else {
837
+ await ctx.close().catch(() => {});
838
+ api.logger.info(`[cli-bridge:${p.name}] profile exists but not logged in — needs /xxx-login`);
839
+ }
840
+ } catch (err) {
841
+ api.logger.warn(`[cli-bridge:${p.name}] startup restore failed: ${(err as Error).message}`);
831
842
  }
832
- } catch (err) {
833
- api.logger.warn(`[cli-bridge:${p.name}] startup restore failed: ${(err as Error).message}`);
834
- }
835
843
 
836
- // Sequential delay avoid spawning all 4 Chromium instances at once
837
- await new Promise(r => setTimeout(r, 3000));
838
- }
839
- })();
844
+ // Sequential — never spawn all 4 Chromium instances at once
845
+ await new Promise(r => setTimeout(r, 3000));
846
+ }
847
+ })();
848
+ }
840
849
 
841
850
  // ── Phase 1: openai-codex auth bridge ─────────────────────────────────────
842
851
  if (enableCodex) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-cli-bridge-elvatis",
3
3
  "name": "OpenClaw CLI Bridge",
4
- "version": "1.3.4",
4
+ "version": "1.3.5",
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": "1.3.4",
3
+ "version": "1.3.5",
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": {