diff --git a/plugin/web/server.ts b/plugin/web/server.ts index 45f4776..7af7e53 100644 --- a/plugin/web/server.ts +++ b/plugin/web/server.ts @@ -596,14 +596,47 @@ export function createBridgeServer(config: BridgeServerConfig): http.Server { return; } - const execFn = (toolInstance as { execute: (id: string, args: unknown) => Promise<{ content?: Array<{ type: string; text?: string }> }> }).execute; - const toolResult = await execFn(randomUUID(), toolArgs); + const execFn = (toolInstance as { execute: (id: string, args: unknown) => Promise }).execute; + const toolResultRaw = await execFn(randomUUID(), toolArgs); - // Extract text content from AgentToolResult - const text = (toolResult.content ?? []) - .filter((c) => c.type === "text" && c.text) - .map((c) => c.text as string) - .join("\n"); + // Normalize the result shape. OpenClaw plugins return one of: + // (a) AgentToolResult — { content: [{type:'text', text:'...'}, ...] } + // used by tools that wrap via asContent() (e.g. all + // Dialectic.OpenclawPlugin tools). + // (b) raw JSON-able object — { ok:true, ...domain fields } + // used by tools that return data directly (e.g. every + // Fabric.OpenclawPlugin tool: fabric-channel-list, + // fabric-guild-list, fabric-send-message, etc). + // (c) null / undefined — fire-and-forget (rare). + // + // Pre-2026-05-24 the bridge ONLY handled shape (a); shape (b) + // silently returned "(no result)" to Claude Code (bug: + // fabric-channel-list and friends were unusable from contractor + // agents even though the tool actually ran successfully). + let text = ""; + if (toolResultRaw == null) { + text = ""; + } else if ( + typeof toolResultRaw === "object" && + Array.isArray((toolResultRaw as { content?: unknown }).content) + ) { + // Shape (a) + const content = (toolResultRaw as { content: Array<{ type: string; text?: string }> }).content; + text = content + .filter((c) => c.type === "text" && c.text) + .map((c) => c.text as string) + .join("\n"); + } else if (typeof toolResultRaw === "string") { + text = toolResultRaw; + } else { + // Shape (b) — serialize the whole object as JSON. Claude Code + // is happy to parse JSON tool results. + try { + text = JSON.stringify(toolResultRaw); + } catch { + text = String(toolResultRaw); + } + } sendJson(res, 200, { result: text || "(no result)" }); } catch (err) {