@elvatis_com/openclaw-cli-bridge-elvatis 3.5.1 → 3.6.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 +1 -1
- package/SKILL.md +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cli-runner.ts +114 -4
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.6.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.6.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.6.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
|
@@ -1003,6 +1003,108 @@ function detectProjectFromPrompt(prompt: string): { name: string; path: string }
|
|
|
1003
1003
|
return null;
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
|
+
// ── Skill hint injection ─────────────────────────────────────────────────────
|
|
1007
|
+
// Scans ~/.openclaw/skills/ for skill directories with SKILL.md files.
|
|
1008
|
+
// When user prompt mentions a skill name (from the directory name or the SKILL.md
|
|
1009
|
+
// description), injects a pointer so the model knows where to find it.
|
|
1010
|
+
|
|
1011
|
+
interface SkillEntry {
|
|
1012
|
+
name: string;
|
|
1013
|
+
path: string;
|
|
1014
|
+
description: string;
|
|
1015
|
+
keywords: string[];
|
|
1016
|
+
scripts: string[];
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
let _skillRegistry: SkillEntry[] | null = null;
|
|
1020
|
+
let _skillRegistryRefreshedAt = 0;
|
|
1021
|
+
const SKILL_REGISTRY_CACHE_TTL = 120_000; // refresh every 2 min
|
|
1022
|
+
|
|
1023
|
+
function getSkillRegistry(): SkillEntry[] {
|
|
1024
|
+
const now = Date.now();
|
|
1025
|
+
if (_skillRegistry && (now - _skillRegistryRefreshedAt) < SKILL_REGISTRY_CACHE_TTL) {
|
|
1026
|
+
return _skillRegistry;
|
|
1027
|
+
}
|
|
1028
|
+
_skillRegistry = [];
|
|
1029
|
+
const skillsDir = join(homedir(), ".openclaw", "skills");
|
|
1030
|
+
try {
|
|
1031
|
+
if (!existsSync(skillsDir)) return _skillRegistry;
|
|
1032
|
+
const entries = readdirSync(skillsDir);
|
|
1033
|
+
for (const name of entries) {
|
|
1034
|
+
const skillDir = join(skillsDir, name);
|
|
1035
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
1036
|
+
try {
|
|
1037
|
+
if (!statSync(skillDir).isDirectory()) continue;
|
|
1038
|
+
if (!existsSync(skillMd)) continue;
|
|
1039
|
+
// Read first 500 chars of SKILL.md to extract description and keywords
|
|
1040
|
+
const content = readFileSync(skillMd, "utf8").slice(0, 500);
|
|
1041
|
+
const descMatch = content.match(/description:\s*"([^"]+)"/);
|
|
1042
|
+
const description = descMatch?.[1] ?? "";
|
|
1043
|
+
// Build keywords from: skill name, words in description, hyphen-split name parts
|
|
1044
|
+
const keywords = [
|
|
1045
|
+
name,
|
|
1046
|
+
...name.split("-"),
|
|
1047
|
+
...description.toLowerCase().split(/[\s,.:;]+/).filter(w => w.length > 3),
|
|
1048
|
+
];
|
|
1049
|
+
// Find scripts
|
|
1050
|
+
const scriptsDir = join(skillDir, "scripts");
|
|
1051
|
+
let scripts: string[] = [];
|
|
1052
|
+
try {
|
|
1053
|
+
if (existsSync(scriptsDir) && statSync(scriptsDir).isDirectory()) {
|
|
1054
|
+
scripts = readdirSync(scriptsDir).filter(f => f.endsWith(".py") || f.endsWith(".sh"));
|
|
1055
|
+
}
|
|
1056
|
+
} catch { /* no scripts dir */ }
|
|
1057
|
+
_skillRegistry.push({ name, path: skillDir, description, keywords, scripts });
|
|
1058
|
+
} catch { /* skip unreadable skill */ }
|
|
1059
|
+
}
|
|
1060
|
+
} catch { /* no skills dir */ }
|
|
1061
|
+
_skillRegistryRefreshedAt = now;
|
|
1062
|
+
return _skillRegistry;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function detectSkillHints(userText: string): string | null {
|
|
1066
|
+
const skills = getSkillRegistry();
|
|
1067
|
+
if (!skills.length) return null;
|
|
1068
|
+
|
|
1069
|
+
const lower = userText.toLowerCase();
|
|
1070
|
+
const matched: SkillEntry[] = [];
|
|
1071
|
+
|
|
1072
|
+
for (const skill of skills) {
|
|
1073
|
+
// Match by exact skill name in prompt
|
|
1074
|
+
const nameRegex = new RegExp(`\\b${skill.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
1075
|
+
if (nameRegex.test(userText)) {
|
|
1076
|
+
matched.push(skill);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
// Match by description keywords (need at least 2 keyword hits)
|
|
1080
|
+
const uniqueKeywords = [...new Set(skill.keywords)];
|
|
1081
|
+
const hits = uniqueKeywords.filter(kw => lower.includes(kw.toLowerCase()));
|
|
1082
|
+
if (hits.length >= 2) {
|
|
1083
|
+
matched.push(skill);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (!matched.length) return null;
|
|
1088
|
+
|
|
1089
|
+
const hints = matched.map(skill => {
|
|
1090
|
+
const lines = [
|
|
1091
|
+
`[Skill: ${skill.name}]`,
|
|
1092
|
+
`Read the skill instructions with the read tool: ${skill.path}/SKILL.md`,
|
|
1093
|
+
`Then follow the workflow step by step using the available tools (read, exec, web_fetch, etc.).`,
|
|
1094
|
+
];
|
|
1095
|
+
if (skill.scripts.length > 0) {
|
|
1096
|
+
lines.push(`Available scripts (use exec tool to run them):`);
|
|
1097
|
+
for (const s of skill.scripts) {
|
|
1098
|
+
lines.push(` - python3 ${skill.path}/scripts/${s}`);
|
|
1099
|
+
}
|
|
1100
|
+
lines.push(`Always use exec to run scripts. Do NOT output results as plain text when a script can do it.`);
|
|
1101
|
+
}
|
|
1102
|
+
return lines.join("\n");
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
return hints.join("\n\n");
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1006
1108
|
/**
|
|
1007
1109
|
* Route a chat completion to the correct CLI based on model prefix.
|
|
1008
1110
|
* cli-gemini/<id> → gemini CLI
|
|
@@ -1028,11 +1130,12 @@ export async function routeToCliRunner(
|
|
|
1028
1130
|
const hasTools = toolCount > 0;
|
|
1029
1131
|
|
|
1030
1132
|
// Auto-detect project from user messages only (not tool results which mention other projects)
|
|
1133
|
+
const userText = messages
|
|
1134
|
+
.filter((m) => m.role === "user")
|
|
1135
|
+
.map((m) => typeof m.content === "string" ? m.content : "")
|
|
1136
|
+
.join(" ");
|
|
1137
|
+
|
|
1031
1138
|
if (!opts.workdir) {
|
|
1032
|
-
const userText = messages
|
|
1033
|
-
.filter((m) => m.role === "user")
|
|
1034
|
-
.map((m) => typeof m.content === "string" ? m.content : "")
|
|
1035
|
-
.join(" ");
|
|
1036
1139
|
const detected = detectProjectFromPrompt(userText);
|
|
1037
1140
|
if (detected) {
|
|
1038
1141
|
opts = { ...opts, workdir: detected.path };
|
|
@@ -1041,6 +1144,13 @@ export async function routeToCliRunner(
|
|
|
1041
1144
|
}
|
|
1042
1145
|
}
|
|
1043
1146
|
|
|
1147
|
+
// Skill hints: inject pointers to local skill files when user prompt matches known patterns
|
|
1148
|
+
const skillHints = detectSkillHints(userText);
|
|
1149
|
+
if (skillHints) {
|
|
1150
|
+
prompt = `${skillHints}\n\n${prompt}`;
|
|
1151
|
+
debugLog("SKILL-HINT", "injected skill hints", { len: skillHints.length });
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1044
1154
|
// Strip "vllm/" prefix if present — OpenClaw sends the full provider path
|
|
1045
1155
|
// (e.g. "vllm/cli-claude/claude-sonnet-4-6") but the router only needs the
|
|
1046
1156
|
// "cli-<type>/<model>" portion.
|