@elvatis_com/openclaw-cli-bridge-elvatis 3.3.1 → 3.4.1
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/CLAUDE.md +4 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cli-runner.ts +95 -7
- package/src/config.ts +3 -0
package/CLAUDE.md
CHANGED
|
@@ -30,6 +30,8 @@ OpenClaw Gateway ──(HTTP)──> proxy-server.ts ──(spawn)──> claude
|
|
|
30
30
|
- **Smart fallback** — Sonnet tries first (better tool selection), 30s stale timeout kills it fast, Haiku takes over (~10s, reliable but picks wrong tools sometimes)
|
|
31
31
|
- **Compact tool schema** — when >8 tools, only send name+params (skip descriptions/full JSON schema), cuts prompt ~60%
|
|
32
32
|
- **Exit 143 = our SIGTERM** — not OOM, not crash. The bridge's timeout/stale-output detector sends SIGTERM, Claude CLI exits 143
|
|
33
|
+
- **Consecutive timeout rotation** — after 3 timeouts in a row on the same session, auto-expire it and create a fresh one. Prevents poisoned sessions from blocking all requests
|
|
34
|
+
- **Workspace project auto-detection** — scans `~/.openclaw/workspace/` for project directories; when the prompt contains an exact match of a project name, auto-sets `workdir` and injects `[Context: Working directory is ...]` into the prompt
|
|
33
35
|
|
|
34
36
|
## Build & Test
|
|
35
37
|
|
|
@@ -69,6 +71,8 @@ All magic numbers live here. Key values:
|
|
|
69
71
|
| `TOOL_HEAVY_THRESHOLD` | 10 | Reduce MAX_MESSAGES from 20 to 12 when tools exceed this |
|
|
70
72
|
| `COMPACT_TOOL_THRESHOLD` | 8 | Switch to compact tool schema (name+params only) |
|
|
71
73
|
| `TOOL_ROUTING_THRESHOLD` | 8 | (in proxy-server) Was used for Haiku routing, now Sonnet-first with fast fallback |
|
|
74
|
+
| `CONSECUTIVE_TIMEOUT_LIMIT` | 3 | (in cli-runner) Auto-expire session after N consecutive timeouts |
|
|
75
|
+
| `WORKSPACE_DIR` | `~/.openclaw/workspace` | Project directory scanned for auto-detection |
|
|
72
76
|
|
|
73
77
|
## Tool Protocol (src/tool-protocol.ts)
|
|
74
78
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code, OpenCode, Pi) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
|
|
4
4
|
|
|
5
|
-
**Current version:** `3.
|
|
5
|
+
**Current version:** `3.4.1`
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/SKILL.md
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-cli-bridge-elvatis",
|
|
3
3
|
"slug": "openclaw-cli-bridge-elvatis",
|
|
4
4
|
"name": "OpenClaw CLI Bridge",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.4.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
|
|
8
8
|
"providers": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elvatis_com/openclaw-cli-bridge-elvatis",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.1",
|
|
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/cli-runner.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { spawn, execSync } from "node:child_process";
|
|
20
20
|
import { tmpdir, homedir } from "node:os";
|
|
21
|
-
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync } from "node:fs";
|
|
21
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
23
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
24
24
|
import { ensureClaudeToken, refreshClaudeToken } from "./claude-auth.js";
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
TIMEOUT_GRACE_MS,
|
|
38
38
|
MEDIA_TMP_DIR,
|
|
39
39
|
STALE_OUTPUT_TIMEOUT_MS,
|
|
40
|
+
WORKSPACE_DIR,
|
|
40
41
|
} from "./config.js";
|
|
41
42
|
import { debugLog } from "./debug-log.js";
|
|
42
43
|
|
|
@@ -567,8 +568,11 @@ interface CliSessionEntry {
|
|
|
567
568
|
createdAt: number;
|
|
568
569
|
lastUsedAt: number;
|
|
569
570
|
requestCount: number;
|
|
571
|
+
consecutiveTimeouts: number;
|
|
570
572
|
}
|
|
571
573
|
|
|
574
|
+
const CONSECUTIVE_TIMEOUT_LIMIT = 3;
|
|
575
|
+
|
|
572
576
|
const cliSessions = new Map<string, CliSessionEntry>();
|
|
573
577
|
let sessionsLoaded = false;
|
|
574
578
|
|
|
@@ -609,6 +613,7 @@ function getOrCreateSession(provider: string, model: string): CliSessionEntry {
|
|
|
609
613
|
createdAt: Date.now(),
|
|
610
614
|
lastUsedAt: Date.now(),
|
|
611
615
|
requestCount: 0,
|
|
616
|
+
consecutiveTimeouts: 0,
|
|
612
617
|
};
|
|
613
618
|
cliSessions.set(model, entry);
|
|
614
619
|
saveCliSessions();
|
|
@@ -617,7 +622,21 @@ function getOrCreateSession(provider: string, model: string): CliSessionEntry {
|
|
|
617
622
|
|
|
618
623
|
function recordSessionSuccess(model: string): void {
|
|
619
624
|
const s = cliSessions.get(model);
|
|
620
|
-
if (s) { s.requestCount++; s.lastUsedAt = Date.now(); saveCliSessions(); }
|
|
625
|
+
if (s) { s.requestCount++; s.lastUsedAt = Date.now(); s.consecutiveTimeouts = 0; saveCliSessions(); }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function recordSessionTimeout(model: string): void {
|
|
629
|
+
const s = cliSessions.get(model);
|
|
630
|
+
if (!s) return;
|
|
631
|
+
s.consecutiveTimeouts++;
|
|
632
|
+
s.lastUsedAt = Date.now();
|
|
633
|
+
if (s.consecutiveTimeouts >= CONSECUTIVE_TIMEOUT_LIMIT) {
|
|
634
|
+
debugLog("SESSION", `${s.provider} session ${s.sessionId.slice(0, 8)} expired`, {
|
|
635
|
+
reason: "consecutive_timeouts", consecutiveTimeouts: s.consecutiveTimeouts, requestCount: s.requestCount,
|
|
636
|
+
});
|
|
637
|
+
cliSessions.delete(model);
|
|
638
|
+
}
|
|
639
|
+
saveCliSessions();
|
|
621
640
|
}
|
|
622
641
|
|
|
623
642
|
function invalidateSession(model: string): void {
|
|
@@ -682,15 +701,15 @@ export async function runClaude(
|
|
|
682
701
|
|
|
683
702
|
// Session failed — check if it's a timeout or auth issue
|
|
684
703
|
if (result.timedOut) {
|
|
685
|
-
//
|
|
686
|
-
|
|
704
|
+
// Track consecutive timeouts — after 3 in a row, expire the session
|
|
705
|
+
recordSessionTimeout(model);
|
|
687
706
|
throw new Error(`claude exited ${result.exitCode}: ${annotateExitError(result.exitCode, result.stderr, true, modelId)}`);
|
|
688
707
|
}
|
|
689
708
|
|
|
690
709
|
const stderr = result.stderr || "(no output)";
|
|
691
710
|
|
|
692
|
-
// Session might be corrupted or
|
|
693
|
-
if (stderr.includes("session") || stderr.includes("resume") || stderr.includes("not found")) {
|
|
711
|
+
// Session might be corrupted, expired, or locked by a zombie process — invalidate and retry
|
|
712
|
+
if (stderr.includes("session") || stderr.includes("resume") || stderr.includes("not found") || stderr.includes("already in use")) {
|
|
694
713
|
debugLog("CLAUDE", `session ${session.sessionId.slice(0, 8)} invalid, creating fresh`, { error: stderr.slice(0, 100) });
|
|
695
714
|
invalidateSession(model);
|
|
696
715
|
// Retry once with a fresh session
|
|
@@ -705,6 +724,8 @@ export async function runClaude(
|
|
|
705
724
|
recordSessionSuccess(model);
|
|
706
725
|
return retry.stdout;
|
|
707
726
|
}
|
|
727
|
+
// Retry also failed — invalidate the fresh session so the next request doesn't reuse it
|
|
728
|
+
invalidateSession(model);
|
|
708
729
|
throw new Error(`claude exited ${retry.exitCode}: ${annotateExitError(retry.exitCode, retry.stderr || "(no output)", false, modelId)}`);
|
|
709
730
|
}
|
|
710
731
|
|
|
@@ -920,6 +941,63 @@ export interface RouteOptions {
|
|
|
920
941
|
log?: (msg: string) => void;
|
|
921
942
|
}
|
|
922
943
|
|
|
944
|
+
// ── Workspace project detection ──────────────────────────────────────────────
|
|
945
|
+
// Scans WORKSPACE_DIR for project directories. When the user's prompt contains
|
|
946
|
+
// an exact match of a project name, auto-sets workdir and injects context.
|
|
947
|
+
|
|
948
|
+
let _workspaceProjects: string[] | null = null;
|
|
949
|
+
let _workspaceProjectsRefreshedAt = 0;
|
|
950
|
+
const WORKSPACE_CACHE_TTL = 60_000; // refresh project list every 60s
|
|
951
|
+
|
|
952
|
+
function getWorkspaceProjects(): string[] {
|
|
953
|
+
const now = Date.now();
|
|
954
|
+
if (_workspaceProjects && (now - _workspaceProjectsRefreshedAt) < WORKSPACE_CACHE_TTL) {
|
|
955
|
+
return _workspaceProjects;
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
// Find all .openclaw/workspace dirs — default location + any custom ones
|
|
959
|
+
const candidates = [WORKSPACE_DIR];
|
|
960
|
+
_workspaceProjects = [];
|
|
961
|
+
for (const wsDir of candidates) {
|
|
962
|
+
if (!existsSync(wsDir)) continue;
|
|
963
|
+
const entries = readdirSync(wsDir);
|
|
964
|
+
for (const name of entries) {
|
|
965
|
+
try {
|
|
966
|
+
if (statSync(join(wsDir, name)).isDirectory()) {
|
|
967
|
+
_workspaceProjects.push(name);
|
|
968
|
+
}
|
|
969
|
+
} catch { /* skip unreadable entries */ }
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
_workspaceProjectsRefreshedAt = now;
|
|
973
|
+
} catch {
|
|
974
|
+
_workspaceProjects = [];
|
|
975
|
+
}
|
|
976
|
+
return _workspaceProjects;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function detectProjectFromPrompt(prompt: string): { name: string; path: string } | null {
|
|
980
|
+
const projects = getWorkspaceProjects();
|
|
981
|
+
if (!projects.length) return null;
|
|
982
|
+
|
|
983
|
+
// Sort by name length descending — match longest project name first
|
|
984
|
+
// (e.g. "openclaw-cli-bridge-elvatis" before "openclaw-cli-bridge")
|
|
985
|
+
const sorted = [...projects].sort((a, b) => b.length - a.length);
|
|
986
|
+
|
|
987
|
+
for (const name of sorted) {
|
|
988
|
+
// Case-insensitive exact word match — the project name must appear as a
|
|
989
|
+
// distinct token in the prompt (not a substring of a longer word)
|
|
990
|
+
const regex = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
991
|
+
if (regex.test(prompt)) {
|
|
992
|
+
const projectPath = join(WORKSPACE_DIR, name);
|
|
993
|
+
if (existsSync(projectPath)) {
|
|
994
|
+
return { name, path: projectPath };
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
923
1001
|
/**
|
|
924
1002
|
* Route a chat completion to the correct CLI based on model prefix.
|
|
925
1003
|
* cli-gemini/<id> → gemini CLI
|
|
@@ -941,9 +1019,19 @@ export async function routeToCliRunner(
|
|
|
941
1019
|
opts: RouteOptions = {}
|
|
942
1020
|
): Promise<CliToolResult> {
|
|
943
1021
|
const toolCount = opts.tools?.length ?? 0;
|
|
944
|
-
|
|
1022
|
+
let prompt = formatPrompt(messages, toolCount);
|
|
945
1023
|
const hasTools = toolCount > 0;
|
|
946
1024
|
|
|
1025
|
+
// Auto-detect project from prompt and set workdir + inject context
|
|
1026
|
+
if (!opts.workdir) {
|
|
1027
|
+
const detected = detectProjectFromPrompt(prompt);
|
|
1028
|
+
if (detected) {
|
|
1029
|
+
opts = { ...opts, workdir: detected.path };
|
|
1030
|
+
prompt = `[Context: Working directory is ${detected.path}]\n\n${prompt}`;
|
|
1031
|
+
debugLog("WORKSPACE", `auto-detected project "${detected.name}"`, { path: detected.path });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
947
1035
|
// Strip "vllm/" prefix if present — OpenClaw sends the full provider path
|
|
948
1036
|
// (e.g. "vllm/cli-claude/claude-sonnet-4-6") but the router only needs the
|
|
949
1037
|
// "cli-<type>/<model>" portion.
|
package/src/config.ts
CHANGED
|
@@ -162,6 +162,9 @@ export const DEFAULT_MODEL_FALLBACKS: Record<string, string[]> = {
|
|
|
162
162
|
/** Base directory for all CLI bridge state files. */
|
|
163
163
|
export const OPENCLAW_DIR = join(homedir(), ".openclaw");
|
|
164
164
|
|
|
165
|
+
/** Workspace directory containing all projects. */
|
|
166
|
+
export const WORKSPACE_DIR = join(OPENCLAW_DIR, "workspace");
|
|
167
|
+
|
|
165
168
|
/** State file — persists the model active before the last /cli-* switch. */
|
|
166
169
|
export const STATE_FILE = join(OPENCLAW_DIR, "cli-bridge-state.json");
|
|
167
170
|
|