@elvatis_com/openclaw-cli-bridge-elvatis 3.1.2 → 3.2.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/README.md +6 -1
- package/SKILL.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cli-runner.ts +134 -34
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.2.0`
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -406,6 +406,11 @@ npm run ci # lint + typecheck + test
|
|
|
406
406
|
|
|
407
407
|
## Changelog
|
|
408
408
|
|
|
409
|
+
### v3.2.0
|
|
410
|
+
- **feat:** Claude session resume — persistent sessions eliminate the 20KB prompt replay that caused Sonnet to hang. First request creates a session (`--session-id`), subsequent requests resume it (`--resume`). Claude keeps the conversation context; the bridge only sends the new message.
|
|
411
|
+
- **feat:** session registry persisted to `~/.openclaw/cli-bridge/claude-sessions.json` — survives gateway restarts, auto-expires after 2 hours of inactivity
|
|
412
|
+
- **feat:** auto-recovery: corrupted/expired sessions are detected and recreated transparently
|
|
413
|
+
|
|
409
414
|
### v3.1.2
|
|
410
415
|
- **fix:** fallback models returning text instead of tool_calls in a tool loop now trigger the next model in the chain. Previously Haiku would say "Lass mich das starten:" as text but never call a tool — conversation died.
|
|
411
416
|
- **feat:** `[FALLBACK-NO-TOOLS]` debug log category for tool-format violations
|
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.2.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.2.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,9 +18,9 @@
|
|
|
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 } from "node:fs";
|
|
21
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { randomBytes } from "node:crypto";
|
|
23
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
24
24
|
import { ensureClaudeToken, refreshClaudeToken } from "./claude-auth.js";
|
|
25
25
|
import {
|
|
26
26
|
type ToolDefinition,
|
|
@@ -535,10 +535,71 @@ export async function runGemini(
|
|
|
535
535
|
// Claude Code CLI
|
|
536
536
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
537
537
|
|
|
538
|
+
// ── Claude session registry ─────────────────────────────────────────────────
|
|
539
|
+
// Persistent sessions avoid re-sending the full 20KB prompt on every request.
|
|
540
|
+
// First call creates a session; subsequent calls resume it with just the new message.
|
|
541
|
+
|
|
542
|
+
const CLAUDE_SESSIONS_FILE = join(homedir(), ".openclaw", "cli-bridge", "claude-sessions.json");
|
|
543
|
+
|
|
544
|
+
interface ClaudeSessionEntry {
|
|
545
|
+
sessionId: string;
|
|
546
|
+
model: string;
|
|
547
|
+
createdAt: number;
|
|
548
|
+
lastUsedAt: number;
|
|
549
|
+
requestCount: number;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const claudeSessions = new Map<string, ClaudeSessionEntry>();
|
|
553
|
+
|
|
554
|
+
function loadClaudeSessions(): void {
|
|
555
|
+
try {
|
|
556
|
+
const data = JSON.parse(readFileSync(CLAUDE_SESSIONS_FILE, "utf8"));
|
|
557
|
+
if (Array.isArray(data.sessions)) {
|
|
558
|
+
for (const s of data.sessions) claudeSessions.set(s.model, s);
|
|
559
|
+
}
|
|
560
|
+
} catch { /* no sessions file yet */ }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function saveClaudeSessions(): void {
|
|
564
|
+
try {
|
|
565
|
+
mkdirSync(join(homedir(), ".openclaw", "cli-bridge"), { recursive: true });
|
|
566
|
+
writeFileSync(CLAUDE_SESSIONS_FILE, JSON.stringify({
|
|
567
|
+
version: 1,
|
|
568
|
+
sessions: [...claudeSessions.values()],
|
|
569
|
+
}, null, 2));
|
|
570
|
+
} catch { /* best effort */ }
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function getOrCreateSession(model: string): ClaudeSessionEntry {
|
|
574
|
+
if (claudeSessions.size === 0) loadClaudeSessions();
|
|
575
|
+
const existing = claudeSessions.get(model);
|
|
576
|
+
// Reuse session if it's less than 2 hours old
|
|
577
|
+
if (existing && (Date.now() - existing.lastUsedAt) < 2 * 60 * 60 * 1000) {
|
|
578
|
+
return existing;
|
|
579
|
+
}
|
|
580
|
+
const entry: ClaudeSessionEntry = {
|
|
581
|
+
sessionId: randomUUID(),
|
|
582
|
+
model,
|
|
583
|
+
createdAt: Date.now(),
|
|
584
|
+
lastUsedAt: Date.now(),
|
|
585
|
+
requestCount: 0,
|
|
586
|
+
};
|
|
587
|
+
claudeSessions.set(model, entry);
|
|
588
|
+
saveClaudeSessions();
|
|
589
|
+
return entry;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function invalidateSession(model: string): void {
|
|
593
|
+
claudeSessions.delete(model);
|
|
594
|
+
saveClaudeSessions();
|
|
595
|
+
}
|
|
596
|
+
|
|
538
597
|
/**
|
|
539
|
-
* Run Claude Code CLI in headless mode with
|
|
540
|
-
*
|
|
541
|
-
*
|
|
598
|
+
* Run Claude Code CLI in headless mode with session resume.
|
|
599
|
+
*
|
|
600
|
+
* First request: creates a new session with --session-id.
|
|
601
|
+
* Subsequent requests: --resume <session-id> with only the new message.
|
|
602
|
+
* This eliminates the 20KB prompt replay that causes Sonnet to hang.
|
|
542
603
|
*/
|
|
543
604
|
export async function runClaude(
|
|
544
605
|
prompt: string,
|
|
@@ -547,13 +608,12 @@ export async function runClaude(
|
|
|
547
608
|
workdir?: string,
|
|
548
609
|
opts?: { tools?: ToolDefinition[]; log?: (msg: string) => void }
|
|
549
610
|
): Promise<string> {
|
|
550
|
-
// Proactively refresh OAuth token if it's about to expire (< 5 min remaining).
|
|
551
|
-
// No-op for API-key users.
|
|
552
611
|
await ensureClaudeToken();
|
|
553
612
|
|
|
554
613
|
const model = stripPrefix(modelId);
|
|
555
|
-
|
|
556
|
-
|
|
614
|
+
const session = getOrCreateSession(model);
|
|
615
|
+
const isResume = session.requestCount > 0;
|
|
616
|
+
|
|
557
617
|
const args: string[] = [
|
|
558
618
|
"-p",
|
|
559
619
|
"--output-format", "text",
|
|
@@ -562,44 +622,84 @@ export async function runClaude(
|
|
|
562
622
|
"--model", model,
|
|
563
623
|
];
|
|
564
624
|
|
|
625
|
+
if (isResume) {
|
|
626
|
+
args.push("--resume", session.sessionId);
|
|
627
|
+
} else {
|
|
628
|
+
args.push("--session-id", session.sessionId);
|
|
629
|
+
}
|
|
630
|
+
|
|
565
631
|
// When tools are present, sandwich the conversation between tool instructions.
|
|
566
|
-
//
|
|
567
|
-
//
|
|
632
|
+
// On resume: only send the last user message (Claude has the full history).
|
|
633
|
+
// On first request: send the full prompt with tool block.
|
|
568
634
|
const effectivePrompt = opts?.tools?.length
|
|
569
635
|
? buildToolPromptBlock(opts.tools) + "\n\n" + prompt + "\n\nREMINDER: You MUST respond with ONLY valid JSON — either {\"tool_calls\":[...]} or {\"content\":\"...\"}. Nothing else."
|
|
570
636
|
: prompt;
|
|
571
637
|
|
|
572
638
|
const cwd = workdir ?? homedir();
|
|
573
|
-
debugLog("CLAUDE",
|
|
639
|
+
debugLog("CLAUDE", `${isResume ? "resume" : "new"} ${model} session=${session.sessionId.slice(0, 8)}`, {
|
|
640
|
+
promptLen: effectivePrompt.length, promptKB: Math.round(effectivePrompt.length / 1024),
|
|
641
|
+
requestCount: session.requestCount, cwd, timeoutMs: Math.round(timeoutMs / 1000),
|
|
642
|
+
});
|
|
643
|
+
|
|
574
644
|
const result = await runCli("claude", args, effectivePrompt, timeoutMs, { cwd, log: opts?.log });
|
|
575
645
|
|
|
576
|
-
//
|
|
577
|
-
if (result.exitCode
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
646
|
+
// Session succeeded — update registry
|
|
647
|
+
if (result.exitCode === 0 || result.stdout.length > 0) {
|
|
648
|
+
session.requestCount++;
|
|
649
|
+
session.lastUsedAt = Date.now();
|
|
650
|
+
saveClaudeSessions();
|
|
651
|
+
return result.stdout;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Session failed — check if it's a timeout or auth issue
|
|
655
|
+
if (result.timedOut) {
|
|
656
|
+
// Don't invalidate session on timeout — it's still valid, just slow
|
|
657
|
+
session.lastUsedAt = Date.now();
|
|
658
|
+
saveClaudeSessions();
|
|
659
|
+
throw new Error(`claude exited ${result.exitCode}: ${annotateExitError(result.exitCode, result.stderr, true, modelId)}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const stderr = result.stderr || "(no output)";
|
|
663
|
+
|
|
664
|
+
// Session might be corrupted or expired — invalidate and retry with a fresh session
|
|
665
|
+
if (stderr.includes("session") || stderr.includes("resume") || stderr.includes("not found")) {
|
|
666
|
+
debugLog("CLAUDE", `session ${session.sessionId.slice(0, 8)} invalid, creating fresh`, { error: stderr.slice(0, 100) });
|
|
667
|
+
invalidateSession(model);
|
|
668
|
+
// Retry once with a fresh session
|
|
669
|
+
const freshSession = getOrCreateSession(model);
|
|
670
|
+
const freshArgs = [
|
|
671
|
+
"-p", "--output-format", "text",
|
|
672
|
+
"--permission-mode", "bypassPermissions", "--dangerously-skip-permissions",
|
|
673
|
+
"--model", model, "--session-id", freshSession.sessionId,
|
|
674
|
+
];
|
|
675
|
+
const retry = await runCli("claude", freshArgs, effectivePrompt, timeoutMs, { cwd, log: opts?.log });
|
|
676
|
+
if (retry.exitCode === 0 || retry.stdout.length > 0) {
|
|
677
|
+
freshSession.requestCount++;
|
|
678
|
+
freshSession.lastUsedAt = Date.now();
|
|
679
|
+
saveClaudeSessions();
|
|
680
|
+
return retry.stdout;
|
|
581
681
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
throw new Error(`claude exited ${retry.exitCode} (after token refresh): ${retryStderr}`);
|
|
596
|
-
}
|
|
682
|
+
throw new Error(`claude exited ${retry.exitCode}: ${annotateExitError(retry.exitCode, retry.stderr || "(no output)", false, modelId)}`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Auth failure — refresh token and retry
|
|
686
|
+
if (stderr.includes("401") || stderr.includes("Invalid authentication credentials") || stderr.includes("authentication_error")) {
|
|
687
|
+
await refreshClaudeToken();
|
|
688
|
+
const retry = await runCli("claude", args, effectivePrompt, timeoutMs, { cwd, log: opts?.log });
|
|
689
|
+
if (retry.exitCode === 0 || retry.stdout.length > 0) {
|
|
690
|
+
session.requestCount++;
|
|
691
|
+
session.lastUsedAt = Date.now();
|
|
692
|
+
saveClaudeSessions();
|
|
597
693
|
return retry.stdout;
|
|
598
694
|
}
|
|
599
|
-
|
|
695
|
+
const retryStderr = retry.stderr || "(no output)";
|
|
696
|
+
if (retryStderr.includes("401") || retryStderr.includes("authentication_error")) {
|
|
697
|
+
throw new Error("Claude CLI OAuth token refresh failed. Re-login required: run `claude auth logout && claude auth login`.");
|
|
698
|
+
}
|
|
699
|
+
throw new Error(`claude exited ${retry.exitCode} (after token refresh): ${retryStderr}`);
|
|
600
700
|
}
|
|
601
701
|
|
|
602
|
-
|
|
702
|
+
throw new Error(`claude exited ${result.exitCode}: ${annotateExitError(result.exitCode, stderr, false, modelId)}`);
|
|
603
703
|
}
|
|
604
704
|
|
|
605
705
|
// ──────────────────────────────────────────────────────────────────────────────
|