@elvatis_com/openclaw-cli-bridge-elvatis 0.2.3 → 0.2.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 +128 -48
- package/index.ts +30 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cli-runner.ts +87 -90
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# openclaw-cli-bridge-elvatis
|
|
2
2
|
|
|
3
|
-
> OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code) as model providers — with slash commands for instant model switching.
|
|
3
|
+
> OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code) as model providers — with slash commands for instant model switching, restore, and health testing.
|
|
4
4
|
|
|
5
|
-
**Current version:** `0.2.
|
|
5
|
+
**Current version:** `0.2.5`
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -14,30 +14,59 @@ Registers the `openai-codex` provider by reading OAuth tokens already stored by
|
|
|
14
14
|
### Phase 2 — Request bridge (local proxy)
|
|
15
15
|
Starts a local OpenAI-compatible HTTP proxy on `127.0.0.1:31337` and configures OpenClaw's `vllm` provider to route calls through `gemini` and `claude` CLI subprocesses.
|
|
16
16
|
|
|
17
|
-
Prompt delivery
|
|
17
|
+
**Prompt delivery:** always via **stdin** (never CLI args or `@file`) — avoids `E2BIG` for long sessions and Gemini agentic mode. Each message batch is truncated to the last 20 messages + system message (`MAX_MESSAGES`/`MAX_MSG_CHARS` in `src/cli-runner.ts`).
|
|
18
18
|
|
|
19
|
-
| Model reference | CLI invoked |
|
|
20
|
-
|
|
21
|
-
| `vllm/cli-gemini/gemini-2.5-pro` | `gemini -m gemini-2.5-pro
|
|
22
|
-
| `vllm/cli-gemini/gemini-2.5-flash` | `gemini -m gemini-2.5-flash
|
|
23
|
-
| `vllm/cli-gemini/gemini-3-pro` | `gemini -m gemini-3-pro
|
|
24
|
-
| `vllm/cli-claude/claude-sonnet-4-6` | `claude -p --output-format text --model claude-sonnet-4-6` (stdin) |
|
|
25
|
-
| `vllm/cli-claude/claude-opus-4-6` | `claude -p --output-format text --model claude-opus-4-6` (stdin) |
|
|
26
|
-
| `vllm/cli-claude/claude-haiku-4-5` | `claude -p --output-format text --model claude-haiku-4-5` (stdin) |
|
|
19
|
+
| Model reference | CLI invoked | Latency |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `vllm/cli-gemini/gemini-2.5-pro` | `gemini -m gemini-2.5-pro -p ""` (stdin, cwd=/tmp) | ~8–10s |
|
|
22
|
+
| `vllm/cli-gemini/gemini-2.5-flash` | `gemini -m gemini-2.5-flash -p ""` (stdin, cwd=/tmp) | ~4–6s |
|
|
23
|
+
| `vllm/cli-gemini/gemini-3-pro` | `gemini -m gemini-3-pro -p ""` (stdin, cwd=/tmp) | ~8–10s |
|
|
24
|
+
| `vllm/cli-claude/claude-sonnet-4-6` | `claude -p --output-format text --model claude-sonnet-4-6` (stdin) | ~2–4s |
|
|
25
|
+
| `vllm/cli-claude/claude-opus-4-6` | `claude -p --output-format text --model claude-opus-4-6` (stdin) | ~3–5s |
|
|
26
|
+
| `vllm/cli-claude/claude-haiku-4-5` | `claude -p --output-format text --model claude-haiku-4-5` (stdin) | ~1–3s |
|
|
27
27
|
|
|
28
28
|
### Phase 3 — Slash commands
|
|
29
|
-
|
|
29
|
+
Ten plugin-registered commands (all `requireAuth: true`):
|
|
30
|
+
|
|
31
|
+
**Claude Code CLI** (routed via local proxy on `:31337`):
|
|
30
32
|
|
|
31
|
-
| Command |
|
|
33
|
+
| Command | Model |
|
|
32
34
|
|---|---|
|
|
33
35
|
| `/cli-sonnet` | `vllm/cli-claude/claude-sonnet-4-6` |
|
|
34
36
|
| `/cli-opus` | `vllm/cli-claude/claude-opus-4-6` |
|
|
35
37
|
| `/cli-haiku` | `vllm/cli-claude/claude-haiku-4-5` |
|
|
38
|
+
|
|
39
|
+
**Gemini CLI** (routed via local proxy on `:31337`, stdin + `cwd=/tmp`):
|
|
40
|
+
|
|
41
|
+
| Command | Model |
|
|
42
|
+
|---|---|
|
|
36
43
|
| `/cli-gemini` | `vllm/cli-gemini/gemini-2.5-pro` |
|
|
37
44
|
| `/cli-gemini-flash` | `vllm/cli-gemini/gemini-2.5-flash` |
|
|
38
45
|
| `/cli-gemini3` | `vllm/cli-gemini/gemini-3-pro` |
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
**Codex CLI** (via `openai-codex` provider — Codex CLI OAuth auth, calls OpenAI API directly, **not** through the local proxy):
|
|
48
|
+
|
|
49
|
+
| Command | Model |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `/cli-codex` | `openai-codex/gpt-5.3-codex` |
|
|
52
|
+
| `/cli-codex-mini` | `openai-codex/gpt-5.1-codex-mini` |
|
|
53
|
+
|
|
54
|
+
**Utility:**
|
|
55
|
+
|
|
56
|
+
| Command | What it does |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `/cli-back` | Restore the model active **before** the last `/cli-*` switch |
|
|
59
|
+
| `/cli-test [model]` | One-shot proxy health check — **does NOT switch your active model** |
|
|
60
|
+
|
|
61
|
+
**`/cli-back` details:**
|
|
62
|
+
- Before every `/cli-*` switch the current model is saved to `~/.openclaw/cli-bridge-state.json`
|
|
63
|
+
- `/cli-back` reads it, calls `openclaw models set <previous>`, then clears the file
|
|
64
|
+
- State survives gateway restarts — safe to use any time
|
|
65
|
+
|
|
66
|
+
**`/cli-test` details:**
|
|
67
|
+
- Accepts short form (`cli-sonnet`) or full path (`vllm/cli-claude/claude-sonnet-4-6`)
|
|
68
|
+
- Default when no arg given: `cli-claude/claude-sonnet-4-6`
|
|
69
|
+
- Reports response content, latency, and confirms your active model is unchanged
|
|
41
70
|
|
|
42
71
|
---
|
|
43
72
|
|
|
@@ -57,7 +86,7 @@ All commands require `requireAuth: true` — only authorized/owner senders can e
|
|
|
57
86
|
# From ClawHub
|
|
58
87
|
clawhub install openclaw-cli-bridge-elvatis
|
|
59
88
|
|
|
60
|
-
# Or from workspace (development
|
|
89
|
+
# Or from workspace (development)
|
|
61
90
|
# Add to ~/.openclaw/openclaw.json:
|
|
62
91
|
# plugins.load.paths: ["~/.openclaw/workspace/openclaw-cli-bridge-elvatis"]
|
|
63
92
|
# plugins.entries.openclaw-cli-bridge-elvatis: { "enabled": true }
|
|
@@ -69,37 +98,61 @@ clawhub install openclaw-cli-bridge-elvatis
|
|
|
69
98
|
|
|
70
99
|
### 1. Enable + restart
|
|
71
100
|
|
|
72
|
-
```
|
|
73
|
-
|
|
101
|
+
```json
|
|
102
|
+
// ~/.openclaw/openclaw.json → plugins.entries
|
|
74
103
|
"openclaw-cli-bridge-elvatis": { "enabled": true }
|
|
104
|
+
```
|
|
75
105
|
|
|
106
|
+
```bash
|
|
76
107
|
openclaw gateway restart
|
|
77
108
|
```
|
|
78
109
|
|
|
79
|
-
### 2.
|
|
110
|
+
### 2. Verify (check gateway logs)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
[cli-bridge] proxy ready on :31337
|
|
114
|
+
[cli-bridge] registered 8 commands: /cli-sonnet, /cli-opus, /cli-haiku,
|
|
115
|
+
/cli-gemini, /cli-gemini-flash, /cli-gemini3, /cli-back, /cli-test
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 3. Register Codex auth (optional — Phase 1 only)
|
|
80
119
|
|
|
81
120
|
```bash
|
|
82
121
|
openclaw models auth login --provider openai-codex
|
|
83
122
|
# Select: "Codex CLI (existing login)"
|
|
84
123
|
```
|
|
85
124
|
|
|
86
|
-
###
|
|
87
|
-
|
|
88
|
-
On startup the plugin auto-patches `openclaw.json` with the `vllm` provider config (port `31337`) and logs:
|
|
125
|
+
### 4. Test without switching your model
|
|
89
126
|
|
|
90
127
|
```
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
/cli-test
|
|
129
|
+
→ 🧪 CLI Bridge Test
|
|
130
|
+
Model: vllm/cli-claude/claude-sonnet-4-6
|
|
131
|
+
Response: CLI bridge OK
|
|
132
|
+
Latency: 2531ms
|
|
133
|
+
Active model unchanged: anthropic/claude-sonnet-4-6
|
|
134
|
+
|
|
135
|
+
/cli-test cli-gemini
|
|
136
|
+
→ 🧪 CLI Bridge Test
|
|
137
|
+
Model: vllm/cli-gemini/gemini-2.5-pro
|
|
138
|
+
Response: CLI bridge OK
|
|
139
|
+
Latency: 8586ms
|
|
140
|
+
Active model unchanged: anthropic/claude-sonnet-4-6
|
|
93
141
|
```
|
|
94
142
|
|
|
95
|
-
###
|
|
96
|
-
|
|
97
|
-
Use any `/cli-*` command from any connected channel:
|
|
143
|
+
### 5. Switch and restore
|
|
98
144
|
|
|
99
145
|
```
|
|
100
146
|
/cli-sonnet
|
|
101
147
|
→ ✅ Switched to Claude Sonnet 4.6 (CLI)
|
|
102
148
|
`vllm/cli-claude/claude-sonnet-4-6`
|
|
149
|
+
Use /cli-back to restore previous model.
|
|
150
|
+
|
|
151
|
+
... test things ...
|
|
152
|
+
|
|
153
|
+
/cli-back
|
|
154
|
+
→ ✅ Restored previous model
|
|
155
|
+
`anthropic/claude-sonnet-4-6`
|
|
103
156
|
```
|
|
104
157
|
|
|
105
158
|
---
|
|
@@ -125,17 +178,33 @@ In `~/.openclaw/openclaw.json` → `plugins.entries.openclaw-cli-bridge-elvatis.
|
|
|
125
178
|
```
|
|
126
179
|
OpenClaw agent
|
|
127
180
|
│
|
|
128
|
-
├─ openai-codex/*
|
|
181
|
+
├─ openai-codex/* ──────────────────────────► OpenAI API (direct)
|
|
182
|
+
│ auth: ~/.codex/auth.json OAuth tokens ▲
|
|
183
|
+
│ │
|
|
184
|
+
│ /cli-codex, /cli-codex-mini ─────────────────┘ (switch to this provider)
|
|
129
185
|
│
|
|
130
186
|
└─ vllm/cli-gemini/* ─┐
|
|
131
187
|
vllm/cli-claude/* ─┤─► localhost:31337 (openclaw-cli-bridge proxy)
|
|
132
|
-
│ ├─ cli-gemini/* → gemini -m <model>
|
|
133
|
-
│
|
|
188
|
+
│ ├─ cli-gemini/* → gemini -m <model> -p ""
|
|
189
|
+
│ │ stdin=prompt, cwd=/tmp
|
|
190
|
+
│ │ (neutral cwd prevents agentic mode)
|
|
191
|
+
│ └─ cli-claude/* → claude -p --model <model>
|
|
192
|
+
│ stdin=prompt
|
|
134
193
|
└───────────────────────────────────────────────────
|
|
135
194
|
|
|
136
|
-
Slash commands (bypass agent):
|
|
137
|
-
/cli-sonnet|opus|haiku|gemini|gemini-flash|gemini3
|
|
138
|
-
└─►
|
|
195
|
+
Slash commands (bypass agent, requireAuth=true):
|
|
196
|
+
/cli-sonnet|opus|haiku|gemini|gemini-flash|gemini3|codex|codex-mini
|
|
197
|
+
└─► saves current model → ~/.openclaw/cli-bridge-state.json
|
|
198
|
+
└─► openclaw models set <model> (~1s, atomic)
|
|
199
|
+
|
|
200
|
+
/cli-back
|
|
201
|
+
└─► reads ~/.openclaw/cli-bridge-state.json
|
|
202
|
+
└─► openclaw models set <previous>
|
|
203
|
+
|
|
204
|
+
/cli-test [model]
|
|
205
|
+
└─► HTTP POST → localhost:31337 (no global model change)
|
|
206
|
+
└─► reports response + latency
|
|
207
|
+
└─► NOTE: only tests the proxy — Codex models bypass the proxy
|
|
139
208
|
```
|
|
140
209
|
|
|
141
210
|
---
|
|
@@ -143,12 +212,14 @@ Slash commands (bypass agent):
|
|
|
143
212
|
## Known Issues & Fixes
|
|
144
213
|
|
|
145
214
|
### `spawn E2BIG` (fixed in v0.2.1)
|
|
215
|
+
**Symptom:** `CLI error for cli-claude/…: spawn E2BIG` after ~500+ messages.
|
|
216
|
+
**Cause:** Gateway injects large values into `process.env` at runtime. Spreading it into `spawn()` exceeds Linux's `ARG_MAX` (~2MB).
|
|
217
|
+
**Fix:** `buildMinimalEnv()` — only passes `HOME`, `PATH`, `USER`, and auth keys.
|
|
146
218
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
**
|
|
150
|
-
|
|
151
|
-
**Fix:** `buildMinimalEnv()` in `src/cli-runner.ts` — only passes `HOME`, `PATH`, `USER`, and auth keys to the subprocess. Immune to gateway runtime env size.
|
|
219
|
+
### Gemini agentic mode / hangs (fixed in v0.2.4)
|
|
220
|
+
**Symptom:** Gemini hangs, returns wrong answers, or says "directory does not exist".
|
|
221
|
+
**Cause:** `@file` syntax (`gemini -p @/tmp/xxx.txt`) triggers agentic mode — Gemini scans the working directory for project context and treats prompts as task instructions. Running from the workspace root makes this worse.
|
|
222
|
+
**Fix:** Stdin delivery (`gemini -p ""` with prompt via stdin) + `cwd=/tmp`. Same pattern as Claude.
|
|
152
223
|
|
|
153
224
|
---
|
|
154
225
|
|
|
@@ -156,30 +227,39 @@ Slash commands (bypass agent):
|
|
|
156
227
|
|
|
157
228
|
```bash
|
|
158
229
|
npm run typecheck # tsc --noEmit
|
|
159
|
-
npm test # vitest run
|
|
230
|
+
npm test # vitest run (5 unit tests for formatPrompt)
|
|
160
231
|
```
|
|
161
232
|
|
|
162
|
-
Test coverage: `test/cli-runner.test.ts` — unit tests for `formatPrompt` (truncation, system message handling, MAX_MSG_CHARS).
|
|
163
|
-
|
|
164
233
|
---
|
|
165
234
|
|
|
166
235
|
## Changelog
|
|
167
236
|
|
|
237
|
+
### v0.2.5
|
|
238
|
+
- **feat:** `/cli-codex` → `openai-codex/gpt-5.3-codex`
|
|
239
|
+
- **feat:** `/cli-codex-mini` → `openai-codex/gpt-5.1-codex-mini`
|
|
240
|
+
- Codex commands use the `openai-codex` provider (Codex CLI OAuth auth, direct OpenAI API — not the local proxy)
|
|
241
|
+
|
|
242
|
+
### v0.2.4
|
|
243
|
+
- **fix:** Gemini agentic mode — replaced `@file` with stdin delivery (`-p ""`) + `cwd=/tmp`
|
|
244
|
+
- **fix:** Filter `[WARN]` and `Loaded cached credentials` noise from Gemini stderr
|
|
245
|
+
- Added `RunCliOptions` interface with optional `cwd` field
|
|
246
|
+
|
|
247
|
+
### v0.2.3
|
|
248
|
+
- **feat:** `/cli-back` — restore previous model (state persisted in `~/.openclaw/cli-bridge-state.json`)
|
|
249
|
+
- **feat:** `/cli-test [model]` — one-shot proxy health check without changing active model
|
|
250
|
+
|
|
168
251
|
### v0.2.2
|
|
169
252
|
- **feat:** Phase 3 — `/cli-*` slash commands for instant model switching
|
|
170
|
-
- All 6 commands
|
|
171
|
-
- Calls `openclaw models set <model>` via `api.runtime.system.runCommandWithTimeout`
|
|
253
|
+
- All 6 model commands via `api.registerCommand` with `requireAuth: true`
|
|
172
254
|
|
|
173
255
|
### v0.2.1
|
|
174
|
-
- **fix:** `spawn E2BIG` —
|
|
175
|
-
- **feat:**
|
|
176
|
-
- Added Gemini 3 Pro model (`vllm/cli-gemini/gemini-3-pro`)
|
|
256
|
+
- **fix:** `spawn E2BIG` — `buildMinimalEnv()` instead of spreading full `process.env`
|
|
257
|
+
- **feat:** Unit tests (`test/cli-runner.test.ts`)
|
|
177
258
|
|
|
178
259
|
### v0.2.0
|
|
179
260
|
- **feat:** Phase 2 — local OpenAI-compatible proxy server
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
- Auto-patch of `openclaw.json` vllm provider config on first start
|
|
261
|
+
- Stdin prompt delivery, `MAX_MESSAGES=20` + `MAX_MSG_CHARS=4000` truncation
|
|
262
|
+
- Auto-patch of `openclaw.json` vllm provider config
|
|
183
263
|
|
|
184
264
|
### v0.1.x
|
|
185
265
|
- Phase 1: Codex CLI OAuth auth bridge
|
package/index.ts
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* are handled by the Gemini CLI and Claude Code CLI subprocesses.
|
|
10
10
|
*
|
|
11
11
|
* Phase 3 (slash commands): registers /cli-* commands for instant model switching.
|
|
12
|
-
* /cli-sonnet → vllm/cli-claude/claude-sonnet-4-6
|
|
13
|
-
* /cli-opus → vllm/cli-claude/claude-opus-4-6
|
|
14
|
-
* /cli-haiku → vllm/cli-claude/claude-haiku-4-5
|
|
15
|
-
* /cli-gemini → vllm/cli-gemini/gemini-2.5-pro
|
|
16
|
-
* /cli-gemini-flash → vllm/cli-gemini/gemini-2.5-flash
|
|
17
|
-
* /cli-gemini3 → vllm/cli-gemini/gemini-3-pro
|
|
12
|
+
* /cli-sonnet → vllm/cli-claude/claude-sonnet-4-6 (Claude Code CLI proxy)
|
|
13
|
+
* /cli-opus → vllm/cli-claude/claude-opus-4-6 (Claude Code CLI proxy)
|
|
14
|
+
* /cli-haiku → vllm/cli-claude/claude-haiku-4-5 (Claude Code CLI proxy)
|
|
15
|
+
* /cli-gemini → vllm/cli-gemini/gemini-2.5-pro (Gemini CLI proxy)
|
|
16
|
+
* /cli-gemini-flash → vllm/cli-gemini/gemini-2.5-flash (Gemini CLI proxy)
|
|
17
|
+
* /cli-gemini3 → vllm/cli-gemini/gemini-3-pro (Gemini CLI proxy)
|
|
18
|
+
* /cli-codex → openai-codex/gpt-5.3-codex (Codex CLI OAuth, direct API)
|
|
19
|
+
* /cli-codex-mini → openai-codex/gpt-5.1-codex-mini (Codex CLI OAuth, direct API)
|
|
18
20
|
* /cli-back → restore model that was active before last /cli-* switch
|
|
19
21
|
* /cli-test [model] → one-shot proxy health check (does NOT switch global model)
|
|
20
22
|
*
|
|
@@ -116,42 +118,57 @@ function readCurrentModel(): string | null {
|
|
|
116
118
|
// Phase 3: model command table
|
|
117
119
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
118
120
|
const CLI_MODEL_COMMANDS = [
|
|
121
|
+
// ── Claude (via local proxy → Claude Code CLI) ──────────────────────────────
|
|
119
122
|
{
|
|
120
123
|
name: "cli-sonnet",
|
|
121
124
|
model: "vllm/cli-claude/claude-sonnet-4-6",
|
|
122
|
-
description: "Switch to Claude Sonnet 4.6 (CLI
|
|
125
|
+
description: "Switch to Claude Sonnet 4.6 (Claude Code CLI via local proxy)",
|
|
123
126
|
label: "Claude Sonnet 4.6 (CLI)",
|
|
124
127
|
},
|
|
125
128
|
{
|
|
126
129
|
name: "cli-opus",
|
|
127
130
|
model: "vllm/cli-claude/claude-opus-4-6",
|
|
128
|
-
description: "Switch to Claude Opus 4.6 (CLI
|
|
131
|
+
description: "Switch to Claude Opus 4.6 (Claude Code CLI via local proxy)",
|
|
129
132
|
label: "Claude Opus 4.6 (CLI)",
|
|
130
133
|
},
|
|
131
134
|
{
|
|
132
135
|
name: "cli-haiku",
|
|
133
136
|
model: "vllm/cli-claude/claude-haiku-4-5",
|
|
134
|
-
description: "Switch to Claude Haiku 4.5 (CLI
|
|
137
|
+
description: "Switch to Claude Haiku 4.5 (Claude Code CLI via local proxy)",
|
|
135
138
|
label: "Claude Haiku 4.5 (CLI)",
|
|
136
139
|
},
|
|
140
|
+
// ── Gemini (via local proxy → Gemini CLI) ───────────────────────────────────
|
|
137
141
|
{
|
|
138
142
|
name: "cli-gemini",
|
|
139
143
|
model: "vllm/cli-gemini/gemini-2.5-pro",
|
|
140
|
-
description: "Switch to Gemini 2.5 Pro (CLI
|
|
144
|
+
description: "Switch to Gemini 2.5 Pro (Gemini CLI via local proxy)",
|
|
141
145
|
label: "Gemini 2.5 Pro (CLI)",
|
|
142
146
|
},
|
|
143
147
|
{
|
|
144
148
|
name: "cli-gemini-flash",
|
|
145
149
|
model: "vllm/cli-gemini/gemini-2.5-flash",
|
|
146
|
-
description: "Switch to Gemini 2.5 Flash (CLI
|
|
150
|
+
description: "Switch to Gemini 2.5 Flash (Gemini CLI via local proxy)",
|
|
147
151
|
label: "Gemini 2.5 Flash (CLI)",
|
|
148
152
|
},
|
|
149
153
|
{
|
|
150
154
|
name: "cli-gemini3",
|
|
151
155
|
model: "vllm/cli-gemini/gemini-3-pro",
|
|
152
|
-
description: "Switch to Gemini 3 Pro (CLI
|
|
156
|
+
description: "Switch to Gemini 3 Pro (Gemini CLI via local proxy)",
|
|
153
157
|
label: "Gemini 3 Pro (CLI)",
|
|
154
158
|
},
|
|
159
|
+
// ── Codex (via openai-codex provider — Codex CLI OAuth auth, direct API) ────
|
|
160
|
+
{
|
|
161
|
+
name: "cli-codex",
|
|
162
|
+
model: "openai-codex/gpt-5.3-codex",
|
|
163
|
+
description: "Switch to GPT-5.3 Codex (openai-codex provider, Codex CLI auth)",
|
|
164
|
+
label: "GPT-5.3 Codex",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "cli-codex-mini",
|
|
168
|
+
model: "openai-codex/gpt-5.1-codex-mini",
|
|
169
|
+
description: "Switch to GPT-5.1 Codex Mini (openai-codex provider, Codex CLI auth)",
|
|
170
|
+
label: "GPT-5.1 Codex Mini",
|
|
171
|
+
},
|
|
155
172
|
] as const;
|
|
156
173
|
|
|
157
174
|
/** Default model used by /cli-test when no arg is given */
|
|
@@ -260,7 +277,7 @@ function proxyTestRequest(
|
|
|
260
277
|
const plugin = {
|
|
261
278
|
id: "openclaw-cli-bridge-elvatis",
|
|
262
279
|
name: "OpenClaw CLI Bridge",
|
|
263
|
-
version: "0.2.
|
|
280
|
+
version: "0.2.5",
|
|
264
281
|
description:
|
|
265
282
|
"Phase 1: openai-codex auth bridge. " +
|
|
266
283
|
"Phase 2: HTTP proxy for gemini/claude CLIs. " +
|
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.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": "0.2.
|
|
3
|
+
"version": "0.2.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
|
"scripts": {
|
package/src/cli-runner.ts
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
* Spawns CLI subprocesses (gemini, claude) and captures their output.
|
|
5
5
|
* Input: OpenAI-format messages → formatted prompt string → CLI stdin.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Both Gemini and Claude receive the prompt via stdin to avoid:
|
|
8
|
+
* - E2BIG (arg list too long) for large conversation histories
|
|
9
|
+
* - Gemini agentic mode (triggered by @file syntax + workspace cwd)
|
|
10
|
+
*
|
|
11
|
+
* Gemini is always spawned with cwd = tmpdir() so it doesn't scan the
|
|
12
|
+
* workspace and enter agentic mode.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { spawn } from "node:child_process";
|
|
12
|
-
import {
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { tmpdir, homedir } from "node:os";
|
|
16
17
|
|
|
17
18
|
/** Max messages to include in the prompt sent to the CLI. */
|
|
18
19
|
const MAX_MESSAGES = 20;
|
|
@@ -31,7 +32,7 @@ export interface ChatMessage {
|
|
|
31
32
|
/**
|
|
32
33
|
* Convert OpenAI messages to a single flat prompt string.
|
|
33
34
|
* Truncates to MAX_MESSAGES (keeping the most recent) and MAX_MSG_CHARS per
|
|
34
|
-
* message to avoid
|
|
35
|
+
* message to avoid oversized payloads.
|
|
35
36
|
*/
|
|
36
37
|
export function formatPrompt(messages: ChatMessage[]): string {
|
|
37
38
|
if (messages.length === 0) return "";
|
|
@@ -42,7 +43,7 @@ export function formatPrompt(messages: ChatMessage[]): string {
|
|
|
42
43
|
const recent = nonSystem.slice(-MAX_MESSAGES);
|
|
43
44
|
const truncated = system ? [system, ...recent] : recent;
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
+
// Single short user message — send bare (no wrapping needed)
|
|
46
47
|
if (truncated.length === 1 && truncated[0].role === "user") {
|
|
47
48
|
return truncateContent(truncated[0].content);
|
|
48
49
|
}
|
|
@@ -51,13 +52,10 @@ export function formatPrompt(messages: ChatMessage[]): string {
|
|
|
51
52
|
.map((m) => {
|
|
52
53
|
const content = truncateContent(m.content);
|
|
53
54
|
switch (m.role) {
|
|
54
|
-
case "system":
|
|
55
|
-
|
|
56
|
-
case "assistant":
|
|
57
|
-
return `[Assistant]\n${content}`;
|
|
55
|
+
case "system": return `[System]\n${content}`;
|
|
56
|
+
case "assistant": return `[Assistant]\n${content}`;
|
|
58
57
|
case "user":
|
|
59
|
-
default:
|
|
60
|
-
return `[User]\n${content}`;
|
|
58
|
+
default: return `[User]\n${content}`;
|
|
61
59
|
}
|
|
62
60
|
})
|
|
63
61
|
.join("\n\n");
|
|
@@ -69,40 +67,26 @@ function truncateContent(s: string): string {
|
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
72
|
-
//
|
|
70
|
+
// Minimal environment for spawned subprocesses
|
|
73
71
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
74
72
|
|
|
75
|
-
export interface CliRunResult {
|
|
76
|
-
stdout: string;
|
|
77
|
-
stderr: string;
|
|
78
|
-
exitCode: number;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
73
|
/**
|
|
82
74
|
* Build a minimal, safe environment for spawning CLI subprocesses.
|
|
83
75
|
*
|
|
84
|
-
* WHY: The OpenClaw gateway
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* ARG_MAX (~2 MB on Linux), causing "spawn E2BIG". Using only the vars that
|
|
76
|
+
* WHY: The OpenClaw gateway modifies process.env at runtime (OPENCLAW_* vars,
|
|
77
|
+
* session context, etc.). Spreading the full process.env into spawn() can push
|
|
78
|
+
* argv+envp over ARG_MAX (~2 MB on Linux) → "spawn E2BIG". Only passing what
|
|
88
79
|
* the CLI tools actually need keeps us well under the limit regardless of
|
|
89
|
-
*
|
|
80
|
+
* gateway runtime state.
|
|
90
81
|
*/
|
|
91
82
|
function buildMinimalEnv(): Record<string, string> {
|
|
92
|
-
const pick = (key: string)
|
|
93
|
-
|
|
94
|
-
const env: Record<string, string> = {
|
|
95
|
-
NO_COLOR: "1",
|
|
96
|
-
TERM: "dumb",
|
|
97
|
-
};
|
|
83
|
+
const pick = (key: string) => process.env[key];
|
|
84
|
+
const env: Record<string, string> = { NO_COLOR: "1", TERM: "dumb" };
|
|
98
85
|
|
|
99
|
-
// Essential path/identity vars — always include when present.
|
|
100
86
|
for (const key of ["HOME", "PATH", "USER", "LOGNAME", "SHELL", "TMPDIR", "TMP", "TEMP"]) {
|
|
101
87
|
const v = pick(key);
|
|
102
88
|
if (v) env[key] = v;
|
|
103
89
|
}
|
|
104
|
-
|
|
105
|
-
// Allow google-auth / claude auth paths to be inherited.
|
|
106
90
|
for (const key of [
|
|
107
91
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
108
92
|
"ANTHROPIC_API_KEY",
|
|
@@ -120,37 +104,56 @@ function buildMinimalEnv(): Record<string, string> {
|
|
|
120
104
|
return env;
|
|
121
105
|
}
|
|
122
106
|
|
|
107
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
// Core subprocess runner
|
|
109
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export interface CliRunResult {
|
|
112
|
+
stdout: string;
|
|
113
|
+
stderr: string;
|
|
114
|
+
exitCode: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface RunCliOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Working directory for the subprocess.
|
|
120
|
+
* Defaults to homedir() — a neutral dir that won't trigger agentic context scanning.
|
|
121
|
+
*/
|
|
122
|
+
cwd?: string;
|
|
123
|
+
timeoutMs?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
123
126
|
/**
|
|
124
|
-
* Spawn a CLI and deliver the prompt via stdin
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
+
* Spawn a CLI and deliver the prompt via stdin.
|
|
128
|
+
*
|
|
129
|
+
* cwd defaults to homedir() so CLIs that scan the working directory for
|
|
130
|
+
* project context (like Gemini) don't accidentally enter agentic mode.
|
|
127
131
|
*/
|
|
128
132
|
export function runCli(
|
|
129
133
|
cmd: string,
|
|
130
134
|
args: string[],
|
|
131
135
|
prompt: string,
|
|
132
|
-
timeoutMs = 120_000
|
|
136
|
+
timeoutMs = 120_000,
|
|
137
|
+
opts: RunCliOptions = {}
|
|
133
138
|
): Promise<CliRunResult> {
|
|
139
|
+
const cwd = opts.cwd ?? homedir();
|
|
140
|
+
|
|
134
141
|
return new Promise((resolve, reject) => {
|
|
135
142
|
const proc = spawn(cmd, args, {
|
|
136
143
|
timeout: timeoutMs,
|
|
137
144
|
env: buildMinimalEnv(),
|
|
145
|
+
cwd,
|
|
138
146
|
});
|
|
139
147
|
|
|
140
148
|
let stdout = "";
|
|
141
149
|
let stderr = "";
|
|
142
150
|
|
|
143
|
-
// Write prompt to stdin then close — prevents the CLI from waiting for more input.
|
|
144
151
|
proc.stdin.write(prompt, "utf8", () => {
|
|
145
152
|
proc.stdin.end();
|
|
146
153
|
});
|
|
147
154
|
|
|
148
|
-
proc.stdout.on("data", (d: Buffer) => {
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
proc.stderr.on("data", (d: Buffer) => {
|
|
152
|
-
stderr += d.toString();
|
|
153
|
-
});
|
|
155
|
+
proc.stdout.on("data", (d: Buffer) => { stdout += d.toString(); });
|
|
156
|
+
proc.stderr.on("data", (d: Buffer) => { stderr += d.toString(); });
|
|
154
157
|
|
|
155
158
|
proc.on("close", (code) => {
|
|
156
159
|
resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code ?? 0 });
|
|
@@ -167,8 +170,19 @@ export function runCli(
|
|
|
167
170
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
168
171
|
|
|
169
172
|
/**
|
|
170
|
-
* Run
|
|
171
|
-
*
|
|
173
|
+
* Run Gemini CLI in headless mode with prompt delivered via stdin.
|
|
174
|
+
*
|
|
175
|
+
* WHY stdin (not @file):
|
|
176
|
+
* The @file syntax (`gemini -p @/tmp/xxx.txt`) triggers Gemini's agentic
|
|
177
|
+
* mode — it scans the current working directory for project context and
|
|
178
|
+
* interprets the prompt as a task instruction, not a Q&A. This causes hangs,
|
|
179
|
+
* wrong answers, and "directory does not exist" errors when run from a
|
|
180
|
+
* project workspace.
|
|
181
|
+
*
|
|
182
|
+
* Gemini CLI: -p "" triggers headless mode; stdin content is the actual prompt
|
|
183
|
+
* (per Gemini docs: "prompt is appended to input on stdin (if any)").
|
|
184
|
+
*
|
|
185
|
+
* cwd = tmpdir() — neutral empty-ish dir, prevents workspace context scanning.
|
|
172
186
|
*/
|
|
173
187
|
export async function runGemini(
|
|
174
188
|
prompt: string,
|
|
@@ -176,24 +190,22 @@ export async function runGemini(
|
|
|
176
190
|
timeoutMs: number
|
|
177
191
|
): Promise<string> {
|
|
178
192
|
const model = stripPrefix(modelId);
|
|
179
|
-
//
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return result.stdout || result.stderr;
|
|
194
|
-
} finally {
|
|
195
|
-
try { unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
193
|
+
// -p "" = headless mode trigger; actual prompt arrives via stdin
|
|
194
|
+
const args = ["-m", model, "-p", ""];
|
|
195
|
+
const result = await runCli("gemini", args, prompt, timeoutMs, { cwd: tmpdir() });
|
|
196
|
+
|
|
197
|
+
// Filter out [WARN] lines from stderr (Gemini emits noisy permission warnings)
|
|
198
|
+
const cleanStderr = result.stderr
|
|
199
|
+
.split("\n")
|
|
200
|
+
.filter((l) => !l.startsWith("[WARN]") && !l.startsWith("Loaded cached"))
|
|
201
|
+
.join("\n")
|
|
202
|
+
.trim();
|
|
203
|
+
|
|
204
|
+
if (result.exitCode !== 0 && result.stdout.length === 0) {
|
|
205
|
+
throw new Error(`gemini exited ${result.exitCode}: ${cleanStderr || "(no output)"}`);
|
|
196
206
|
}
|
|
207
|
+
|
|
208
|
+
return result.stdout || cleanStderr;
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -201,7 +213,7 @@ export async function runGemini(
|
|
|
201
213
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
202
214
|
|
|
203
215
|
/**
|
|
204
|
-
* Run
|
|
216
|
+
* Run Claude Code CLI in headless mode with prompt delivered via stdin.
|
|
205
217
|
* Strips the model prefix ("cli-claude/claude-opus-4-6" → "claude-opus-4-6").
|
|
206
218
|
*/
|
|
207
219
|
export async function runClaude(
|
|
@@ -210,24 +222,17 @@ export async function runClaude(
|
|
|
210
222
|
timeoutMs: number
|
|
211
223
|
): Promise<string> {
|
|
212
224
|
const model = stripPrefix(modelId);
|
|
213
|
-
// No prompt argument — deliver via stdin to avoid E2BIG
|
|
214
225
|
const args = [
|
|
215
226
|
"-p",
|
|
216
|
-
"--output-format",
|
|
217
|
-
"
|
|
218
|
-
"--
|
|
219
|
-
"
|
|
220
|
-
"--tools",
|
|
221
|
-
"",
|
|
222
|
-
"--model",
|
|
223
|
-
model,
|
|
227
|
+
"--output-format", "text",
|
|
228
|
+
"--permission-mode", "plan",
|
|
229
|
+
"--tools", "",
|
|
230
|
+
"--model", model,
|
|
224
231
|
];
|
|
225
232
|
const result = await runCli("claude", args, prompt, timeoutMs);
|
|
226
233
|
|
|
227
234
|
if (result.exitCode !== 0 && result.stdout.length === 0) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
`claude exited ${result.exitCode}: ${result.stderr || "(no output)"}`
|
|
230
|
-
);
|
|
235
|
+
throw new Error(`claude exited ${result.exitCode}: ${result.stderr || "(no output)"}`);
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
return result.stdout;
|
|
@@ -238,8 +243,7 @@ export async function runClaude(
|
|
|
238
243
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
239
244
|
|
|
240
245
|
/**
|
|
241
|
-
* Route a chat completion
|
|
242
|
-
* Model naming convention:
|
|
246
|
+
* Route a chat completion to the correct CLI based on model prefix.
|
|
243
247
|
* cli-gemini/<id> → gemini CLI
|
|
244
248
|
* cli-claude/<id> → claude CLI
|
|
245
249
|
*/
|
|
@@ -250,17 +254,11 @@ export async function routeToCliRunner(
|
|
|
250
254
|
): Promise<string> {
|
|
251
255
|
const prompt = formatPrompt(messages);
|
|
252
256
|
|
|
253
|
-
if (model.startsWith("cli-gemini/"))
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (model.startsWith("cli-claude/")) {
|
|
258
|
-
return runClaude(prompt, model, timeoutMs);
|
|
259
|
-
}
|
|
257
|
+
if (model.startsWith("cli-gemini/")) return runGemini(prompt, model, timeoutMs);
|
|
258
|
+
if (model.startsWith("cli-claude/")) return runClaude(prompt, model, timeoutMs);
|
|
260
259
|
|
|
261
260
|
throw new Error(
|
|
262
|
-
`Unknown CLI bridge model: "${model}".
|
|
263
|
-
`Use "cli-gemini/<model>" or "cli-claude/<model>".`
|
|
261
|
+
`Unknown CLI bridge model: "${model}". Use "cli-gemini/<model>" or "cli-claude/<model>".`
|
|
264
262
|
);
|
|
265
263
|
}
|
|
266
264
|
|
|
@@ -268,7 +266,6 @@ export async function routeToCliRunner(
|
|
|
268
266
|
// Helpers
|
|
269
267
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
270
268
|
|
|
271
|
-
/** Strip the "cli-gemini/" or "cli-claude/" prefix from a model ID. */
|
|
272
269
|
function stripPrefix(modelId: string): string {
|
|
273
270
|
const slash = modelId.indexOf("/");
|
|
274
271
|
return slash === -1 ? modelId : modelId.slice(slash + 1);
|