| 106 | } |
| 107 | |
| 108 | char *cbm_client_adapter_pi(const char *binary_path) { |
| 109 | if (!binary_path || !binary_path[0]) { |
| 110 | return NULL; |
| 111 | } |
| 112 | char bin[CBM_SZ_1K]; |
| 113 | if (!cbm_client_adapter_escape_js(binary_path, bin, sizeof(bin))) { |
| 114 | return NULL; |
| 115 | } |
| 116 | int count = cbm_mcp_tool_count(); |
| 117 | if (count <= 0) { |
| 118 | return NULL; |
| 119 | } |
| 120 | |
| 121 | adapter_sb_t sb = {0}; |
| 122 | emit_header(&sb, "pi"); |
| 123 | sb_append(&sb, "import { spawn } from 'node:child_process';\n\n"); |
| 124 | sb_append(&sb, "const BIN = '"); |
| 125 | sb_append(&sb, bin); |
| 126 | sb_append(&sb, "';\n\n"); |
| 127 | |
| 128 | /* One shared invoker: the binary prints JSON on stdout, so take the last |
| 129 | * line that parses rather than trusting the first — a stray log line on |
| 130 | * stdout must not break the bridge. */ |
| 131 | sb_append( |
| 132 | &sb, "async function call(tool, args, signal) {\n" |
| 133 | " return new Promise((resolve) => {\n" |
| 134 | " const child = spawn(BIN, ['cli', tool, JSON.stringify(args ?? {})], {\n" |
| 135 | " stdio: ['ignore', 'pipe', 'pipe'],\n" |
| 136 | " env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n" |
| 137 | " });\n" |
| 138 | " let out = '';\n" |
| 139 | " const onAbort = () => { if (!child.killed) child.kill(); };\n" |
| 140 | " signal?.addEventListener('abort', onAbort, { once: true });\n" |
| 141 | " child.stdout.on('data', (d) => (out += d.toString()));\n" |
| 142 | " child.on('error', (e) => {\n" |
| 143 | " signal?.removeEventListener('abort', onAbort);\n" |
| 144 | " resolve({ error: String(e && e.message ? e.message : e) });\n" |
| 145 | " });\n" |
| 146 | " child.on('close', () => {\n" |
| 147 | " signal?.removeEventListener('abort', onAbort);\n" |
| 148 | " const lines = out.split('\\n').map((l) => l.trim()).filter(Boolean);\n" |
| 149 | " for (let i = lines.length - 1; i >= 0; i--) {\n" |
| 150 | " try { return resolve(JSON.parse(lines[i])); } catch { /* keep scanning */ }\n" |
| 151 | " }\n" |
| 152 | " resolve({ error: 'no JSON response from codebase-memory-mcp' });\n" |
| 153 | " });\n" |
| 154 | " });\n" |
| 155 | "}\n\n"); |
| 156 | |
| 157 | /* Register every tool the registry advertises — never a hand-picked subset, |
| 158 | * which is the drift this generator exists to prevent. |
| 159 | * |
| 160 | * DEFAULT export, and it must stay that way. Pi loads an extension via |
| 161 | * jiti.import(path, { default: true }) and rejects anything that is not a |
| 162 | * function, so a named `export function register(pi)` hands the loader the |
| 163 | * module namespace object instead and every install fails with "Extension |
| 164 | * does not export a valid factory function". A Pi extension that fails to |
| 165 | * load takes EVERY pi command down with it — `pi doctor` included — not |
no test coverage detected