| 139 | } |
| 140 | |
| 141 | char *cbm_client_adapter_pi(const char *binary_path) { |
| 142 | if (!binary_path || !binary_path[0]) { |
| 143 | return NULL; |
| 144 | } |
| 145 | char bin[CBM_SZ_1K]; |
| 146 | if (!cbm_client_adapter_escape_js(binary_path, bin, sizeof(bin))) { |
| 147 | return NULL; |
| 148 | } |
| 149 | int count = cbm_mcp_tool_count(); |
| 150 | if (count <= 0) { |
| 151 | return NULL; |
| 152 | } |
| 153 | |
| 154 | adapter_sb_t sb = {0}; |
| 155 | emit_header(&sb, "pi"); |
| 156 | /* Pin the coding-agent contract so a probe against @mariozechner/pi |
| 157 | * (pods CLI) or an old AgentTool arity cannot be mistaken for this file. */ |
| 158 | sb_append(&sb, "// Target: @earendil-works/pi-coding-agent >= 0.74.0 (verified 0.84.2)\n" |
| 159 | "// ToolDefinition.execute(toolCallId, params, signal, onUpdate, ctx)\n"); |
| 160 | sb_append(&sb, "import { spawn } from 'node:child_process';\n\n"); |
| 161 | sb_append(&sb, "const BIN = '"); |
| 162 | sb_append(&sb, bin); |
| 163 | sb_append(&sb, "';\n\n"); |
| 164 | |
| 165 | /* One shared invoker: the binary prints JSON on stdout, so take the last |
| 166 | * line that parses rather than trusting the first — a stray log line on |
| 167 | * stdout must not break the bridge. */ |
| 168 | sb_append( |
| 169 | &sb, "async function call(tool, args, signal) {\n" |
| 170 | " return new Promise((resolve) => {\n" |
| 171 | " const child = spawn(BIN, ['cli', '--json', tool], {\n" |
| 172 | " stdio: ['pipe', 'pipe', 'pipe'],\n" |
| 173 | " env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n" |
| 174 | " });\n" |
| 175 | " let out = '';\n" |
| 176 | " let stdinError;\n" |
| 177 | " const onAbort = () => { if (!child.killed) child.kill(); };\n" |
| 178 | " signal?.addEventListener('abort', onAbort, { once: true });\n" |
| 179 | " child.stdout.on('data', (d) => (out += d.toString()));\n" |
| 180 | " child.stdin.on('error', (e) => { stdinError = e; });\n" |
| 181 | " child.on('error', (e) => {\n" |
| 182 | " signal?.removeEventListener('abort', onAbort);\n" |
| 183 | " resolve({ error: String(e && e.message ? e.message : e) });\n" |
| 184 | " });\n" |
| 185 | " child.on('close', () => {\n" |
| 186 | " signal?.removeEventListener('abort', onAbort);\n" |
| 187 | " const lines = out.split('\\n').map((l) => l.trim()).filter(Boolean);\n" |
| 188 | " for (let i = lines.length - 1; i >= 0; i--) {\n" |
| 189 | " try { return resolve(JSON.parse(lines[i])); } catch { /* keep scanning */ }\n" |
| 190 | " }\n" |
| 191 | " if (stdinError) {\n" |
| 192 | " return resolve({ error: String(stdinError.message || stdinError) });\n" |
| 193 | " }\n" |
| 194 | " resolve({ error: 'no JSON response from codebase-memory-mcp' });\n" |
| 195 | " });\n" |
| 196 | " child.stdin.end(JSON.stringify(args ?? {}));\n" |
| 197 | " });\n" |
| 198 | "}\n\n"); |
no test coverage detected