mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #1392 from DeusData/feat/generated-client-adapters
feat(install): generate pi and OpenCode extensions from the tool registry
This commit is contained in:
5 files changed
+482
-3
No files matched your search
+1
-1
@@ -352,7 +352,7 @@ WATCHER_SRCS = src/watcher/watcher.c
|
||||
GIT_SRCS = src/git/git_context.c
|
||||
|
||||
# CLI module (new)
|
||||
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c \
|
||||
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \
|
||||
src/cli/agent_clients.c src/cli/agent_profiles.c \
|
||||
src/cli/config_json_like.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c \
|
||||
src/cli/config_text_edit.c src/cli/activation_transaction.c
|
||||
|
||||
+76
-2
@@ -21,6 +21,7 @@
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/sha256.h"
|
||||
#include "cli/client_adapter.h"
|
||||
#include "mcp/mcp.h" // cbm_mcp_tool_input_schema — CLI flag parser + per-tool --help
|
||||
#include "mcp/index_supervisor.h"
|
||||
|
||||
@@ -7826,13 +7827,69 @@ static void install_devin_durable_context(const cbm_agent_registry_context_t *re
|
||||
}
|
||||
}
|
||||
|
||||
static void install_pi_durable_context(const char *home, bool force, bool dry_run) {
|
||||
/* Write a generated client extension module into `path` as a managed block.
|
||||
*
|
||||
* The module is generated from the tool registry rather than shipped (see
|
||||
* src/cli/client_adapter.h), and it goes in as a MARKED BLOCK rather than a
|
||||
* whole-file write: the directory is auto-loaded by the client, so a user may
|
||||
* legitimately keep their own module there, and clobbering it would be the
|
||||
* install routine destroying user content.
|
||||
*
|
||||
* `generate` returns heap-allocated text or NULL; NULL is a hard error rather
|
||||
* than a skip, because a silently absent extension is exactly the failure mode
|
||||
* that left #616 a no-op for six weeks. */
|
||||
static void install_generated_client_extension(const char *label, const char *path,
|
||||
const char *binary_path,
|
||||
char *(*generate)(const char *), bool dry_run) {
|
||||
if (g_install_plan) {
|
||||
plan_record(label, "extension", path);
|
||||
return;
|
||||
}
|
||||
char *content = generate(binary_path);
|
||||
if (!content) {
|
||||
record_agent_config_error(false, label, "extension_generate", path);
|
||||
return;
|
||||
}
|
||||
bool installed = true;
|
||||
if (!dry_run && (!prepare_config_parent(path) ||
|
||||
cbm_text_upsert_managed_block(path, CBM_ADAPTER_MARKER_START,
|
||||
CBM_ADAPTER_MARKER_END, content) != 0)) {
|
||||
installed = false;
|
||||
record_agent_config_error(false, label, "extension_install", path);
|
||||
}
|
||||
free(content);
|
||||
if (installed) {
|
||||
printf(" extension: %s\n", path);
|
||||
}
|
||||
}
|
||||
|
||||
/* Remove only OUR marked block, never the file: a user's own module may share
|
||||
* it, and owned removal is the convention every other uninstall path here
|
||||
* follows. */
|
||||
static void uninstall_generated_client_extension(const char *label, const char *path,
|
||||
bool dry_run) {
|
||||
if (!dry_run && cbm_file_exists(path) &&
|
||||
cbm_text_remove_managed_block(path, CBM_ADAPTER_MARKER_START, CBM_ADAPTER_MARKER_END) !=
|
||||
0) {
|
||||
record_agent_config_error(true, label, "extension_uninstall", path);
|
||||
return;
|
||||
}
|
||||
printf(" extension: removed managed block\n");
|
||||
}
|
||||
|
||||
static void install_pi_durable_context(const char *home, const char *binary_path, bool force,
|
||||
bool dry_run) {
|
||||
char instructions_path[CLI_BUF_1K];
|
||||
char skills_dir[CLI_BUF_1K];
|
||||
char extension_path[CLI_BUF_1K];
|
||||
snprintf(instructions_path, sizeof(instructions_path), "%s/.pi/agent/AGENTS.md", home);
|
||||
snprintf(skills_dir, sizeof(skills_dir), "%s/.pi/agent/skills", home);
|
||||
snprintf(extension_path, sizeof(extension_path), "%s/.pi/agent/extensions/cbmem.ts", home);
|
||||
install_managed_agent_instructions("Pi", instructions_path, dry_run);
|
||||
install_agent_skill("Pi", skills_dir, force, dry_run);
|
||||
/* pi has no MCP client, so this bridge is its ONLY route to the graph. */
|
||||
install_generated_client_extension("Pi", extension_path, binary_path, cbm_client_adapter_pi,
|
||||
dry_run);
|
||||
}
|
||||
|
||||
static void install_kimi_durable_context(const cbm_agent_registry_context_t *registry,
|
||||
@@ -8010,7 +8067,7 @@ static void install_agent_client_registry(const char *home, const char *binary_p
|
||||
} else if (profile->id == CBM_AGENT_CLIENT_POCHI) {
|
||||
install_pochi_durable_context(home, force, dry_run);
|
||||
} else if (profile->id == CBM_AGENT_CLIENT_PI) {
|
||||
install_pi_durable_context(home, force, dry_run);
|
||||
install_pi_durable_context(home, binary_path, force, dry_run);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8162,6 +8219,16 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
|
||||
.dialect = CBM_GRAPH_DIALECT_OPENCODE,
|
||||
},
|
||||
dry_run);
|
||||
/* OpenCode already reaches every tool over MCP (installed just above),
|
||||
* so this adds no tools -- only the automatic graph lookup before a
|
||||
* grep/glob that other clients get from their own hook configuration.
|
||||
* OpenCode has no such configuration; a plugin module is its only
|
||||
* extension point (verified against their plugin documentation). */
|
||||
char plugin_path[CLI_BUF_1K];
|
||||
snprintf(plugin_path, sizeof(plugin_path), "%s/.config/opencode/plugins/cbm-augment.ts",
|
||||
home);
|
||||
install_generated_client_extension("OpenCode", plugin_path, binary_path,
|
||||
cbm_client_adapter_opencode, dry_run);
|
||||
}
|
||||
if (agents->antigravity) {
|
||||
char cp[CLI_BUF_1K];
|
||||
@@ -9797,6 +9864,9 @@ static void uninstall_pi_durable_context(const char *home, bool dry_run) {
|
||||
}
|
||||
printf(" instructions: removed managed context\n");
|
||||
uninstall_agent_skill("Pi", skills_dir, dry_run);
|
||||
char extension_path[CLI_BUF_1K];
|
||||
snprintf(extension_path, sizeof(extension_path), "%s/.pi/agent/extensions/cbmem.ts", home);
|
||||
uninstall_generated_client_extension("Pi", extension_path, dry_run);
|
||||
}
|
||||
|
||||
static void uninstall_managed_agent_instructions(const char *label, const char *instructions_path,
|
||||
@@ -10109,6 +10179,10 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
|
||||
.dialect = CBM_GRAPH_DIALECT_OPENCODE,
|
||||
},
|
||||
dry_run);
|
||||
char plugin_path[CLI_BUF_1K];
|
||||
snprintf(plugin_path, sizeof(plugin_path), "%s/.config/opencode/plugins/cbm-augment.ts",
|
||||
home);
|
||||
uninstall_generated_client_extension("OpenCode", plugin_path, dry_run);
|
||||
}
|
||||
if (agents->antigravity) {
|
||||
char cp[CLI_BUF_1K];
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* client_adapter.c — Generate client extension modules from the MCP tool registry.
|
||||
*
|
||||
* See client_adapter.h for why these are generated rather than shipped.
|
||||
*/
|
||||
#include "cli/client_adapter.h"
|
||||
|
||||
#include "foundation/constants.h"
|
||||
#include "mcp/mcp.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* A generated module is small and bounded by the registry size; grow a plain
|
||||
* buffer rather than pulling in a builder for two call sites. */
|
||||
typedef struct {
|
||||
char *buf;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
bool failed;
|
||||
} adapter_sb_t;
|
||||
|
||||
static void sb_append(adapter_sb_t *sb, const char *s) {
|
||||
if (sb->failed || !s) {
|
||||
return;
|
||||
}
|
||||
size_t add = strlen(s);
|
||||
if (sb->len + add + 1 > sb->cap) {
|
||||
size_t want = sb->cap ? sb->cap : CBM_SZ_2K;
|
||||
while (want < sb->len + add + 1) {
|
||||
if (want > SIZE_MAX / PAIR_LEN) {
|
||||
sb->failed = true;
|
||||
return;
|
||||
}
|
||||
want *= PAIR_LEN;
|
||||
}
|
||||
char *grown = realloc(sb->buf, want);
|
||||
if (!grown) {
|
||||
sb->failed = true;
|
||||
return;
|
||||
}
|
||||
sb->buf = grown;
|
||||
sb->cap = want;
|
||||
}
|
||||
memcpy(sb->buf + sb->len, s, add);
|
||||
sb->len += add;
|
||||
sb->buf[sb->len] = '\0';
|
||||
}
|
||||
|
||||
bool cbm_client_adapter_escape_js(const char *in, char *out, size_t out_sz) {
|
||||
if (!in || !out || out_sz == 0) {
|
||||
return false;
|
||||
}
|
||||
size_t o = 0;
|
||||
for (const char *p = in; *p; p++) {
|
||||
/* Only these four can terminate or corrupt a single-quoted literal.
|
||||
* Escaping more would be harmless but makes the emitted path harder to
|
||||
* read when a user opens the generated file. */
|
||||
const char *esc = NULL;
|
||||
switch (*p) {
|
||||
case '\\':
|
||||
esc = "\\\\";
|
||||
break;
|
||||
case '\'':
|
||||
esc = "\\'";
|
||||
break;
|
||||
case '\n':
|
||||
esc = "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
esc = "\\r";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
size_t need = esc ? strlen(esc) : 1U;
|
||||
if (o + need + 1 > out_sz) {
|
||||
out[0] = '\0';
|
||||
return false; /* never emit a truncated literal */
|
||||
}
|
||||
if (esc) {
|
||||
memcpy(out + o, esc, need);
|
||||
} else {
|
||||
out[o] = *p;
|
||||
}
|
||||
o += need;
|
||||
}
|
||||
out[o] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Shared preamble: the ownership markers and a note telling whoever opens the
|
||||
* file that edits inside the block are overwritten. */
|
||||
static void emit_header(adapter_sb_t *sb, const char *client) {
|
||||
/* No markers here: cbm_text_upsert_managed_block adds them and REJECTS
|
||||
* content that already carries them, so the body must stay marker-free. */
|
||||
sb_append(sb, "// Generated by codebase-memory-mcp for ");
|
||||
sb_append(sb, client);
|
||||
sb_append(sb,
|
||||
".\n"
|
||||
"// Regenerated on install/update from the MCP tool registry, so it cannot\n"
|
||||
"// drift from the tools the server actually serves. Edits inside this block\n"
|
||||
"// are overwritten; edit outside it, or remove the markers to take ownership.\n");
|
||||
}
|
||||
|
||||
char *cbm_client_adapter_pi(const char *binary_path) {
|
||||
if (!binary_path || !binary_path[0]) {
|
||||
return NULL;
|
||||
}
|
||||
char bin[CBM_SZ_1K];
|
||||
if (!cbm_client_adapter_escape_js(binary_path, bin, sizeof(bin))) {
|
||||
return NULL;
|
||||
}
|
||||
int count = cbm_mcp_tool_count();
|
||||
if (count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
adapter_sb_t sb = {0};
|
||||
emit_header(&sb, "pi");
|
||||
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
|
||||
sb_append(&sb, "const BIN = '");
|
||||
sb_append(&sb, bin);
|
||||
sb_append(&sb, "';\n\n");
|
||||
|
||||
/* One shared invoker: the binary prints JSON on stdout, so take the last
|
||||
* line that parses rather than trusting the first — a stray log line on
|
||||
* stdout must not break the bridge. */
|
||||
sb_append(
|
||||
&sb, "async function call(tool, args, signal) {\n"
|
||||
" return new Promise((resolve) => {\n"
|
||||
" const child = spawn(BIN, ['cli', tool, JSON.stringify(args ?? {})], {\n"
|
||||
" stdio: ['ignore', 'pipe', 'pipe'],\n"
|
||||
" env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n"
|
||||
" });\n"
|
||||
" let out = '';\n"
|
||||
" const onAbort = () => { if (!child.killed) child.kill(); };\n"
|
||||
" signal?.addEventListener('abort', onAbort, { once: true });\n"
|
||||
" child.stdout.on('data', (d) => (out += d.toString()));\n"
|
||||
" child.on('error', (e) => {\n"
|
||||
" signal?.removeEventListener('abort', onAbort);\n"
|
||||
" resolve({ error: String(e && e.message ? e.message : e) });\n"
|
||||
" });\n"
|
||||
" child.on('close', () => {\n"
|
||||
" signal?.removeEventListener('abort', onAbort);\n"
|
||||
" const lines = out.split('\\n').map((l) => l.trim()).filter(Boolean);\n"
|
||||
" for (let i = lines.length - 1; i >= 0; i--) {\n"
|
||||
" try { return resolve(JSON.parse(lines[i])); } catch { /* keep scanning */ }\n"
|
||||
" }\n"
|
||||
" resolve({ error: 'no JSON response from codebase-memory-mcp' });\n"
|
||||
" });\n"
|
||||
" });\n"
|
||||
"}\n\n");
|
||||
|
||||
/* Register every tool the registry advertises — never a hand-picked subset,
|
||||
* which is the drift this generator exists to prevent. */
|
||||
sb_append(&sb, "export function register(pi) {\n");
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *name = cbm_mcp_tool_name(i);
|
||||
if (!name || !name[0]) {
|
||||
continue;
|
||||
}
|
||||
sb_append(&sb, " pi.registerTool({ name: '");
|
||||
sb_append(&sb, name);
|
||||
sb_append(&sb, "', run: (args, ctx) => call('");
|
||||
sb_append(&sb, name);
|
||||
sb_append(&sb, "', args, ctx?.signal) });\n");
|
||||
}
|
||||
sb_append(&sb, "}\n");
|
||||
|
||||
if (sb.failed) {
|
||||
free(sb.buf);
|
||||
return NULL;
|
||||
}
|
||||
return sb.buf;
|
||||
}
|
||||
|
||||
char *cbm_client_adapter_opencode(const char *binary_path) {
|
||||
if (!binary_path || !binary_path[0]) {
|
||||
return NULL;
|
||||
}
|
||||
char bin[CBM_SZ_1K];
|
||||
if (!cbm_client_adapter_escape_js(binary_path, bin, sizeof(bin))) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
adapter_sb_t sb = {0};
|
||||
emit_header(&sb, "OpenCode");
|
||||
sb_append(&sb, "// OpenCode already reaches every tool over MCP; this adds only the\n"
|
||||
"// automatic graph lookup before a grep/glob, which other clients get\n"
|
||||
"// through their own hook configuration.\n");
|
||||
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
|
||||
sb_append(&sb, "const BIN = '");
|
||||
sb_append(&sb, bin);
|
||||
sb_append(&sb, "';\n\n");
|
||||
|
||||
/* hook-augment requires hook_event_name and accepts Grep/Glob only under
|
||||
* PreToolUse; omitting it makes the whole hook a silent no-op. */
|
||||
sb_append(&sb, "function augment(tool, args) {\n"
|
||||
" return new Promise((resolve) => {\n"
|
||||
" const child = spawn(BIN, ['hook-augment'], {\n"
|
||||
" stdio: ['pipe', 'pipe', 'ignore'],\n"
|
||||
" env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n"
|
||||
" });\n"
|
||||
" let out = '';\n"
|
||||
" child.stdout.on('data', (d) => (out += d.toString()));\n"
|
||||
" child.on('error', () => resolve(''));\n"
|
||||
" child.on('close', () => resolve(out));\n"
|
||||
" child.stdin.end(JSON.stringify({\n"
|
||||
" hook_event_name: 'PreToolUse',\n"
|
||||
" tool_name: tool,\n"
|
||||
" tool_input: args ?? {},\n"
|
||||
" }));\n"
|
||||
" });\n"
|
||||
"}\n\n");
|
||||
|
||||
sb_append(&sb,
|
||||
"export const CodebaseMemory = async () => ({\n"
|
||||
" 'tool.execute.after': async (input, output) => {\n"
|
||||
" const tool = input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' "
|
||||
": null;\n"
|
||||
" if (!tool) return;\n"
|
||||
" const extra = await augment(tool, output?.args);\n"
|
||||
" if (extra && typeof output?.output === 'string') {\n"
|
||||
" output.output += '\\n' + extra;\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
"});\n");
|
||||
|
||||
if (sb.failed) {
|
||||
free(sb.buf);
|
||||
return NULL;
|
||||
}
|
||||
return sb.buf;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* client_adapter.h — Generate client extension modules from the MCP tool registry.
|
||||
*
|
||||
* Some clients have no MCP support (pi), and some have no declarative hook
|
||||
* configuration (OpenCode); their only extension point is a JavaScript/TypeScript
|
||||
* module. We do not ship such a module as a repository asset — it would be a
|
||||
* second, hand-maintained copy of the tool surface, and hand-maintained copies of
|
||||
* the tool list have already produced defects here.
|
||||
*
|
||||
* Instead the module is GENERATED at install time from the live registry
|
||||
* (cbm_mcp_tool_count / cbm_mcp_tool_name), so it cannot drift: adding a tool to
|
||||
* TOOLS[] adds it to every generated adapter with no second edit.
|
||||
*
|
||||
* Generated text is returned heap-allocated; the caller frees it. Nothing here
|
||||
* touches the filesystem — writing is the caller's job, so dry-run and
|
||||
* owned-block semantics stay in one place.
|
||||
*/
|
||||
#ifndef CBM_CLI_CLIENT_ADAPTER_H
|
||||
#define CBM_CLI_CLIENT_ADAPTER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Marker lines delimiting the generated region inside a client's extension
|
||||
* module. A file carrying these is ours to rewrite; one without them is the
|
||||
* user's and is left alone. Kept in the comment syntax shared by JS and TS. */
|
||||
#define CBM_ADAPTER_MARKER_START "// codebase-memory-mcp:start"
|
||||
#define CBM_ADAPTER_MARKER_END "// codebase-memory-mcp:end"
|
||||
|
||||
/* Emit a pi extension registering every tool in the MCP registry.
|
||||
*
|
||||
* pi has no MCP client, so this bridge is the only route to the graph for it.
|
||||
* Each registered tool shells out to `<binary> cli <tool> '<json-args>'`, which
|
||||
* is an existing public entry point rather than a private one.
|
||||
*
|
||||
* `binary_path` is embedded as a JS string literal and is escaped; a Windows
|
||||
* path such as C:\Users\x\bin\cbm.exe must not corrupt the module or, worse,
|
||||
* produce a file that fails to parse inside an auto-loaded extension directory.
|
||||
*
|
||||
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
|
||||
char *cbm_client_adapter_pi(const char *binary_path);
|
||||
|
||||
/* Emit an OpenCode plugin that augments grep/glob results from the graph.
|
||||
*
|
||||
* OpenCode HAS MCP (see cbm_upsert_opencode_mcp), so this adds no tools — it
|
||||
* only supplies the automatic "consult the graph before grepping" reflex that
|
||||
* other clients get through their own native hook configuration. OpenCode has
|
||||
* no such configuration; a plugin module is its only extension point.
|
||||
*
|
||||
* NOTE for maintainers: this hooks `tool.execute.after`, whose ability to
|
||||
* modify a tool's output is NOT part of OpenCode's documented plugin contract
|
||||
* (only `tool.execute.before`'s argument mutation is). If OpenCode changes it,
|
||||
* augmentation stops silently — no error, no signal. That risk is accepted
|
||||
* deliberately and recorded here so it is not rediscovered as a mystery.
|
||||
*
|
||||
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
|
||||
char *cbm_client_adapter_opencode(const char *binary_path);
|
||||
|
||||
/* Escape a path for embedding in a single-quoted JS string literal: backslash,
|
||||
* single quote, and the newline/CR that would terminate the literal outright.
|
||||
* Writes at most out_sz-1 characters plus NUL. Returns false when the escaped
|
||||
* form would not fit, so a caller never emits a truncated literal.
|
||||
*
|
||||
* Exposed for tests: the escaping is the part most likely to break a user's
|
||||
* install on Windows, and it deserves direct coverage rather than only being
|
||||
* reachable through a full generate call. */
|
||||
bool cbm_client_adapter_escape_js(const char *in, char *out, size_t out_sz);
|
||||
|
||||
#endif /* CBM_CLI_CLIENT_ADAPTER_H */
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "test_helpers.h"
|
||||
|
||||
#include <cli/agent_clients.h>
|
||||
#include <cli/client_adapter.h>
|
||||
#include <foundation/constants.h>
|
||||
#include <mcp/mcp.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -1062,6 +1065,98 @@ TEST(agent_clients_continue_refuses_foreign_same_name_and_nonsequence_section) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Generated client adapters ──────────────────────────────────── */
|
||||
|
||||
/* The generator exists so a client extension cannot carry a hand-picked subset
|
||||
* of the tool surface. #534 shipped one mirroring 7 of 15 tools; every tool
|
||||
* added afterwards would have been silently missing for that client. This pins
|
||||
* the property that makes generation worth doing: EVERY registry tool appears. */
|
||||
TEST(client_adapter_pi_registers_every_registry_tool) {
|
||||
char *js = cbm_client_adapter_pi("/usr/local/bin/codebase-memory-mcp");
|
||||
ASSERT_NOT_NULL(js);
|
||||
|
||||
int count = cbm_mcp_tool_count();
|
||||
ASSERT_GT(count, 0);
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *name = cbm_mcp_tool_name(i);
|
||||
ASSERT_NOT_NULL(name);
|
||||
char needle[128];
|
||||
snprintf(needle, sizeof(needle), "name: '%s'", name);
|
||||
/* A missing tool here means the adapter drifted from the registry —
|
||||
* exactly the failure this design removes. */
|
||||
ASSERT_NOT_NULL(strstr(js, needle));
|
||||
}
|
||||
/* The body must NOT carry the ownership markers: cbm_text_upsert_managed_block
|
||||
* adds them and rejects content that already has them, so emitting them here
|
||||
* would make every install fail. */
|
||||
ASSERT_NULL(strstr(js, CBM_ADAPTER_MARKER_START));
|
||||
ASSERT_NULL(strstr(js, CBM_ADAPTER_MARKER_END));
|
||||
ASSERT_NOT_NULL(strstr(js, "Generated by codebase-memory-mcp"));
|
||||
free(js);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #616 rejected `"` in its path template but not `\`, so a Windows home like
|
||||
* C:\Users\urs\bin produced `\u` — an invalid unicode escape — and the whole
|
||||
* auto-loaded plugin failed to parse. A broken plugin in an auto-load directory
|
||||
* is worse than an absent one, so the escaping gets direct coverage. */
|
||||
TEST(client_adapter_escapes_windows_paths_and_quotes) {
|
||||
char out[256];
|
||||
|
||||
ASSERT_TRUE(cbm_client_adapter_escape_js("C:\\Users\\urs\\bin\\cbm.exe", out, sizeof(out)));
|
||||
ASSERT_STR_EQ(out, "C:\\\\Users\\\\urs\\\\bin\\\\cbm.exe");
|
||||
/* The backslash before "urs" is DOUBLED, which is what stops JS reading
|
||||
* `\u` as a unicode escape — the exact break in #616. Asserting the
|
||||
* doubled form is the property; a bare `\u` substring still appears (as
|
||||
* escaped-backslash + 'u') and would be a misleading thing to test. */
|
||||
ASSERT_NOT_NULL(strstr(out, "\\\\urs"));
|
||||
|
||||
ASSERT_TRUE(cbm_client_adapter_escape_js("/home/o'brien/bin/cbm", out, sizeof(out)));
|
||||
ASSERT_STR_EQ(out, "/home/o\\'brien/bin/cbm");
|
||||
|
||||
/* A newline would terminate the literal outright. */
|
||||
ASSERT_TRUE(cbm_client_adapter_escape_js("/tmp/a\nb", out, sizeof(out)));
|
||||
ASSERT_STR_EQ(out, "/tmp/a\\nb");
|
||||
|
||||
/* Truncation must fail closed rather than emit half a literal. */
|
||||
char tiny[4];
|
||||
ASSERT_FALSE(cbm_client_adapter_escape_js("C:\\Users", tiny, sizeof(tiny)));
|
||||
ASSERT_STR_EQ(tiny, "");
|
||||
|
||||
/* And a path that cannot be escaped must abort generation, not produce a
|
||||
* module with a corrupt BIN. */
|
||||
char huge[CBM_SZ_2K];
|
||||
memset(huge, 'x', sizeof(huge) - 1);
|
||||
huge[sizeof(huge) - 1] = '\0';
|
||||
ASSERT_NULL(cbm_client_adapter_pi(huge));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* hook-augment requires hook_event_name and accepts Grep/Glob only under
|
||||
* PreToolUse. #616's payload omitted it, so the plugin produced zero bytes and
|
||||
* was a silent no-op for six weeks. Pin the field into the generated payload. */
|
||||
TEST(client_adapter_opencode_sends_the_required_hook_event) {
|
||||
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
|
||||
ASSERT_NOT_NULL(js);
|
||||
ASSERT_NOT_NULL(strstr(js, "hook_event_name: 'PreToolUse'"));
|
||||
ASSERT_NOT_NULL(strstr(js, "tool.execute.after"));
|
||||
/* OpenCode reaches the tools over MCP already; this adapter must not
|
||||
* register any, or we reintroduce the second tool surface. */
|
||||
ASSERT_NULL(strstr(js, "registerTool"));
|
||||
ASSERT_NULL(strstr(js, CBM_ADAPTER_MARKER_START));
|
||||
free(js);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Empty/NULL inputs must not produce a module at all. */
|
||||
TEST(client_adapter_rejects_missing_binary_path) {
|
||||
ASSERT_NULL(cbm_client_adapter_pi(NULL));
|
||||
ASSERT_NULL(cbm_client_adapter_pi(""));
|
||||
ASSERT_NULL(cbm_client_adapter_opencode(NULL));
|
||||
ASSERT_NULL(cbm_client_adapter_opencode(""));
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(agent_clients) {
|
||||
RUN_TEST(agent_clients_registry_is_stable_and_callback_driven);
|
||||
RUN_TEST(agent_clients_next_wave_metadata_matches_supported_surfaces);
|
||||
@@ -1089,4 +1184,8 @@ SUITE(agent_clients) {
|
||||
RUN_TEST(agent_clients_deep_same_name_json_fails_closed_byte_identically);
|
||||
RUN_TEST(agent_clients_continue_uses_owned_yaml_sequence_item);
|
||||
RUN_TEST(agent_clients_continue_refuses_foreign_same_name_and_nonsequence_section);
|
||||
RUN_TEST(client_adapter_pi_registers_every_registry_tool);
|
||||
RUN_TEST(client_adapter_escapes_windows_paths_and_quotes);
|
||||
RUN_TEST(client_adapter_opencode_sends_the_required_hook_event);
|
||||
RUN_TEST(client_adapter_rejects_missing_binary_path);
|
||||
}
|
||||
Reference in new issue
Block a user