diff --git a/packages/core/package.json b/packages/core/package.json index 10a747b..d269860 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@leadbay/core", - "version": "0.2.1", + "version": "0.2.2", "private": true, "description": "Leadbay shared core: HTTP client and protocol-agnostic tool definitions. Consumed by @leadbay/leadclaw and @leadbay/mcp.", "type": "module", diff --git a/packages/core/src/composite/refine-prompt.ts b/packages/core/src/composite/refine-prompt.ts index 91df108..f7defd4 100644 --- a/packages/core/src/composite/refine-prompt.ts +++ b/packages/core/src/composite/refine-prompt.ts @@ -62,7 +62,7 @@ export const refinePrompt: Tool = { would_call: { method: "POST", path: `/organizations/${orgId}/user_prompt`, - body: { prompt: params.prompt }, + body: { user_prompt: params.prompt }, }, }; } @@ -73,7 +73,7 @@ export const refinePrompt: Tool = { const STALE_GUARD_MS = 5_000; await client.requestVoid("POST", `/organizations/${orgId}/user_prompt`, { - prompt: params.prompt, + user_prompt: params.prompt, }); // Cache invalidation — /me's computing_intelligence flag is now true. diff --git a/packages/core/src/tools/set-user-prompt.ts b/packages/core/src/tools/set-user-prompt.ts index 365d3d0..a3eb904 100644 --- a/packages/core/src/tools/set-user-prompt.ts +++ b/packages/core/src/tools/set-user-prompt.ts @@ -36,12 +36,12 @@ export const setUserPrompt: Tool = { would_call: { method: "POST", path: `/organizations/${orgId}/user_prompt`, - body: { prompt: params.prompt }, + body: { user_prompt: params.prompt }, }, }; } await client.requestVoid("POST", `/organizations/${orgId}/user_prompt`, { - prompt: params.prompt, + user_prompt: params.prompt, }); // Mutates organization.computing_intelligence (and clears any pending // clarification). The /me cache holds organization.computing_intelligence; diff --git a/packages/core/test/unit/client.test.ts b/packages/core/test/unit/client.test.ts index f1d24c5..bc5981f 100644 --- a/packages/core/test/unit/client.test.ts +++ b/packages/core/test/unit/client.test.ts @@ -376,6 +376,88 @@ describe("Granular write tools invalidate /me cache", () => { }); }); +describe("user_prompt POST body shape (contract pin — #3508)", () => { + // Backend's UserPromptPayload is kotlinx.serialization @SerialName("user_prompt"); + // strict deserialization rejects { prompt: ... }. These tests pin the wire key + // so the contract bug can't silently reappear. + it("setUserPrompt sends { user_prompt }, not { prompt }", async () => { + const { setUserPrompt } = await import("../../src/tools/set-user-prompt.js"); + const { requests } = mockHttp([ + { + method: "GET", + path: "/1.5/users/me", + status: 200, + body: { + id: "u", + organization: { id: "org-1", name: "X", computing_intelligence: false }, + }, + }, + { method: "POST", path: "/1.5/organizations/org-1/user_prompt", status: 204 }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + await setUserPrompt.execute(client, { prompt: "focus on hospitals" }); + const post = requests.find( + (r) => r.method === "POST" && r.path === "/1.5/organizations/org-1/user_prompt" + ); + expect(post?.body).toBeDefined(); + const parsed = JSON.parse(post!.body!); + expect(parsed).toEqual({ user_prompt: "focus on hospitals" }); + expect(parsed).not.toHaveProperty("prompt"); + }); + + it("refinePrompt sends { user_prompt }, not { prompt }", async () => { + const { refinePrompt } = await import("../../src/composite/refine-prompt.js"); + const { requests } = mockHttp([ + { + method: "GET", + path: "/1.5/users/me", + status: 200, + body: { + id: "u", + admin: true, + organization: { id: "org-1", name: "X", computing_intelligence: false }, + }, + }, + { method: "POST", path: "/1.5/organizations/org-1/user_prompt", status: 204 }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + // clarification_poll_attempts: 0 skips the poll loop entirely. + await refinePrompt.execute(client, { + prompt: "focus on hospitals", + clarification_poll_attempts: 0, + }); + const post = requests.find( + (r) => r.method === "POST" && r.path === "/1.5/organizations/org-1/user_prompt" + ); + expect(post?.body).toBeDefined(); + const parsed = JSON.parse(post!.body!); + expect(parsed).toEqual({ user_prompt: "focus on hospitals" }); + expect(parsed).not.toHaveProperty("prompt"); + }); + + it("refinePrompt dry_run preview uses user_prompt key", async () => { + const { refinePrompt } = await import("../../src/composite/refine-prompt.js"); + mockHttp([ + { + method: "GET", + path: "/1.5/users/me", + status: 200, + body: { + id: "u", + admin: true, + organization: { id: "org-1", name: "X" }, + }, + }, + ]); + const client = new LeadbayClient(BASE, "u.test-token"); + const res: any = await refinePrompt.execute(client, { + prompt: "p", + dry_run: true, + }); + expect(res.would_call.body).toEqual({ user_prompt: "p" }); + }); +}); + describe("LeadbayClient.acquireSelectionLock — Mutex", () => { it("serialises concurrent selection holders", async () => { const client = new LeadbayClient(BASE, "u.test-token"); diff --git a/packages/leadclaw/CHANGELOG.md b/packages/leadclaw/CHANGELOG.md index 6f357e2..da816c0 100644 --- a/packages/leadclaw/CHANGELOG.md +++ b/packages/leadclaw/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog — @leadbay/leadclaw +## 0.2.3 — 2026-04-21 + +Bug fix release. Picks up `@leadbay/core@0.2.2` underneath. + +- **Fix [product#3508](https://github.com/leadbay/product/issues/3508)**: `leadbay_refine_prompt` (and the granular `leadbay_set_user_prompt`) now send `{ user_prompt }` to `POST /organizations/{orgId}/user_prompt` instead of `{ prompt }`. The backend's `UserPromptPayload` uses `@SerialName("user_prompt")` with strict kotlinx.serialization, so the old key was rejected as a deserialization error (400). The `dry_run` preview was affected the same way. Version kept in sync with `@leadbay/mcp@0.2.3`. + ## 0.2.2 — 2026-04-21 Bug fix + contract correction + mental-model docs release. Picks up `@leadbay/core@0.2.1` underneath. diff --git a/packages/leadclaw/openclaw.plugin.json b/packages/leadclaw/openclaw.plugin.json index 3b1c475..027fde4 100644 --- a/packages/leadclaw/openclaw.plugin.json +++ b/packages/leadclaw/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "leadclaw", "name": "LeadClaw", "description": "Leadbay for AI agents: a daily sales-lead inbox with firmographic + AI qualification layers, plus on-demand deeper qualification and contact enrichment. Each login delivers a fresh batch of leads paced by the user's recent consumption; the agent skims, deepens promising ones, and proposes outreach.", - "version": "0.2.2", + "version": "0.2.3", "contracts": { "tools": [ "leadbay_login", diff --git a/packages/leadclaw/package.json b/packages/leadclaw/package.json index da429c7..a34b4ca 100644 --- a/packages/leadclaw/package.json +++ b/packages/leadclaw/package.json @@ -1,6 +1,6 @@ { "name": "@leadbay/leadclaw", - "version": "0.2.2", + "version": "0.2.3", "description": "OpenClaw plugin for Leadbay — AI lead discovery, qualification, and enrichment", "type": "module", "main": "dist/index.js", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index e8199a9..6c26fe9 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog — @leadbay/mcp +## 0.2.3 — 2026-04-21 + +Bug fix release. + +- **Fix [product#3508](https://github.com/leadbay/product/issues/3508)**: `leadbay_refine_prompt` (and the granular `leadbay_set_user_prompt`) now send the correct `{ user_prompt }` body key to `POST /organizations/{orgId}/user_prompt`. Previous versions sent `{ prompt }`, which the backend's strict kotlinx.serialization rejected with a JSON deserialization error (400). The `dry_run` preview for both tools was printing the wrong shape too, which hid the mismatch from anyone inspecting it. New unit tests pin the wire key so this contract can't silently regress again. + ## 0.2.2 — 2026-04-21 Bug fix + contract correction + mental-model docs release. diff --git a/packages/mcp/package.json b/packages/mcp/package.json index cedbd29..ca7c8bf 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@leadbay/mcp", - "version": "0.2.2", + "version": "0.2.3", "description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.", "type": "module", "bin": {