@elvatis_com/openclaw-cli-bridge-elvatis 0.2.19 → 0.2.20
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 +31 -2
- package/test/cli-runner.test.ts +54 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
|
|
4
4
|
|
|
5
|
-
**Current version:** `0.2.
|
|
5
|
+
**Current version:** `0.2.20`
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -281,6 +281,11 @@ npm test # vitest run (45 tests)
|
|
|
281
281
|
|
|
282
282
|
## Changelog
|
|
283
283
|
|
|
284
|
+
### v0.2.20
|
|
285
|
+
- **fix:** `formatPrompt` now defensively coerces `content` to string via `contentToString()` — prevents `[object Object]` reaching the CLI when WhatsApp group messages contain structured content objects instead of plain strings
|
|
286
|
+
- **feat:** `ChatMessage.content` now accepts `string | ContentPart[] | unknown` (OpenAI multimodal content arrays supported)
|
|
287
|
+
- **feat:** New `contentToString()` helper: handles string, OpenAI ContentPart arrays, arbitrary objects (JSON.stringify), null/undefined
|
|
288
|
+
|
|
284
289
|
### v0.2.19
|
|
285
290
|
- **feat:** `/cli-list` command — formatted overview of all registered models grouped by provider
|
|
286
291
|
- **docs:** Rewrite README to reflect current state (correct model names, command count, requireAuth, test count, /cli-list docs)
|
package/SKILL.md
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-cli-bridge-elvatis",
|
|
3
3
|
"name": "OpenClaw CLI Bridge",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.20",
|
|
5
5
|
"description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
|
|
6
6
|
"providers": [
|
|
7
7
|
"openai-codex"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elvatis_com/openclaw-cli-bridge-elvatis",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
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
|
@@ -24,9 +24,15 @@ const MAX_MSG_CHARS = 4000;
|
|
|
24
24
|
// Message formatting
|
|
25
25
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
+
export interface ContentPart {
|
|
28
|
+
type: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
export interface ChatMessage {
|
|
28
33
|
role: "system" | "user" | "assistant";
|
|
29
|
-
content
|
|
34
|
+
/** Plain string or OpenAI-style content array (multimodal / structured). */
|
|
35
|
+
content: string | ContentPart[] | unknown;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -61,7 +67,30 @@ export function formatPrompt(messages: ChatMessage[]): string {
|
|
|
61
67
|
.join("\n\n");
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Coerce any message content value to a plain string.
|
|
72
|
+
*
|
|
73
|
+
* Handles:
|
|
74
|
+
* - string → as-is
|
|
75
|
+
* - ContentPart[] → join text parts (OpenAI multimodal format)
|
|
76
|
+
* - other object → JSON.stringify (prevents "[object Object]" from reaching the CLI)
|
|
77
|
+
* - null/undefined → ""
|
|
78
|
+
*/
|
|
79
|
+
function contentToString(content: unknown): string {
|
|
80
|
+
if (typeof content === "string") return content;
|
|
81
|
+
if (content === null || content === undefined) return "";
|
|
82
|
+
if (Array.isArray(content)) {
|
|
83
|
+
return (content as ContentPart[])
|
|
84
|
+
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
85
|
+
.map((c) => c.text!)
|
|
86
|
+
.join("\n");
|
|
87
|
+
}
|
|
88
|
+
if (typeof content === "object") return JSON.stringify(content);
|
|
89
|
+
return String(content);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function truncateContent(raw: unknown): string {
|
|
93
|
+
const s = contentToString(raw);
|
|
65
94
|
if (s.length <= MAX_MSG_CHARS) return s;
|
|
66
95
|
return s.slice(0, MAX_MSG_CHARS) + `\n...[truncated ${s.length - MAX_MSG_CHARS} chars]`;
|
|
67
96
|
}
|
package/test/cli-runner.test.ts
CHANGED
|
@@ -99,6 +99,60 @@ describe("formatPrompt", () => {
|
|
|
99
99
|
expect(result).toContain("[System]");
|
|
100
100
|
expect(result).toContain("[User]");
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
// contentToString coercion tests (fix: [object Object] in WhatsApp group messages)
|
|
104
|
+
it("coerces ContentPart array to plain text", () => {
|
|
105
|
+
const result = formatPrompt([
|
|
106
|
+
{ role: "user", content: [{ type: "text", text: "Hello from WA group" }] },
|
|
107
|
+
]);
|
|
108
|
+
expect(result).toBe("Hello from WA group");
|
|
109
|
+
expect(result).not.toContain("[object Object]");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("joins multiple text ContentParts with newline", () => {
|
|
113
|
+
const result = formatPrompt([
|
|
114
|
+
{
|
|
115
|
+
role: "user",
|
|
116
|
+
content: [
|
|
117
|
+
{ type: "text", text: "Part one" },
|
|
118
|
+
{ type: "text", text: "Part two" },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
expect(result).toContain("Part one");
|
|
123
|
+
expect(result).toContain("Part two");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("ignores non-text ContentParts (e.g. image)", () => {
|
|
127
|
+
const result = formatPrompt([
|
|
128
|
+
{
|
|
129
|
+
role: "user",
|
|
130
|
+
content: [
|
|
131
|
+
{ type: "image_url", url: "https://example.com/img.png" },
|
|
132
|
+
{ type: "text", text: "describe this" },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
expect(result).toBe("describe this");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("coerces plain object content to JSON string (not [object Object])", () => {
|
|
140
|
+
const result = formatPrompt([
|
|
141
|
+
{ role: "user", content: { text: "structured", extra: 42 } as any },
|
|
142
|
+
]);
|
|
143
|
+
expect(result).not.toBe("[object Object]");
|
|
144
|
+
expect(result).toContain("structured");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("handles null content gracefully", () => {
|
|
148
|
+
const result = formatPrompt([{ role: "user", content: null as any }]);
|
|
149
|
+
expect(result).toBe("");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("handles undefined content gracefully", () => {
|
|
153
|
+
const result = formatPrompt([{ role: "user", content: undefined as any }]);
|
|
154
|
+
expect(result).toBe("");
|
|
155
|
+
});
|
|
102
156
|
});
|
|
103
157
|
|
|
104
158
|
// ──────────────────────────────────────────────────────────────────────────────
|