@elvatis_com/openclaw-cli-bridge-elvatis 3.3.1 → 3.4.0
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 +91 -5
- 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.0`
|
|
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.0",
|
|
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.0",
|
|
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,8 +701,8 @@ 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
|
|
|
@@ -920,6 +939,63 @@ export interface RouteOptions {
|
|
|
920
939
|
log?: (msg: string) => void;
|
|
921
940
|
}
|
|
922
941
|
|
|
942
|
+
// ── Workspace project detection ──────────────────────────────────────────────
|
|
943
|
+
// Scans WORKSPACE_DIR for project directories. When the user's prompt contains
|
|
944
|
+
// an exact match of a project name, auto-sets workdir and injects context.
|
|
945
|
+
|
|
946
|
+
let _workspaceProjects: string[] | null = null;
|
|
947
|
+
let _workspaceProjectsRefreshedAt = 0;
|
|
948
|
+
const WORKSPACE_CACHE_TTL = 60_000; // refresh project list every 60s
|
|
949
|
+
|
|
950
|
+
function getWorkspaceProjects(): string[] {
|
|
951
|
+
const now = Date.now();
|
|
952
|
+
if (_workspaceProjects && (now - _workspaceProjectsRefreshedAt) < WORKSPACE_CACHE_TTL) {
|
|
953
|
+
return _workspaceProjects;
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
// Find all .openclaw/workspace dirs — default location + any custom ones
|
|
957
|
+
const candidates = [WORKSPACE_DIR];
|
|
958
|
+
_workspaceProjects = [];
|
|
959
|
+
for (const wsDir of candidates) {
|
|
960
|
+
if (!existsSync(wsDir)) continue;
|
|
961
|
+
const entries = readdirSync(wsDir);
|
|
962
|
+
for (const name of entries) {
|
|
963
|
+
try {
|
|
964
|
+
if (statSync(join(wsDir, name)).isDirectory()) {
|
|
965
|
+
_workspaceProjects.push(name);
|
|
966
|
+
}
|
|
967
|
+
} catch { /* skip unreadable entries */ }
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
_workspaceProjectsRefreshedAt = now;
|
|
971
|
+
} catch {
|
|
972
|
+
_workspaceProjects = [];
|
|
973
|
+
}
|
|
974
|
+
return _workspaceProjects;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function detectProjectFromPrompt(prompt: string): { name: string; path: string } | null {
|
|
978
|
+
const projects = getWorkspaceProjects();
|
|
979
|
+
if (!projects.length) return null;
|
|
980
|
+
|
|
981
|
+
// Sort by name length descending — match longest project name first
|
|
982
|
+
// (e.g. "openclaw-cli-bridge-elvatis" before "openclaw-cli-bridge")
|
|
983
|
+
const sorted = [...projects].sort((a, b) => b.length - a.length);
|
|
984
|
+
|
|
985
|
+
for (const name of sorted) {
|
|
986
|
+
// Case-insensitive exact word match — the project name must appear as a
|
|
987
|
+
// distinct token in the prompt (not a substring of a longer word)
|
|
988
|
+
const regex = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
989
|
+
if (regex.test(prompt)) {
|
|
990
|
+
const projectPath = join(WORKSPACE_DIR, name);
|
|
991
|
+
if (existsSync(projectPath)) {
|
|
992
|
+
return { name, path: projectPath };
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
|
|
923
999
|
/**
|
|
924
1000
|
* Route a chat completion to the correct CLI based on model prefix.
|
|
925
1001
|
* cli-gemini/<id> → gemini CLI
|
|
@@ -941,9 +1017,19 @@ export async function routeToCliRunner(
|
|
|
941
1017
|
opts: RouteOptions = {}
|
|
942
1018
|
): Promise<CliToolResult> {
|
|
943
1019
|
const toolCount = opts.tools?.length ?? 0;
|
|
944
|
-
|
|
1020
|
+
let prompt = formatPrompt(messages, toolCount);
|
|
945
1021
|
const hasTools = toolCount > 0;
|
|
946
1022
|
|
|
1023
|
+
// Auto-detect project from prompt and set workdir + inject context
|
|
1024
|
+
if (!opts.workdir) {
|
|
1025
|
+
const detected = detectProjectFromPrompt(prompt);
|
|
1026
|
+
if (detected) {
|
|
1027
|
+
opts = { ...opts, workdir: detected.path };
|
|
1028
|
+
prompt = `[Context: Working directory is ${detected.path}]\n\n${prompt}`;
|
|
1029
|
+
debugLog("WORKSPACE", `auto-detected project "${detected.name}"`, { path: detected.path });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
947
1033
|
// Strip "vllm/" prefix if present — OpenClaw sends the full provider path
|
|
948
1034
|
// (e.g. "vllm/cli-claude/claude-sonnet-4-6") but the router only needs the
|
|
949
1035
|
// "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
|
|