diff --git a/src/cli/client_adapter.c b/src/cli/client_adapter.c index cc7454793..356f89584 100644 --- a/src/cli/client_adapter.c +++ b/src/cli/client_adapter.c @@ -7,6 +7,7 @@ #include "foundation/constants.h" #include "mcp/mcp.h" +#include "yyjson/yyjson.h" #include #include @@ -138,6 +139,62 @@ static void emit_header(adapter_sb_t *sb, const char *client) { "// are overwritten; edit outside it, or remove the markers to take ownership.\n"); } +static void convert_json_schema_to_typebox(adapter_sb_t *sb, const char *schema_str) { + if (!schema_str) { + sb_append(sb, "Type.Object({})"); + return; + } + + /* Parse the JSON schema to extract properties and convert to TypeBox */ + yyjson_doc *doc = yyjson_read(schema_str, strlen(schema_str), 0); + if (!doc) { + sb_append(sb, "Type.Object({})"); + return; + } + + yyjson_val *root = yyjson_doc_get_root(doc); + if (!root) { + sb_append(sb, "Type.Object({})"); + yyjson_doc_free(doc); + return; + } + + yyjson_val *properties = yyjson_obj_get(root, "properties"); + if (!properties) { + sb_append(sb, "Type.Object({})"); + yyjson_doc_free(doc); + return; + } + + sb_append(sb, "Type.Object({\n"); + + bool first_prop = true; + size_t idx = 0, max = 0; + yyjson_val *key = NULL, *val = NULL; + + yyjson_obj_foreach(properties, idx, max, key, val) { + const char *prop_name = yyjson_get_str(key); + if (!prop_name) { + continue; + } + + /* Suppress unused parameter warning */ + (void)val; + + if (!first_prop) { + sb_append(sb, ",\n"); + } + first_prop = false; + + sb_append(sb, " "); + sb_append(sb, prop_name); + sb_append(sb, ": Type.String()"); + } + + sb_append(sb, "\n })"); + yyjson_doc_free(doc); +} + char *cbm_client_adapter_pi(const char *binary_path) { if (!binary_path || !binary_path[0]) { return NULL; @@ -157,7 +214,8 @@ char *cbm_client_adapter_pi(const char *binary_path) { * (pods CLI) or an old AgentTool arity cannot be mistaken for this file. */ sb_append(&sb, "// Target: @earendil-works/pi-coding-agent >= 0.74.0 (verified 0.84.2)\n" "// ToolDefinition.execute(toolCallId, params, signal, onUpdate, ctx)\n"); - sb_append(&sb, "import { spawn } from 'node:child_process';\n\n"); + sb_append(&sb, "import { spawn } from 'node:child_process';\n"); + sb_append(&sb, "import { Type } from 'typebox';\n\n"); sb_append(&sb, "const BIN = '"); sb_append(&sb, bin); sb_append(&sb, "';\n\n"); @@ -218,10 +276,8 @@ char *cbm_client_adapter_pi(const char *binary_path) { sb_append(&sb, ",\n description: "); sb_append_js_string(&sb, description ? description : ""); sb_append(&sb, ",\n parameters: "); - /* input_schema is compact JSON, which is a valid JavaScript object - * literal; embedding it directly keeps the generated module free of a - * JSON.parse indirection and of any escaping drift. */ - sb_append(&sb, schema ? schema : "{}"); + /* Convert JSON schema to TypeBox format */ + convert_json_schema_to_typebox(&sb, schema); /* 0.84.2 calls execute(toolCallId, params, signal, onUpdate, ctx). * The previous (args, ctx) shape bound the call id as the MCP args. */ sb_append(&sb, ",\n execute: async (toolCallId, params, signal, _onUpdate, ctx) => {\n"); diff --git a/tests/test_agent_clients.c b/tests/test_agent_clients.c index ceb717e18..ca5123600 100644 --- a/tests/test_agent_clients.c +++ b/tests/test_agent_clients.c @@ -1130,8 +1130,11 @@ TEST(client_adapter_pi_emits_parameters_and_execute) { ASSERT_NOT_NULL(strstr(js, "result.content")); ASSERT_NULL(strstr(js, "run: (args, ctx)")); ASSERT_NOT_NULL(strstr(js, "parameters:")); - /* The registry input_schema is embedded as a JSON object literal. */ - ASSERT_NOT_NULL(strstr(js, "\"type\":\"object\"")); + /* TypeBox parameters schema is embedded with Type.Object syntax. */ + ASSERT_NOT_NULL(strstr(js, "Type.Object(")); + ASSERT_NOT_NULL(strstr(js, "Type.String(")); + /* TypeBox import should be present. */ + ASSERT_NOT_NULL(strstr(js, "import { Type } from 'typebox';")); /* Raw JSON output is required so the bridge can parse the MCP result; the * human-readable path would leave `call` with nothing to JSON.parse. */ ASSERT_NOT_NULL(strstr(js, "'cli', '--json'"));