@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 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.3.1`
5
+ **Current version:** `3.4.0`
6
6
 
7
7
  ---
8
8
 
package/SKILL.md CHANGED
@@ -68,4 +68,4 @@ On gateway restart, if any session has expired, a **WhatsApp alert** is sent aut
68
68
 
69
69
  See `README.md` for full configuration reference and architecture diagram.
70
70
 
71
- **Version:** 3.3.1
71
+ **Version:** 3.4.0
@@ -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.3.1",
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.1",
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
- // Don't invalidate session on timeout it's still valid, just slow
686
- recordSessionSuccess(model); // keep session alive
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
- const prompt = formatPrompt(messages, toolCount);
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