@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 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.19`
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
@@ -53,4 +53,4 @@ Each command runs `openclaw models set <model>` atomically and replies with a co
53
53
 
54
54
  See `README.md` for full configuration reference and architecture diagram.
55
55
 
56
- **Version:** 0.2.19
56
+ **Version:** 0.2.20
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-cli-bridge-elvatis",
3
3
  "name": "OpenClaw CLI Bridge",
4
- "version": "0.2.19",
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.19",
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: string;
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
- function truncateContent(s: string): string {
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
  }
@@ -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
  // ──────────────────────────────────────────────────────────────────────────────