Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions pkg/scoop/codebase-memory-mcp.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
{
"version": "0.8.1",
"description": "Fast code intelligence engine — native executable with verified runtime assets",
"homepage": "https://github.com/DeusData/codebase-memory-mcp",
"version": "0.11.0",
"description": "Code intelligence knowledge graph MCP server for AI coding agents",
"homepage": "https://deusdata.github.io/codebase-memory-mcp/",
"license": "MIT",
"notes": "To register this MCP server with your clients, run `codebase-memory-mcp install` after install.",
"architecture": {
"64bit": {
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.8.1/codebase-memory-mcp-windows-amd64.zip",
"hash": "a602ad090ed3f49d86c55472f73f27ad7055222806a82358f2e08513e027f00f",
"bin": "codebase-memory-mcp.exe"
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.11.0/codebase-memory-mcp-windows-amd64.zip",
"hash": "6eb6beaf261b19e419766e78baf93cbc3cf1c6338cff8fb7c0234859f96d1685"
},
"arm64": {
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.11.0/codebase-memory-mcp-windows-arm64.zip",
"hash": "52b29881214fce47d529e098308b1de77c40f6812e20a34d588ee4c25b84fd1a"
}
},
"bin": "codebase-memory-mcp.exe",
"checkver": {
"github": "https://github.com/DeusData/codebase-memory-mcp"
},
Expand All @@ -18,8 +23,15 @@
"64bit": {
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v$version/codebase-memory-mcp-windows-amd64.zip",
"hash": {
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v$version/checksums.txt",
"find": "([a-f0-9]{64})\\s+codebase-memory-mcp-windows-amd64\\.zip"
"url": "$baseurl/checksums.txt",
"find": "([a-f0-9]{64})\\s+codebase-memory-mcp-windows-amd64\\.zip$"
}
},
"arm64": {
"url": "https://github.com/DeusData/codebase-memory-mcp/releases/download/v$version/codebase-memory-mcp-windows-arm64.zip",
"hash": {
"url": "$baseurl/checksums.txt",
"find": "([a-f0-9]{64})\\s+codebase-memory-mcp-windows-arm64\\.zip$"
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/git/git_context.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

#include "foundation/compat_fs.h"
#include "foundation/constants.h"
#include "foundation/platform.h"
#include "foundation/str_util.h"

#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

enum {
GIT_CMD_MAX = 1024,
Expand Down Expand Up @@ -254,8 +254,7 @@ int cbm_git_context_resolve(const char *path, cbm_git_context_t *out) {
return CBM_NOT_FOUND;
}

struct stat st;
out->root_exists = (stat(path, &st) == 0);
out->root_exists = cbm_is_dir(path);
if (!out->root_exists) {
return 0;
}
Expand Down
15 changes: 7 additions & 8 deletions src/ui/http_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,6 @@ static void handle_processes(cbm_http_conn_t *c) {

/* ── Directory browser ────────────────────────────────────────── */

#include <dirent.h>

static void append_roots_json(char *buf, size_t bufsz, int *pos) {
http_appendf(buf, bufsz, pos, ",\"roots\":[");
Expand Down Expand Up @@ -770,7 +769,7 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
return;
}

DIR *dir = opendir(path);
cbm_dir_t *dir = cbm_opendir(path);
if (!dir) {
cbm_http_replyf(c, 403, g_cors_json, "{\"error\":\"cannot open directory\"}");
return;
Expand All @@ -781,16 +780,16 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
int pos = 0;
http_appendf(buf, sizeof(buf), &pos, "{\"path\":\"%s\",\"dirs\":[", path);

struct dirent *ent;
cbm_dirent_t *ent;
int count = 0;
while ((ent = readdir(dir)) != NULL) {
while ((ent = cbm_readdir(dir)) != NULL) {
/* Skip hidden dirs and . / .. */
if (ent->d_name[0] == '.')
if (ent->name[0] == '.')
continue;

/* Check if it's actually a directory */
char full[2048];
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
snprintf(full, sizeof(full), "%s/%s", path, ent->name);
if (!cbm_is_dir(full))
continue;

Expand All @@ -799,7 +798,7 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
/* Escape directory name to prevent XSS (e.g., names with quotes/angle brackets) */
{
char esc[512];
cbm_json_escape(esc, (int)sizeof(esc), ent->d_name);
cbm_json_escape(esc, (int)sizeof(esc), ent->name);
http_appendf(buf, sizeof(buf), &pos, "\"%s\"", esc);
}
if (pos >= (int)sizeof(buf)) {
Expand All @@ -810,7 +809,7 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
if (count >= 200)
break; /* safety limit */
}
closedir(dir);
cbm_closedir(dir);

/* Parent path — escape to prevent injection */
char parent[1024];
Expand Down
81 changes: 81 additions & 0 deletions tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -15426,6 +15426,81 @@ TEST(cli_build_args_json_repeated_array_issue680) {
PASS();
}

/* An array-typed flag also accepts the JSON-array spelling shown by the MCP
* schemas, expanding it into individual items instead of one opaque keyword. */
TEST(cli_build_args_json_array_literal) {
char *err = NULL;
char *argv[] = {"--semantic-query", "[\"create\",\"billing\",\"account\"]"};
char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"semantic_query\":[\"create\",\"billing\",\"account\"]") != NULL);
free(json);
PASS();
}

/* An explicit empty JSON array contributes no items; it must not turn into
* the literal keyword "[]". */
TEST(cli_build_args_json_array_literal_empty) {
char *err = NULL;
char *argv[] = {"--semantic-query", "[]"};
char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"semantic_query\":[]") != NULL);
ASSERT(strstr(json, "\"[]\"") == NULL);
free(json);
PASS();
}

/* Malformed JSON keeps the pre-existing behaviour: the value stays one
* literal string and no error is raised. */
TEST(cli_build_args_json_array_literal_malformed) {
char *err = NULL;
char *argv[] = {"--semantic-query", "[\"create\","};
char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err);
ASSERT_NOT_NULL(json);
ASSERT_NULL(err);
ASSERT(strstr(json, "\"semantic_query\":[\"[\\\"create\\\",\"]") != NULL);
free(json);
PASS();
}

/* Non-string items of a well-formed JSON array are copied as typed values,
* not stringified and not collapsed into one literal element. */
TEST(cli_build_args_json_array_literal_non_string_items) {
char *err = NULL;
char *argv[] = {"--semantic-query", "[1,2]"};
char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"semantic_query\":[1,2]") != NULL);
ASSERT(strstr(json, "\"[1,2]\"") == NULL);
free(json);
PASS();
}

/* Repeated flags and array literals may be mixed; items accumulate in
* argument order. */
TEST(cli_build_args_json_array_literal_mixed_with_repeated) {
char *err = NULL;
char *argv[] = {"--semantic-query", "send", "--semantic-query", "[\"publish\",\"emit\"]",
"--semantic-query", "notify"};
char *json = cbm_cli_build_args_json("search_graph", 6, argv, &err);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"semantic_query\":[\"send\",\"publish\",\"emit\",\"notify\"]") != NULL);
free(json);
PASS();
}

/* A repeated array literal accumulates into one array. */
TEST(cli_build_args_json_array_literal_repeated) {
char *err = NULL;
char *argv[] = {"--semantic-query", "[\"send\"]", "--semantic-query", "[\"publish\"]"};
char *json = cbm_cli_build_args_json("search_graph", 4, argv, &err);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"semantic_query\":[\"send\",\"publish\"]") != NULL);
free(json);
PASS();
}

/* kebab-case flag names map to snake_case JSON keys. */
TEST(cli_build_args_json_kebab_to_snake_issue680) {
char *err = NULL;
Expand Down Expand Up @@ -16560,6 +16635,12 @@ SUITE(cli) {
RUN_TEST(cli_build_args_json_array_flag_accepts_json_literal);
RUN_TEST(cli_build_args_json_unknown_flag_rejected);
RUN_TEST(cli_build_args_json_repeated_array_issue680);
RUN_TEST(cli_build_args_json_array_literal);
RUN_TEST(cli_build_args_json_array_literal_empty);
RUN_TEST(cli_build_args_json_array_literal_malformed);
RUN_TEST(cli_build_args_json_array_literal_non_string_items);
RUN_TEST(cli_build_args_json_array_literal_mixed_with_repeated);
RUN_TEST(cli_build_args_json_array_literal_repeated);
RUN_TEST(cli_build_args_json_kebab_to_snake_issue680);
RUN_TEST(cli_build_args_json_key_equals_value_issue680);
RUN_TEST(cli_build_args_json_bad_positional_errors_issue680);
Expand Down
48 changes: 48 additions & 0 deletions tests/test_daemon_application.c
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,53 @@ static bool app_env_backup_restore(app_env_backup_t *backup) {
return status == 0;
}

/* The Graph UI enters through the daemon's background coordinator rather than
* the MCP tool path. Keep that path UTF-8-safe as well. */
TEST(daemon_application_ui_index_accepts_non_ascii_directory) {
app_env_backup_t cache_environment;
bool cache_saved = app_env_backup_capture(&cache_environment, "CBM_CACHE_DIR");
char root[APP_TEST_PATH_CAP];
char cache[APP_TEST_PATH_CAP];
(void)snprintf(root, sizeof(root),
"%s/cbm-app-ui-index-\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E-XXXXXX", cbm_tmpdir());
(void)snprintf(cache, sizeof(cache), "%s/cbm-app-ui-index-cache-XXXXXX", cbm_tmpdir());
bool dirs_created = cbm_mkdtemp(root) != NULL && cbm_mkdtemp(cache) != NULL;
bool cache_set = cache_saved && dirs_created && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0;

app_fake_worker_context_t fake;
app_fake_worker_context_init(&fake);
atomic_store(&fake.allow_completion, true);
cbm_daemon_application_worker_ops_t worker_ops = {
.context = &fake,
.start = app_fake_worker_start,
.poll = app_fake_worker_poll,
.cancel = app_fake_worker_cancel,
.log_path = app_fake_worker_log_path,
.destroy = app_fake_worker_destroy,
};
cbm_daemon_application_config_t config = {.worker_ops = &worker_ops};
cbm_daemon_application_t *application = cache_set ? cbm_daemon_application_new(&config) : NULL;
int result = application ? cbm_daemon_application_index(application, "", root) : -1;
bool stopped = application && cbm_daemon_application_shutdown(application, APP_TEST_TIMEOUT_MS);
cbm_daemon_application_free(application);
bool root_cleaned = th_rmtree(root) == 0;
bool cache_cleaned = th_rmtree(cache) == 0;
bool cache_restored = app_env_backup_restore(&cache_environment);

ASSERT_TRUE(cache_saved);
ASSERT_TRUE(dirs_created);
ASSERT_TRUE(cache_set);
ASSERT_NOT_NULL(application);
ASSERT_EQ(result, 0);
ASSERT_EQ(atomic_load(&fake.starts), 1);
ASSERT_EQ(atomic_load(&fake.destroys), 1);
ASSERT_TRUE(stopped);
ASSERT_TRUE(root_cleaned);
ASSERT_TRUE(cache_cleaned);
ASSERT_TRUE(cache_restored);
PASS();
}

typedef struct {
atomic_int starts;
atomic_int start_failures_remaining;
Expand Down Expand Up @@ -5652,6 +5699,7 @@ SUITE(daemon_application) {
RUN_TEST(daemon_application_initialize_coalesces_auto_index_for_full_sessions);
RUN_TEST(daemon_application_sensitive_root_blocks_auto_index_but_preserves_controls);
RUN_TEST(daemon_application_sensitive_root_blocks_watch_but_preserves_controls);
RUN_TEST(daemon_application_ui_index_accepts_non_ascii_directory);
RUN_TEST(daemon_application_index_args_compare_repo_path_separator_equivalently);
RUN_TEST(daemon_application_programmatic_index_injects_resource_policy);
RUN_TEST(daemon_application_auto_index_honors_tracked_file_limit);
Expand Down
32 changes: 27 additions & 5 deletions tests/test_git_context.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* (input_path), not to worktree_root. Joining it with worktree_root and then
* string-stripping "/.git" left unresolved ".." components in the result.
*
* These tests shell out to `git`, so they SKIP_PLATFORM on Windows (the CI
* shell there cannot init a repo via system()).
* The canonical_root tests shell out to `git`, so they SKIP_PLATFORM on
* Windows (the CI shell there cannot init a repo via system()). The root-path
* existence test runs on every platform.
*
* Reproduce-first guard: canonical_root_subdir is the genuine RED-without-the-fix
* guard — a repo indexed from a subdirectory yields a relative --git-common-dir
Expand All @@ -34,9 +35,29 @@
#include <limits.h>
#endif

/* These helpers shell out to git and are only used by the non-Windows test
* bodies below; on Windows every test SKIP_PLATFORMs, so guard them here too or
* they'd be unused-static functions and fail the -Werror build. */
/* The Windows narrow stat() API interprets UTF-8 paths through the active ANSI
* code page. That made an existing Unicode repository root appear absent and
* stopped Git discovery before git was invoked (#1240). */
TEST(root_exists_non_ascii_directory) {
/* UTF-8 for "Japanese language", written as bytes to keep the source ASCII. */
char *tmp = th_mktempdir("cbm_gitctx_\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E");
if (!tmp)
FAIL("th_mktempdir returned NULL for non-ASCII path");

cbm_git_context_t ctx = {0};
int rc = cbm_git_context_resolve(tmp, &ctx);
bool root_exists = ctx.root_exists;
cbm_git_context_free(&ctx);
th_rmtree(tmp);

ASSERT_EQ(rc, 0);
ASSERT_TRUE(root_exists);
PASS();
}

/* These helpers shell out to git and are only used by the non-Windows
* canonical_root test bodies below; guard them here too or they'd be
* unused-static functions and fail the -Werror build on Windows. */
#ifndef _WIN32
/* Run a git command inside dir, return 0 on success. */
static int git_run(const char *dir, const char *args) {
Expand Down Expand Up @@ -235,6 +256,7 @@ TEST(canonical_root_linked_worktree) {
/* ── Suite ──────────────────────────────────────────────────────── */

SUITE(git_context) {
RUN_TEST(root_exists_non_ascii_directory);
RUN_TEST(canonical_root_repo_root);
RUN_TEST(canonical_root_subdir);
RUN_TEST(canonical_root_linked_worktree);
Expand Down
Loading
Loading