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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,16 @@ Invoke-WebRequest -Uri https://raw.githubusercontent.com/DeusData/codebase-memor
# 2. (Optional but recommended) Inspect the script
notepad install.ps1

# 3. Run it
# 3. Unblock the downloaded file (removes Mark-of-the-Web restriction added by browsers/Invoke-WebRequest)
Unblock-File .\install.ps1

# 4. Run it
.\install.ps1

```

> **Note:** If you see a script execution policy error, run `Set-ExecutionPolicy -Scope Process Bypass` first, or invoke with `PowerShell -ExecutionPolicy Bypass -File .\install.ps1`.

Options: `--ui` (graph visualization), `--skip-config` (binary only, no agent setup), `--dir=<path>` (custom location).

Restart your coding agent. Say **"Index this project"** — done.
Expand All @@ -86,6 +91,7 @@ Restart your coding agent. Say **"Index this project"** — done.
Windows (PowerShell):
```powershell
Expand-Archive codebase-memory-mcp-windows-amd64.zip -DestinationPath .
Unblock-File .\install.ps1
.\install.ps1
```

Expand Down
12 changes: 7 additions & 5 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ if ($SkipConfig) {
} else {
Write-Host ""
Write-Host "Configuring coding agents..."
try {
& $Dest install -y 2>&1 | Write-Host
} catch {
Write-Host "Agent configuration failed (non-fatal)."
Write-Host "Run manually: codebase-memory-mcp install"
& $Dest install -y 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "error: agent configuration failed (exit code $LASTEXITCODE)" -ForegroundColor Red
Write-Host "The binary was installed, but no coding agents were configured."
Write-Host "Run manually to configure: `"$Dest`" install"
exit 1
}
}

Expand Down
131 changes: 130 additions & 1 deletion src/foundation/compat_fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
#include "foundation/constants.h"
#include "foundation/compat_fs.h"
#include "foundation/compat_fs_internal.h"

#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -197,11 +198,139 @@ int cbm_rmdir(const char *path) {
return ret;
}

/* Build a properly-quoted Windows command line from an argv array.
* Returns a heap-allocated wide string, or NULL on allocation failure.
* Quoting follows the MSVC CRT convention: arguments containing spaces,
* tabs, or double-quotes are wrapped in double-quotes, with backslashes
* before a closing quote doubled and the quote itself escaped. Argument
* bytes are treated as UTF-8 and converted to wide via cbm_utf8_to_wide,
* so non-ASCII arguments (e.g. a non-ASCII %USERPROFILE%) survive intact.
* Declared in compat_fs_internal.h so the test suite can drive it. */
wchar_t *cbm_build_cmdline(const char *const *argv) {
/* First pass: compute required buffer size. */
size_t total = 1; /* NUL terminator */
for (int i = 0; argv[i]; i++) {
const char *arg = argv[i];
bool needs_quote = (arg[0] == '\0');
for (const char *p = arg; *p; p++) {
if (*p == ' ' || *p == '\t' || *p == '"') {
needs_quote = true;
}
}
if (i > 0) {
total++; /* space separator */
}
if (needs_quote) {
total += 2; /* opening and closing quote */
size_t backslashes = 0;
for (const char *p = arg; *p; p++) {
if (*p == '\\') {
backslashes++;
} else if (*p == '"') {
total += backslashes + 1; /* double backslashes + escape backslash */
backslashes = 0;
} else {
backslashes = 0;
}
total++;
}
/* Trailing backslashes before closing quote must be doubled. */
total += backslashes;
} else {
total += strlen(arg);
}
}

/* Build the quoted command line in UTF-8 first, then widen it as a
* whole via cbm_utf8_to_wide. Every character the quoting logic acts
* on (space, tab, '"', '\\') is ASCII and, by UTF-8's design, never
* appears inside a multibyte sequence, so operating on raw bytes here
* is safe and keeps multibyte argument bytes intact for conversion. */
char *buf = (char *)malloc(total);
if (!buf) {
return NULL;
}

/* Second pass: write the command line bytes. */
char *w = buf;
for (int i = 0; argv[i]; i++) {
const char *arg = argv[i];
bool needs_quote = (arg[0] == '\0');
for (const char *p = arg; *p; p++) {
if (*p == ' ' || *p == '\t' || *p == '"') {
needs_quote = true;
break;
}
}
if (i > 0) {
*w++ = ' ';
}
if (needs_quote) {
*w++ = '"';
size_t backslashes = 0;
for (const char *p = arg; *p; p++) {
if (*p == '\\') {
backslashes++;
*w++ = '\\';
} else if (*p == '"') {
/* Double the preceding backslashes, then escape the quote. */
for (size_t b = 0; b < backslashes; b++) {
*w++ = '\\';
}
*w++ = '\\';
*w++ = '"';
backslashes = 0;
} else {
backslashes = 0;
*w++ = *p;
}
}
/* Double trailing backslashes before the closing quote. */
for (size_t b = 0; b < backslashes; b++) {
*w++ = '\\';
}
*w++ = '"';
} else {
for (const char *p = arg; *p; p++) {
*w++ = *p;
}
}
}
*w = '\0';

wchar_t *out = cbm_utf8_to_wide(buf);
free(buf);
return out;
}

int cbm_exec_no_shell(const char *const *argv) {
if (!argv || !argv[0]) {
return CBM_NOT_FOUND;
}
return (int)_spawnvp(_P_WAIT, argv[0], argv);

wchar_t *cmdline = cbm_build_cmdline(argv);
if (!cmdline) {
return CBM_NOT_FOUND;
}

STARTUPINFOW si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
si.cb = sizeof(si);

if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
free(cmdline);
return CBM_NOT_FOUND;
}
free(cmdline);

WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exit_code = (DWORD)CBM_NOT_FOUND;
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return (int)exit_code;
}

#else /* POSIX */
Expand Down
36 changes: 36 additions & 0 deletions src/foundation/compat_fs_internal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* compat_fs_internal.h — Internal helpers exposed for testing.
*
* These functions are implementation details of compat_fs.c; they are
* declared here only so that the test suite can drive them directly.
* Production code outside compat_fs.c should use the public APIs in
* compat_fs.h instead.
*/
#ifndef CBM_FOUNDATION_COMPAT_FS_INTERNAL_H
#define CBM_FOUNDATION_COMPAT_FS_INTERNAL_H

#ifdef _WIN32

#include <wchar.h>

/*
* Build a properly-quoted Windows command line from a NULL-terminated
* argv array. This is the quoting step underlying cbm_exec_no_shell on
* Windows: it is what turns {"taskkill", "/FI", "IMAGENAME eq foo.exe"}
* into `taskkill /FI "IMAGENAME eq foo.exe"` rather than three bare
* tokens (the #697 regression).
*
* Quoting follows the MSVC/CommandLineToArgvW convention: an argument is
* wrapped in double-quotes when it is empty or contains a space, tab, or
* double-quote; backslashes immediately before a quote (literal or the
* closing one) are doubled, and embedded double-quotes are escaped with a
* backslash.
*
* Returns a heap-allocated wide string the caller must free(), or NULL on
* allocation failure.
*/
wchar_t *cbm_build_cmdline(const char *const *argv);

#endif /* _WIN32 */

#endif /* CBM_FOUNDATION_COMPAT_FS_INTERNAL_H */
Loading
Loading