diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2134331..bfb965e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ npm run test:smoke ## PR checklist - [ ] No changes to unrelated files -- [ ] Native build still succeeds (`-Wall -Wextra -Wpedantic`) +- [ ] Native build still succeeds (`-Wall -Wextra -Wpedantic` on Unix, supported Windows compiler path still works) - [ ] `npm run test:smoke` passes - [ ] Docs updated - [ ] Backward compatibility considered diff --git a/README.md b/README.md index ac2fbf2..1e7b705 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Durable memory for Pi agents, built for **low-overhead reliability**: - **C core** (`native/pi-memory.c`) for speed + minimal runtime surface - **TypeScript extension** (`extensions/pi-memory-compact.ts`) for Pi lifecycle automation -- **SQLite DB** (`~/.pi/memory/memory.db`) for single-file durability and simple ops +- **SQLite DB** (`~/.pi/memory/memory.db` on Unix, `%USERPROFILE%\\.pi\\memory\\memory.db` on Windows) for single-file durability and simple ops Repository: https://github.com/SiliconState/Pi-Memory @@ -18,7 +18,7 @@ Pi-Memory is intentionally small: - no server process - no external DB - no runtime framework dependency for core memory engine -- one local DB: `~/.pi/memory/memory.db` +- one local DB in your user `.pi/memory` directory That keeps startup fast, failure modes simple, and behavior predictable. @@ -30,14 +30,14 @@ Pi-Memory is designed as a layered system where each layer has a different job: | Layer | Responsibility | Location | |---|---|---| -| **1. Core memory engine (C + SQLite)** | Stores structured memory: decisions, findings, lessons, entities, sessions, project state | `~/.pi/memory/pi-memory` + `~/.pi/memory/memory.db` | +| **1. Core memory engine (C + SQLite)** | Stores structured memory: decisions, findings, lessons, entities, sessions, project state | user `.pi/memory` dir (`pi-memory`/`pi-memory.exe` + `memory.db`) | | **2. Pi lifecycle extension (TypeScript)** | Hooks into compaction/session events, syncs memory, ingests sessions, resumes intent | `extensions/pi-memory-compact.ts` (installed by Pi package) | | **3. Project memory files (`MEMORY.md`)** | Human-readable, per-project snapshot used as context bridge across sessions | project root `MEMORY.md` with sync markers | Check current DB coverage at any time: ```bash -~/.pi/memory/pi-memory projects +pi-memory projects ``` --- @@ -81,7 +81,7 @@ This installs all Pi components declared in `package.json`: - prompts (`prompts/*.md`) - native C binary compile via postinstall -### Alternative one-liner installer (curl) +### Alternative one-liner installer (Unix only) ```bash curl -fsSL https://raw.githubusercontent.com/SiliconState/Pi-Memory/main/scripts/install.sh | bash @@ -90,20 +90,23 @@ curl -fsSL https://raw.githubusercontent.com/SiliconState/Pi-Memory/main/scripts Then verify: ```bash -~/.pi/memory/pi-memory --version +pi-memory --version ``` -> SQLite is bundled — the only build prerequisite is a C compiler (`cc`/`gcc`/`clang`). -> If native compile is skipped during install, rerun the curl installer or follow manual compile troubleshooting in `docs/INSTALL.md`. +> SQLite is bundled — the only build prerequisite is a C compiler. +> - Unix: `cc` / `gcc` / `clang` +> - Windows: `clang`, `gcc`, or Visual Studio `cl` > -> npm/bun publish is planned but not currently live. For now, use the git install (or curl wrapper) above. +> If native compile is skipped during install, follow the manual compile troubleshooting in `docs/INSTALL.md`. +> +> npm/bun publish is planned but not currently live. For now, use the git install above. --- ## Quick Start ```bash -BIN="${PI_MEMORY_BIN:-$HOME/.pi/memory/pi-memory}" +BIN="${PI_MEMORY_BIN:-pi-memory}" # initialize MEMORY.md markers "$BIN" init @@ -140,8 +143,8 @@ Inside Pi, you can manually tune compaction behavior without changing code: If you want this change recorded in project memory for teammates/future sessions: ```bash -~/.pi/memory/pi-memory state --summary "Compaction threshold set to 75%" -~/.pi/memory/pi-memory sync MEMORY.md --project --limit 15 +pi-memory state --summary "Compaction threshold set to 75%" +pi-memory sync MEMORY.md --project --limit 15 ``` --- diff --git a/bin/pi-memory.js b/bin/pi-memory.js index ab30417..3ff0c6a 100755 --- a/bin/pi-memory.js +++ b/bin/pi-memory.js @@ -9,7 +9,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, ".."); const setupScript = path.join(root, "scripts", "setup.mjs"); -const binary = process.env.PI_MEMORY_BIN?.trim() || path.join(os.homedir(), ".pi", "memory", "pi-memory"); +const defaultBinary = os.platform() === "win32" ? "pi-memory.exe" : "pi-memory"; +const binary = process.env.PI_MEMORY_BIN?.trim() || path.join(os.homedir(), ".pi", "memory", defaultBinary); if (!existsSync(binary)) { const setup = spawnSync(process.execPath, [setupScript], { stdio: "inherit" }); diff --git a/docs/AGENT-USAGE.md b/docs/AGENT-USAGE.md index b1fccd2..e425d41 100644 --- a/docs/AGENT-USAGE.md +++ b/docs/AGENT-USAGE.md @@ -2,7 +2,9 @@ This guide is for agent authors/operators using Pi-Memory in Pi workflows. -If `pi-memory` is not on PATH in your environment, call it explicitly as `~/.pi/memory/pi-memory`. +If `pi-memory` is not on PATH in your environment, call the installed binary directly: +- macOS / Linux: `~/.pi/memory/pi-memory` +- Windows: `%USERPROFILE%\\.pi\\memory\\pi-memory.exe` ## Core operating rules diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3f175ac..5bd5973 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -15,7 +15,8 @@ The C binary is the source of truth for persistence. SQLite database: ```text -~/.pi/memory/memory.db +~/.pi/memory/memory.db # macOS / Linux +%USERPROFILE%\\.pi\\memory\\memory.db # Windows ``` Primary tables: @@ -69,7 +70,7 @@ The extension automates operational memory hygiene around compaction and shutdow The packaged extension resolves pi-memory in this order: 1. `PI_MEMORY_BIN` env var -2. `~/.pi/memory/pi-memory` +2. default binary in the user `.pi/memory` dir (`pi-memory` on Unix, `pi-memory.exe` on Windows) 3. fallback command `pi-memory` in PATH ## MEMORY.md bridge layer diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 2f8846a..edae1ea 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -3,14 +3,16 @@ ## Platform support - **macOS** and **Linux** are supported. -- **Windows** is not currently supported (no native build target). +- **Windows** is supported with a native Windows C compiler (`clang`, `gcc`, or `cl`). ## Prerequisites - Pi installed (`pi` CLI) -- C compiler (`cc`/`clang`/`gcc`) +- C compiler + - macOS/Linux: `cc` / `clang` / `gcc` + - Windows: `clang`, `gcc`, or Visual Studio `cl` -> SQLite is bundled (amalgamation compiled directly into the binary). No system `libsqlite3-dev` or `sqlite3.h` needed. +> SQLite is bundled (amalgamation compiled directly into the binary). No system `libsqlite3-dev`, `sqlite3.h`, or external database server is required. ## Option A (recommended): single Pi command @@ -18,80 +20,133 @@ pi install git:github.com/SiliconState/Pi-Memory ``` -This installs package resources declared in `package.json` (`pi.extensions`, `pi.skills`, `pi.prompts`) and runs native build postinstall. +This installs package resources declared in `package.json` (`pi.extensions`, `pi.skills`, `pi.prompts`) and runs the native build in `postinstall`. Verify: +### macOS / Linux ```bash ~/.pi/memory/pi-memory --version ``` -## Option B: single curl command +### Windows (PowerShell) +```powershell +$env:USERPROFILE\.pi\memory\pi-memory.exe --version +``` + +## Option B: Unix convenience installer ```bash curl -fsSL https://raw.githubusercontent.com/SiliconState/Pi-Memory/main/scripts/install.sh | bash ``` -The installer script runs `pi install ...` and falls back to direct compile if needed. +The shell installer is **Unix-only**. On Windows, use Option A or the manual compile step below. ## npm/bun status npm/bun publish is planned but not currently live. -Use Option A (`pi install git:...`) or Option B (curl installer) for now. +Use Option A (`pi install git:...`) for now. ## Native build output -Default binary location: - +### macOS / Linux ```text ~/.pi/memory/pi-memory +~/.pi/memory/memory.db ``` -Database: - +### Windows ```text -~/.pi/memory/memory.db +%USERPROFILE%\.pi\memory\pi-memory.exe +%USERPROFILE%\.pi\memory\memory.db ``` Override binary path with: +### macOS / Linux ```bash export PI_MEMORY_BIN=/custom/path/pi-memory ``` +### Windows (PowerShell) +```powershell +$env:PI_MEMORY_BIN = 'C:\custom\path\pi-memory.exe' +``` + ## Troubleshooting ### `Failed to compile pi-memory` -Install build prerequisites and rerun the installer: +Install a basic C compiler and rerun: ```bash pi install git:github.com/SiliconState/Pi-Memory ``` -If needed, compile manually: +Or from the package directory: + +```bash +npm run setup +``` + +### Manual compile — macOS / Linux ```bash PKG="$HOME/.pi/agent/git/github.com/SiliconState/Pi-Memory" [ -d "$PKG" ] || PKG="$HOME/.pi/git/github.com/SiliconState/Pi-Memory" mkdir -p "$HOME/.pi/memory" cc -Wall -Wextra -Wpedantic -O2 -std=c11 -o "$HOME/.pi/memory/pi-memory" \ - "$PKG/native/pi-memory.c" "$PKG/native/sqlite3.c" -lpthread -ldl -lm + "$PKG/native/pi-memory.c" "$PKG/native/sqlite3.c" "$PKG/native/getopt_compat.c" -lpthread -ldl -lm chmod +x "$HOME/.pi/memory/pi-memory" ``` +### Manual compile — Windows (PowerShell + clang/gcc) + +```powershell +$pkg = "$env:USERPROFILE\.pi\agent\git\github.com\SiliconState\Pi-Memory" +if (-not (Test-Path $pkg)) { $pkg = (Resolve-Path ".\Pi-Memory").Path } +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.pi\memory" | Out-Null +clang -D_CRT_SECURE_NO_WARNINGS -Wall -Wextra -Wpedantic -O2 -std=c11 ` + -o "$env:USERPROFILE\.pi\memory\pi-memory.exe" ` + "$pkg\native\pi-memory.c" "$pkg\native\sqlite3.c" "$pkg\native\getopt_compat.c" +``` + +### Manual compile — Windows (Developer PowerShell + MSVC `cl`) + +```powershell +$pkg = "$env:USERPROFILE\.pi\agent\git\github.com\SiliconState\Pi-Memory" +if (-not (Test-Path $pkg)) { $pkg = (Resolve-Path ".\Pi-Memory").Path } +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.pi\memory" | Out-Null +cl /nologo /D_CRT_SECURE_NO_WARNINGS /O2 /W4 /EHsc ` + /Fe:"$env:USERPROFILE\.pi\memory\pi-memory.exe" ` + "$pkg\native\pi-memory.c" "$pkg\native\sqlite3.c" "$pkg\native\getopt_compat.c" +``` + ### Extension can’t find binary Set explicit path: +#### macOS / Linux ```bash export PI_MEMORY_BIN="$HOME/.pi/memory/pi-memory" ``` +#### Windows (PowerShell) +```powershell +$env:PI_MEMORY_BIN = "$env:USERPROFILE\.pi\memory\pi-memory.exe" +``` + ### Check health quickly +#### macOS / Linux ```bash ~/.pi/memory/pi-memory --version ~/.pi/memory/pi-memory help ``` + +#### Windows (PowerShell) +```powershell +$env:USERPROFILE\.pi\memory\pi-memory.exe --version +$env:USERPROFILE\.pi\memory\pi-memory.exe help +``` diff --git a/extensions/pi-memory-compact.ts b/extensions/pi-memory-compact.ts index f933c9c..e58d8c4 100644 --- a/extensions/pi-memory-compact.ts +++ b/extensions/pi-memory-compact.ts @@ -13,6 +13,8 @@ import { complete, type Model } from "@mariozechner/pi-ai"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent"; +import { existsSync, readFileSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; const DEFAULT_COMPACT_THRESHOLD = parseThresholdValue(process.env.PI_COMPACT_THRESHOLD, 0.6); // 60% default @@ -74,6 +76,19 @@ function formatThresholdPercent(ratio: number): string { } function getProjectKey(cwd: string): string { + const pinned = process.env.PI_MEMORY_PROJECT?.trim(); + if (pinned) return pinned; + + try { + const memoryFile = path.join(cwd, "MEMORY.md"); + if (existsSync(memoryFile)) { + const header = readFileSync(memoryFile, "utf8").match(/^#\s+Memory\s+[—-]\s+(.+)$/m)?.[1]?.trim(); + if (header) return header; + } + } catch { + // Non-fatal; fall back to cwd basename. + } + return path.basename(cwd); } @@ -82,7 +97,8 @@ async function execPiMemory( args: string[], options?: { timeout?: number; signal?: AbortSignal } ) { - const preferred = process.env.PI_MEMORY_BIN?.trim() || path.join(process.env.HOME ?? "", ".pi", "memory", "pi-memory"); + const defaultBinary = process.platform === "win32" ? "pi-memory.exe" : "pi-memory"; + const preferred = process.env.PI_MEMORY_BIN?.trim() || path.join(os.homedir(), ".pi", "memory", defaultBinary); if (preferred) { const result = await pi.exec(preferred, args, options); diff --git a/native/Makefile b/native/Makefile index d9c7cc6..27c2050 100644 --- a/native/Makefile +++ b/native/Makefile @@ -7,7 +7,7 @@ CC = cc CFLAGS = -Wall -Wextra -Wpedantic -O2 -std=c11 TARGET = pi-memory -SRC = pi-memory.c sqlite3.c +SRC = pi-memory.c sqlite3.c getopt_compat.c INSTALL_DIR = $(HOME)/.pi/memory $(TARGET): $(SRC) sqlite3.h diff --git a/native/getopt_compat.c b/native/getopt_compat.c new file mode 100644 index 0000000..6709b9c --- /dev/null +++ b/native/getopt_compat.c @@ -0,0 +1,164 @@ +#ifdef _WIN32 + +#include "getopt_compat.h" + +#include + +char *optarg = NULL; +int optind = 1; +int opterr = 1; +int optopt = 0; +int optreset = 0; + +static int short_index = 0; +static int scan_index = 1; +static int current_index = -1; +static int first_nonopt = -1; + +static void reset_parser_state(void) { + short_index = 0; + scan_index = optind > 0 ? optind : 1; + current_index = -1; + first_nonopt = -1; + optarg = NULL; +} + +static int find_short_option(const char *optstring, char c) { + const char *p = optstring; + while (*p) { + if (*p == c) return (int)(p - optstring); + p++; + } + return -1; +} + +static int parse_long_option(int argc, char * const argv[], const struct option *longopts, int *longindex) { + char *arg = argv[current_index] + 2; + char *value = strchr(arg, '='); + size_t name_len = value ? (size_t)(value - arg) : strlen(arg); + + if (value) *value++ = '\0'; + + for (int i = 0; longopts && longopts[i].name; i++) { + if (strlen(longopts[i].name) == name_len && strncmp(longopts[i].name, arg, name_len) == 0) { + if (longindex) *longindex = i; + + if (longopts[i].has_arg == required_argument) { + if (value && *value) { + optarg = value; + scan_index = current_index + 1; + } else if (current_index + 1 < argc) { + optarg = argv[current_index + 1]; + scan_index = current_index + 2; + } else { + scan_index = current_index + 1; + current_index = -1; + optopt = longopts[i].val; + return '?'; + } + } else if (longopts[i].has_arg == optional_argument) { + optarg = value; + scan_index = current_index + 1; + } else { + if (value) { + scan_index = current_index + 1; + current_index = -1; + optopt = longopts[i].val; + return '?'; + } + scan_index = current_index + 1; + } + + current_index = -1; + if (longopts[i].flag) { + *longopts[i].flag = longopts[i].val; + return 0; + } + return longopts[i].val; + } + } + + scan_index = current_index + 1; + current_index = -1; + return '?'; +} + +int getopt_long(int argc, char * const argv[], const char *optstring, + const struct option *longopts, int *longindex) { + optarg = NULL; + + if (optreset) { + optreset = 0; + reset_parser_state(); + } + + if (optind <= 0) optind = 1; + if (scan_index <= 0) scan_index = optind; + + while (current_index == -1) { + if (scan_index >= argc) { + optind = first_nonopt >= 0 ? first_nonopt : scan_index; + return -1; + } + + char *arg = argv[scan_index]; + if (!arg || arg[0] != '-' || arg[1] == '\0') { + if (first_nonopt < 0) first_nonopt = scan_index; + scan_index++; + continue; + } + + if (strcmp(arg, "--") == 0) { + scan_index++; + optind = first_nonopt >= 0 ? first_nonopt : scan_index; + return -1; + } + + current_index = scan_index; + if (arg[1] == '-') { + return parse_long_option(argc, argv, longopts, longindex); + } + short_index = 1; + } + + char *arg = argv[current_index]; + char c = arg[short_index++]; + int idx = find_short_option(optstring, c); + if (idx < 0) { + if (arg[short_index] == '\0') { + scan_index = current_index + 1; + current_index = -1; + short_index = 0; + } + optopt = c; + return '?'; + } + + if (optstring[idx + 1] == ':') { + if (arg[short_index] != '\0') { + optarg = &arg[short_index]; + scan_index = current_index + 1; + current_index = -1; + short_index = 0; + } else if (current_index + 1 < argc) { + optarg = argv[current_index + 1]; + scan_index = current_index + 2; + current_index = -1; + short_index = 0; + } else { + scan_index = current_index + 1; + current_index = -1; + short_index = 0; + optopt = c; + return '?'; + } + } else if (arg[short_index] == '\0') { + scan_index = current_index + 1; + current_index = -1; + short_index = 0; + } + + return c; +} + +#endif diff --git a/native/getopt_compat.h b/native/getopt_compat.h new file mode 100644 index 0000000..ffabe46 --- /dev/null +++ b/native/getopt_compat.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef _WIN32 + +#ifdef __cplusplus +extern "C" { +#endif + +#define no_argument 0 +#define required_argument 1 +#define optional_argument 2 + +struct option { + const char *name; + int has_arg; + int *flag; + int val; +}; + +extern char *optarg; +extern int optind; +extern int opterr; +extern int optopt; +extern int optreset; + +int getopt_long(int argc, char * const argv[], const char *optstring, + const struct option *longopts, int *longindex); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/native/pi-memory.c b/native/pi-memory.c index 7c847f4..671881a 100644 --- a/native/pi-memory.c +++ b/native/pi-memory.c @@ -1,6 +1,8 @@ +#ifndef _WIN32 #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif +#endif /* * pi-memory — durable agent memory store @@ -9,7 +11,7 @@ * Single binary. Works 6 months from now. Works 6 years from now. * * Build: make - * Install: make install (copies to ~/.pi/memory/pi-memory) + * Install: make install (copies to ~/.pi/memory/pi-memory[.exe]) * * Usage: * pi-memory log decision --choice <str> [--context <str>] @@ -44,17 +46,28 @@ #include <ctype.h> #include <errno.h> -#include <getopt.h> -#include <pwd.h> #include "sqlite3.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <direct.h> +#include <io.h> +#include "getopt_compat.h" +#define popen _popen +#define pclose _pclose +#define getcwd _getcwd +#else +#include <getopt.h> +#include <pwd.h> #include <unistd.h> +#endif -#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(_WIN32) extern int optreset; #endif @@ -66,6 +79,14 @@ extern int optreset; #define DEFAULT_LIMIT 20 #define DEFAULT_N 10 +#ifdef _WIN32 +#define PATH_SEP_CH '\\' +#define PATH_SEP_STR "\\" +#else +#define PATH_SEP_CH '/' +#define PATH_SEP_STR "/" +#endif + /* ───────────────────────────────────────────────────────────────── Schema ───────────────────────────────────────────────────────────────── */ @@ -150,35 +171,96 @@ static const char *SCHEMA = Utilities ───────────────────────────────────────────────────────────────── */ -static void get_memory_dir(char *out, size_t size) { +static int is_path_sep(char c) { + return c == '/' || c == '\\'; +} + +static int path_mkdir(const char *path) { +#ifdef _WIN32 + return _mkdir(path); +#else + return mkdir(path, 0755); +#endif +} + +static void normalize_path(char *path) { +#ifdef _WIN32 + for (char *p = path; *p; p++) { + if (*p == '/') *p = '\\'; + } +#else + (void)path; +#endif +} + +static const char *path_basename_ptr(const char *path) { + if (!path || !*path) return path; + const char *p = path + strlen(path); + while (p > path && is_path_sep(*(p - 1))) p--; + while (p > path && !is_path_sep(*(p - 1))) p--; + return p; +} + +static void get_home_dir(char *out, size_t size) { const char *home = getenv("HOME"); - if (!home) { +#ifdef _WIN32 + if (!home || !*home) home = getenv("USERPROFILE"); + if ((!home || !*home)) { + const char *drive = getenv("HOMEDRIVE"); + const char *path = getenv("HOMEPATH"); + if (drive && *drive && path && *path) { + snprintf(out, size, "%s%s", drive, path); + normalize_path(out); + return; + } + } + if (!home || !*home) home = getenv("TEMP"); + if (!home || !*home) home = "."; +#else + if (!home || !*home) { struct passwd *pw = getpwuid(getuid()); home = (pw && pw->pw_dir) ? pw->pw_dir : "/tmp"; } - snprintf(out, size, "%s/.pi/memory", home); +#endif + snprintf(out, size, "%s", home); + normalize_path(out); +} + +static void get_memory_dir(char *out, size_t size) { + char home[MAX_PATH]; + get_home_dir(home, sizeof(home)); + snprintf(out, size, "%s%s.pi%smemory", home, PATH_SEP_STR, PATH_SEP_STR); } static void get_db_path(char *out, size_t size) { char dir[MAX_PATH]; get_memory_dir(dir, sizeof(dir)); - snprintf(out, size, "%s/memory.db", dir); + snprintf(out, size, "%s%smemory.db", dir, PATH_SEP_STR); } static int ensure_dir(const char *path) { char tmp[MAX_PATH]; snprintf(tmp, sizeof(tmp), "%s", path); + normalize_path(tmp); + size_t len = strlen(tmp); - if (len > 0 && tmp[len - 1] == '/') tmp[len - 1] = '\0'; + if (len > 0 && is_path_sep(tmp[len - 1])) tmp[len - 1] = '\0'; - for (char *p = tmp + 1; *p; p++) { - if (*p == '/') { + char *start = tmp + 1; +#ifdef _WIN32 + if (isalpha((unsigned char)tmp[0]) && tmp[1] == ':' && is_path_sep(tmp[2])) { + start = tmp + 3; + } +#endif + + for (char *p = start; *p; p++) { + if (is_path_sep(*p)) { *p = '\0'; - if (mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1; - *p = '/'; + if (path_mkdir(tmp) != 0 && errno != EEXIST) return -1; + *p = PATH_SEP_CH; } } - if (mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1; + if (path_mkdir(tmp) != 0 && errno != EEXIST) return -1; return 0; } @@ -400,9 +482,7 @@ static int jx_double_after(const char *line, const char *after_key, const char * */ static void project_from_cwd(const char *cwd, char *out, size_t size) { if (!cwd || !*cwd) { snprintf(out, size, "global"); return; } - const char *p = cwd + strlen(cwd); - while (p > cwd && *(p - 1) != '/') p--; - snprintf(out, size, "%s", p); + snprintf(out, size, "%s", path_basename_ptr(cwd)); } /* ───────────────────────────────────────────────────────────────── @@ -431,17 +511,21 @@ static const char *auto_project(void) { } /* 2. git remote origin */ +#ifdef _WIN32 + FILE *fp = popen("git remote get-url origin 2>NUL", "r"); +#else FILE *fp = popen("git remote get-url origin 2>/dev/null", "r"); +#endif if (fp) { char raw[512] = {0}; if (fgets(raw, sizeof(raw), fp) && raw[0]) { /* strip trailing newline / .git */ size_t len = strlen(raw); - if (len > 0 && raw[len-1] == '\n') raw[--len] = '\0'; + while (len > 0 && (raw[len - 1] == '\n' || raw[len - 1] == '\r')) raw[--len] = '\0'; if (len > 4 && strcmp(raw + len - 4, ".git") == 0) raw[len-=4] = '\0'; - /* take component after last '/' or ':' */ + /* take component after last path separator or ':' */ char *p = raw + len; - while (p > raw && *(p-1) != '/' && *(p-1) != ':') p--; + while (p > raw && !is_path_sep(*(p - 1)) && *(p - 1) != ':') p--; if (*p) { snprintf(buf, sizeof(buf), "%s", p); pclose(fp); @@ -454,11 +538,8 @@ static const char *auto_project(void) { /* 3. basename of cwd */ char cwd[MAX_PATH]; if (getcwd(cwd, sizeof(cwd))) { - char *p = strrchr(cwd, '/'); - if (p && *(p+1)) { - snprintf(buf, sizeof(buf), "%s", p+1); - return buf; - } + snprintf(buf, sizeof(buf), "%s", path_basename_ptr(cwd)); + if (buf[0]) return buf; } /* 4. fallback */ @@ -1993,7 +2074,7 @@ static int cmd_init(int argc, char *argv[]) { fprintf(f, "# Memory — %s\n" "\n" -"> Durable knowledge base for this project, backed by `pi-memory` (SQLite at `~/.pi/memory/memory.db`).\n" +"> Durable knowledge base for this project, backed by `pi-memory` (SQLite in your user `.pi/memory` directory).\n" ">\n" "> **Refresh live sections:**\n" "> ```bash\n" @@ -2385,17 +2466,26 @@ static int cmd_ingest_session(int argc, char *argv[]) { int line_num = 0; /* ── Semantic extraction buckets ── */ - AutoDecision decisions[MAX_AUTO_DECISIONS]; + AutoDecision *decisions = calloc(MAX_AUTO_DECISIONS, sizeof(AutoDecision)); int decision_count = 0; - AutoLesson lessons[MAX_AUTO_LESSONS]; + AutoLesson *lessons = calloc(MAX_AUTO_LESSONS, sizeof(AutoLesson)); int lesson_count = 0; - AutoEntity entities[MAX_AUTO_ENTITIES]; + AutoEntity *entities = calloc(MAX_AUTO_ENTITIES, sizeof(AutoEntity)); int entity_count = 0; char recent_error[600] = {0}; + if (!decisions || !lessons || !entities) { + fprintf(stderr, "error: out of memory allocating ingest buffers\n"); + free(decisions); + free(lessons); + free(entities); + fclose(f); + return 1; + } + /* ── Compaction summaries to store ── */ #define MAX_COMPACTIONS 128 - char *compaction_summaries[MAX_COMPACTIONS]; + char *compaction_summaries[MAX_COMPACTIONS] = {0}; int comp_idx = 0; /* ── Parse line by line ── */ @@ -2543,6 +2633,9 @@ static int cmd_ingest_session(int argc, char *argv[]) { if (!session_id[0]) { fprintf(stderr, "error: no session header found in '%s'\n", filepath); for (int i = 0; i < comp_idx; i++) free(compaction_summaries[i]); + free(decisions); + free(lessons); + free(entities); return 1; } @@ -2564,12 +2657,18 @@ static int cmd_ingest_session(int argc, char *argv[]) { if (dry_run) { printf("\n [DRY RUN — nothing written]\n\n"); for (int i = 0; i < comp_idx; i++) free(compaction_summaries[i]); + free(decisions); + free(lessons); + free(entities); return 0; } sqlite3 *db = open_db(); if (!db) { for (int i = 0; i < comp_idx; i++) free(compaction_summaries[i]); + free(decisions); + free(lessons); + free(entities); return 1; } @@ -2759,6 +2858,9 @@ static int cmd_ingest_session(int argc, char *argv[]) { sqlite3_close(db); for (int i = 0; i < comp_idx; i++) free(compaction_summaries[i]); + free(decisions); + free(lessons); + free(entities); printf("\n done.\n\n"); return 0; @@ -2844,7 +2946,7 @@ static int cmd_sessions(int argc, char *argv[]) { static void usage(void) { printf( "pi-memory v" VERSION " — durable agent memory store\n" - " db: ~/.pi/memory/memory.db\n" + " db: ~/.pi/memory/memory.db (Unix) | %%USERPROFILE%%\\.pi\\memory\\memory.db (Windows)\n" " project: auto-detected from PI_MEMORY_PROJECT | git remote | cwd | 'global'\n\n" " Commands:\n" " init [<project>] [--file <path>] bootstrap MEMORY.md for any project\n" diff --git a/scripts/doctor.mjs b/scripts/doctor.mjs index d2db5b9..52e6894 100755 --- a/scripts/doctor.mjs +++ b/scripts/doctor.mjs @@ -19,7 +19,8 @@ function check(name, fn) { } } -const binary = process.env.PI_MEMORY_BIN?.trim() || path.join(os.homedir(), ".pi", "memory", "pi-memory"); +const defaultBinary = os.platform() === "win32" ? "pi-memory.exe" : "pi-memory"; +const binary = process.env.PI_MEMORY_BIN?.trim() || path.join(os.homedir(), ".pi", "memory", defaultBinary); check("native source present", () => { if (!existsSync(path.join(root, "native", "pi-memory.c"))) throw new Error("native/pi-memory.c missing"); @@ -35,7 +36,7 @@ check("skill present", () => { check("binary installed", () => { if (!existsSync(binary)) throw new Error(`missing binary: ${binary}`); - accessSync(binary, constants.X_OK); + if (os.platform() !== "win32") accessSync(binary, constants.X_OK); }); check("binary runnable", () => { diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs index b3728e0..e4a6749 100755 --- a/scripts/postinstall.mjs +++ b/scripts/postinstall.mjs @@ -1,10 +1,12 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const setupScript = path.join(__dirname, "setup.mjs"); +const defaultBinary = os.platform() === "win32" ? "~/.pi/memory/pi-memory.exe" : "~/.pi/memory/pi-memory"; const result = spawnSync(process.execPath, [setupScript, "--quiet"], { stdio: "pipe", @@ -12,13 +14,13 @@ const result = spawnSync(process.execPath, [setupScript, "--quiet"], { }); if (result.status === 0) { - console.log("[pi-memory] native binary installed in ~/.pi/memory/pi-memory"); + console.log(`[pi-memory] native binary installed in ${defaultBinary}`); process.exit(0); } console.warn("[pi-memory] postinstall compile skipped/failed."); if (result.stderr?.trim()) console.warn(result.stderr.trim()); -console.warn("Install a C compiler (cc/gcc/clang), then run:"); +console.warn("Install a C compiler (Unix: cc/gcc/clang, Windows: clang/gcc/cl), then run:"); console.warn(" npm run setup"); console.warn("from the pi-memory package directory (typically ~/.pi/agent/git/github.com/SiliconState/Pi-Memory)."); process.exit(0); diff --git a/scripts/setup.mjs b/scripts/setup.mjs index 956e1ab..26e1431 100755 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; -import { chmodSync, existsSync, mkdirSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, readdirSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -12,9 +12,10 @@ const quiet = process.argv.includes("--quiet"); const nativeDir = path.join(root, "native"); const source = path.join(nativeDir, "pi-memory.c"); const sqliteSource = path.join(nativeDir, "sqlite3.c"); +const getoptCompat = path.join(nativeDir, "getopt_compat.c"); const outDir = path.join(os.homedir(), ".pi", "memory"); -const outBin = process.env.PI_MEMORY_BIN?.trim() || path.join(outDir, "pi-memory"); -const cc = process.env.CC || "cc"; +const defaultBinName = os.platform() === "win32" ? "pi-memory.exe" : "pi-memory"; +const outBin = process.env.PI_MEMORY_BIN?.trim() || path.join(outDir, defaultBinName); function log(message) { if (!quiet) console.log(message); @@ -24,48 +25,148 @@ function run(command, args) { return spawnSync(command, args, { stdio: quiet ? "pipe" : "inherit", encoding: "utf8", + shell: false, }); } +function quoteIfNeeded(value) { + return /\s/.test(value) ? `"${value}"` : value; +} + +function unique(values) { + return [...new Set(values.filter(Boolean))]; +} + +function collectWinGetCompilers(binaryName, packagePrefix) { + const packagesDir = path.join(os.homedir(), "AppData", "Local", "Microsoft", "WinGet", "Packages"); + if (!existsSync(packagesDir)) return []; + + const hits = []; + for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.startsWith(packagePrefix)) continue; + const packageRoot = path.join(packagesDir, entry.name); + for (const child of readdirSync(packageRoot, { withFileTypes: true })) { + if (!child.isDirectory()) continue; + const candidate = path.join(packageRoot, child.name, "bin", binaryName); + if (existsSync(candidate)) hits.push(candidate); + } + } + return hits; +} + +function discoverWindowsCompilers(binaryName) { + return unique([ + ...collectWinGetCompilers(binaryName, "MartinStorsjo.LLVM-MinGW.UCRT"), + ...collectWinGetCompilers(binaryName, "MartinStorsjo.LLVM-MinGW.MSVCRT"), + path.join("C:", "Program Files", "LLVM", "bin", binaryName), + path.join("C:", "Program Files", "LLVM", "bin", binaryName.replace(/\.exe$/i, "-22.exe")), + ]); +} + +function createCandidates() { + const env = process.env.CC?.trim(); + const platform = os.platform(); + + if (platform === "win32") { + const envCandidate = env + ? { command: env, args: env.toLowerCase().endsWith("cl") || env.toLowerCase() === "cl" ? msvcArgs() : gccStyleArgs(env) } + : null; + + const discoveredClang = discoverWindowsCompilers("clang.exe").map((command) => ({ command, args: gccStyleArgs(command) })); + const discoveredGcc = discoverWindowsCompilers("gcc.exe").map((command) => ({ command, args: gccStyleArgs(command) })); + + return [ + envCandidate, + ...discoveredClang, + ...discoveredGcc, + { command: "clang", args: gccStyleArgs("clang") }, + { command: "gcc", args: gccStyleArgs("gcc") }, + { command: "cc", args: gccStyleArgs("cc") }, + { command: "cl", args: msvcArgs() }, + ].filter(Boolean); + } + + return [ + { command: env || "cc", args: unixArgs() }, + ...(env ? [] : [{ command: "clang", args: unixArgs() }, { command: "gcc", args: unixArgs() }]), + ]; +} + +function unixArgs() { + const platform = os.platform(); + if (platform === "darwin") { + return ["-Wall", "-Wextra", "-Wpedantic", "-O2", "-std=c11", "-o", outBin, source, sqliteSource, getoptCompat, "-lm"]; + } + return ["-Wall", "-Wextra", "-Wpedantic", "-O2", "-std=c11", "-o", outBin, source, sqliteSource, getoptCompat, "-lpthread", "-ldl", "-lm"]; +} + +function gccStyleArgs(_command) { + return [ + "-D_CRT_SECURE_NO_WARNINGS", + "-Wall", + "-Wextra", + "-Wpedantic", + "-O2", + "-std=c11", + "-o", + outBin, + source, + sqliteSource, + getoptCompat, + ]; +} + +function msvcArgs() { + return [ + "/nologo", + "/D_CRT_SECURE_NO_WARNINGS", + "/O2", + "/W4", + "/EHsc", + `/Fe:${outBin}`, + source, + sqliteSource, + getoptCompat, + ]; +} + mkdirSync(path.dirname(outBin), { recursive: true }); -// Bundled SQLite amalgamation — no system sqlite3-dev required. -// Only needs: C compiler + pthreads + dl + math (standard on Linux/macOS). -const compileArgs = [ - "-Wall", - "-Wextra", - "-Wpedantic", - "-O2", - "-std=c11", - "-o", - outBin, - source, - sqliteSource, - "-lpthread", - "-ldl", - "-lm", -]; - -// macOS doesn't need -ldl or -lpthread (included in system libs), -// and passing them may warn. Use platform-appropriate flags. -const platform = os.platform(); -const platformArgs = platform === "darwin" - ? ["-Wall", "-Wextra", "-Wpedantic", "-O2", "-std=c11", "-o", outBin, source, sqliteSource, "-lm"] - : compileArgs; - -log(`Compiling pi-memory -> ${outBin}`); -const result = run(cc, platformArgs); - -if (result.status !== 0) { - const details = [result.stdout, result.stderr].filter(Boolean).join("\n"); +const candidates = createCandidates(); +const attempts = []; +let result = null; +let selected = null; + +for (const candidate of candidates) { + log(`Compiling pi-memory with ${candidate.command} -> ${outBin}`); + result = run(candidate.command, candidate.args); + attempts.push({ candidate, result }); + if (result.status === 0) { + selected = candidate; + break; + } +} + +if (!selected || !result || result.status !== 0) { + const details = attempts + .map(({ candidate, result: attemptResult }) => { + const out = [attemptResult.stdout, attemptResult.stderr].filter(Boolean).join("\n").trim() || "(no compiler output)"; + return `${candidate.command} ${candidate.args.map(quoteIfNeeded).join(" ")}\n${out}`; + }) + .join("\n\n"); + console.error("\nFailed to compile pi-memory."); - console.error("Compiler command:", [cc, ...platformArgs].join(" ")); - if (details) console.error(details); + console.error("Tried compiler commands:\n"); + console.error(details); if (!quiet) { - console.error("\nInstall a C compiler and retry:"); + if (os.platform() === "win32") { + console.error("\nInstall a basic Windows C compiler (clang, gcc, or cl) and retry:"); + } else { + console.error("\nInstall a C compiler and retry:"); + } console.error(" npm run setup"); } - process.exit(result.status ?? 1); + process.exit(result?.status ?? 1); } if (!existsSync(outBin)) { @@ -73,6 +174,6 @@ if (!existsSync(outBin)) { process.exit(1); } -chmodSync(outBin, 0o755); +if (os.platform() !== "win32") chmodSync(outBin, 0o755); log(`Installed: ${outBin}`); log("Done."); diff --git a/skills/memory/SKILL.md b/skills/memory/SKILL.md index d43165c..a31c42f 100644 --- a/skills/memory/SKILL.md +++ b/skills/memory/SKILL.md @@ -1,21 +1,23 @@ --- name: memory -description: Persist and retrieve project knowledge using pi-memory (SQLite at ~/.pi/memory/memory.db). Use when you need to log an architectural decision, record a finding or lesson, check what was decided before, sync a MEMORY.md file with live DB content, bootstrap memory for a new project, or ingest a Pi session file to extract metadata, decisions, lessons, and entities. Works across all projects — project is auto-detected from git remote or cwd. All log commands accept --session-id to cross-reference entries to Pi sessions. +description: Persist and retrieve project knowledge using pi-memory (SQLite in your user `.pi/memory` directory). Use when you need to log an architectural decision, record a finding or lesson, check what was decided before, sync a MEMORY.md file with live DB content, bootstrap memory for a new project, or ingest a Pi session file to extract metadata, decisions, lessons, and entities. Works across all projects — project is auto-detected from git remote or cwd. All log commands accept --session-id to cross-reference entries to Pi sessions. --- # pi-memory v2.1 — Durable Agent Memory -A single compiled binary (`~/.pi/memory/pi-memory`) backed by SQLite. +A single compiled binary backed by SQLite. Works across every project. Survives context resets, compactions, and machine reboots. ## Binary Location -``` -~/.pi/memory/pi-memory (installed binary) -~/.pi/memory/memory.db (database) +```text +~/.pi/memory/pi-memory (installed binary on macOS / Linux) +%USERPROFILE%\\.pi\\memory\\pi-memory.exe (installed binary on Windows) +~/.pi/memory/memory.db (database on macOS / Linux) +%USERPROFILE%\\.pi\\memory\\memory.db (database on Windows) ``` -If `pi-memory` is not in PATH, run `~/.pi/memory/pi-memory ...` directly. +If `pi-memory` is not in PATH, run the installed binary directly. ## Project Auto-Detection @@ -321,9 +323,11 @@ The C source and bundled SQLite amalgamation live in the installed package direc cd ~/.pi/agent/git/github.com/SiliconState/Pi-Memory && npm run setup ``` -Or compile directly: +Or compile directly (Unix example): ```bash PKG="$HOME/.pi/agent/git/github.com/SiliconState/Pi-Memory/native" -cc -O2 -Wall -Wextra -o ~/.pi/memory/pi-memory "$PKG/pi-memory.c" "$PKG/sqlite3.c" -lpthread -ldl -lm +cc -O2 -Wall -Wextra -o ~/.pi/memory/pi-memory "$PKG/pi-memory.c" "$PKG/sqlite3.c" "$PKG/getopt_compat.c" -lpthread -ldl -lm ``` + +On Windows, prefer `npm run setup`, which now supports `clang`, `gcc`, and Visual Studio `cl`.