New:Socket for Asana Is Now Available.Learn more
Sign In

memory-vault

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

memory-vault - npm Package Compare versions

Comparing version
0.3.0
to
0.3.1
+3
-5
package.json
{
"name": "memory-vault",
"version": "0.3.0",
"version": "0.3.1",
"mcpName": "io.github.apurv101/memory-vault",

@@ -12,4 +12,3 @@ "description": "Claude-style memory over a local folder, served via MCP — per-project spaces plus a shared org layer. Harness-neutral, customer-owned.",

"files": [
"server.mjs",
"scripts/scrape.mjs"
"server.mjs"
],

@@ -28,5 +27,4 @@ "keywords": [

"scripts": {
"start": "node server.mjs",
"scrape": "node scripts/scrape.mjs"
"start": "node server.mjs"
}
}
+92
-73
# Memory Vault
An organizational memory layer that works with any harness, any model. Customer-owned, permission-first, harness-neutral.
Local, persistent memory for coding agents, served over MCP.
**Thesis**: the harness is temporary, the model is temporary, the memory is the org. The vault holds the one asset that survives every model swap and every vendor switch.
Memory Vault stores facts as ordinary Markdown files. Each project gets an isolated memory space, while `shared/` holds facts that apply across projects. The files stay on your machine and remain usable if you change models or agent harnesses.
## What it is
## How it works
- **Store**: memories as raw content plus metadata (provenance, ACLs, expiry). Embeddings are rebuilt from raw content, so the vault is never locked to an embedding model.
- **Interface**: MCP server for reads and writes, plus ingestion pipelines for an org's existing corpus (docs, tickets, chat, code).
- **Write governance**: the actual product. Session-to-memory extraction, dedup, contradiction resolution, decay, review queues, provenance on every entry.
The MCP server gives an agent six file operations: `view`, `create`, `str_replace`, `insert`, `delete`, and `rename`.
## What it is not
The agent uses those tools to maintain small memory files and a `MEMORY.md` index. There is no database, vector search, or embedding model. The Markdown files are the source of truth, so you can read, edit, grep, or version them yourself.
- Not a harness. Harnesses are each vendor's crown jewels; the vault sits one layer below, where a standard interface (MCP) already exists.
- Not hosted memory. The vault lives inside the customer's perimeter.
- Not a model. Intelligence is rented; memory is owned.
```text
memory/
MEMORY.md # index of project spaces
shared/
MEMORY.md # cross-project facts
*.md
<project>/
MEMORY.md # project index
*.md
```
## MVP: a local vault — Claude-style memory over a folder
A project connection can access its own directory and `shared/`, but not other projects. An unscoped connection can access the whole vault for cross-project maintenance.
Local-only, zero dependencies, one file (`server.mjs`). The store is a plain directory (`memory/`) of markdown files with `MEMORY.md` indexes — the same shape Claude Code keeps its own memory in. The server exposes exactly Claude's core memory commands over MCP — `view`, `create`, `str_replace`, `insert`, `delete`, `rename` — sandboxed to the requesting project's slice of that folder. No search engine, no database, no auth: the *model* maintains the index, checks for duplicates, and decides what to keep, guided by the server's instructions. Files are the source of truth — edit them by hand, grep them, sync the folder with git.
## Requirements
### Layout — per-project spaces + a shared org layer
- Node.js 18 or newer
- An MCP client that supports Streamable HTTP
Memory Vault has no runtime dependencies.
## Install into a repository
Run this from the repository you want to connect:
```sh
npx -y memory-vault install
```
memory/
MEMORY.md # vault index: one line per space
shared/ # org-wide memory, read-write from every project scope
MEMORY.md
<project>/ # one space per project, isolated from the others
MEMORY.md
*.md
skills/<name>/ # whole skill folders scraped into the vault
```
The scope is carried in the URL: `POST /mcp/<project>` sandboxes a session to `memory/<project>/` plus `shared/`; bare `POST /mcp` is the unscoped whole-vault view — the org "gardener" scope for pruning across projects and promoting facts into `shared/`.
This command:
### Quickstart
1. Starts the local server if it is not already running.
2. Asks which harnesses to wire up, with the detected ones pre-selected (interactive terminals only — everywhere else the detected set is used as is).
3. Writes each chosen harness's MCP config and the shared rules files.
The package is [on npm](https://www.npmjs.com/package/memory-vault) — no clone, no dependencies (Node ≥18):
By default, `install` stores memory in `~/.memory-vault` and derives the project name from the current directory. `connect` is an alias for `install`.
```sh
MEMORY_DIR=~/memory-vault npx memory-vault # serves the folder at http://localhost:8787 (127.0.0.1 only)
npx -y memory-vault install --dry-run # preview changes
npx -y memory-vault install --project my-app # choose the project name
npx -y memory-vault install --harness claude,codex # skip the prompt, pick explicitly
npx -y memory-vault install --yes # skip the prompt, accept detected
```
Set `MEMORY_DIR` to where you want the vault to live — the default is `./memory` relative to wherever you ran the command. `VAULT_PORT` overrides the port. From a clone, `npm start` does the same thing.
Restart your agent session after installing and approve the `vault` MCP server if prompted.
### Connect a repo: `npx memory-vault connect`
## Uninstall from a repository
One command from the repo root wires it up for every harness it finds:
```sh
npx -y memory-vault connect # add --dry-run to preview, --project <name> to override the space name
npx -y memory-vault uninstall
```
It starts the server if it's down (store: `$MEMORY_DIR`, default `~/.memory-vault`), then writes **both layers each detected harness needs** — the MCP registration in its own config format, and the memory ritual in a rules file it actually loads:
Removes everything `install` wrote to the repository — the MCP entries, the rules sections, the dsh patch — deleting a file only when it held nothing else. Your memories are never touched, and the server keeps running for other projects. `disconnect` is an alias for `uninstall`.
| Harness | MCP registration | Ritual |
|---|---|---|
| Claude Code | `.mcp.json` (always written — the repo-level MCP convention) | `CLAUDE.md` (imports `@AGENTS.md`) |
| Cursor | `.cursor/mcp.json` | `AGENTS.md` |
| Codex | `.codex/config.toml` (trusted projects) | `AGENTS.md` |
| DSH | printed pointer to `dsh-cordis.patch.yml` (profile patch stays manual) | — |
## Check the wiring
Re-running is idempotent — existing entries are left alone, missing ones added. Then restart the session and approve the `vault` MCP server when prompted; the memory tools appear from the next session on. If the repo already has harness memory to import, see the scraper below.
```sh
npx -y memory-vault status
```
Or don't do it yourself:
Shows whether the server is up and which store it serves, how the current repository is wired per harness, and every repository recorded by `install` (kept in `~/.memory-vault-connections.json`). If the server is up and the repo is wired but your agent session has no vault tools, the remaining cause is session attachment — `status` prints how to fix it.
> **Ask your agent**: "Set up memory-vault for this repo — run `npx -y memory-vault connect` from the repo root and relay its output."
### Supported harnesses
### Connect from Claude Code (manual)
| Harness | Files configured |
|---|---|
| Claude Code | `.mcp.json`, `CLAUDE.md` |
| Cursor | `.cursor/mcp.json`, `AGENTS.md` |
| Codex | `.codex/config.toml`, `AGENTS.md` |
| DeepSeek Harness | `dsh-cordis.patch.yml` |
Per repo (recorded in the repo's `.mcp.json`):
For DSH, start a session with the generated patch:
```sh
claude mcp add --transport http --scope project vault http://localhost:8787/mcp/<project>
dsh --patch ./dsh-cordis.patch.yml --profile headless "your task"
```
Optionally also at user scope for the whole-vault gardener view — the project-scope entry of the same name wins inside a repo:
## Run the server directly
```sh
claude mcp add --transport http --scope user vault http://localhost:8787/mcp
MEMORY_DIR=~/.memory-vault npx memory-vault
```
New sessions get the memory tools; the server must be running.
The server listens on `127.0.0.1:8787` by default.
### Connect from DeepSeek Harness
| Environment variable | Default | Purpose |
|---|---|---|
| `MEMORY_DIR` | `./memory` | Directory containing the vault |
| `VAULT_PORT` | `8787` | Local HTTP port |
DSH loads MCP servers as Cordis plugin instances via `@deepseek-ai/dsh-mcp-client`. A ready-to-use patch is in `dsh-cordis.patch.yml`:
From a cloned repository, `npm start` runs the same server.
```sh
# Per-session (workspace-local)
dsh --profile headless --patch ./dsh-cordis.patch.yml "your task"
### MCP endpoints
# Or make it permanent by copying the entry into:
# ~/.dsh/profiles/headless/cordis.patch.yml
# ~/.dsh/profiles/web/cordis.patch.yml
```text
POST /mcp/<project> project memory plus shared memory
POST /mcp whole-vault access
```
The patch connects to the local vault at `http://localhost:8787/mcp/memory-vault`; tools appear as `mcp__vault__view`, `mcp__vault__create`, etc. The server must be running.
For example, a manual Claude Code connection is:
### Seed a corpus
```sh
claude mcp add --transport http --scope project vault http://localhost:8787/mcp/my-project
```
Copy markdown files into `memory/<project>/` (or `memory/shared/`) — the store is the folder. Give each `name:` and `description:` frontmatter and an index line in that space's `MEMORY.md` (or let the model tidy that up next session).
## Memory format
### Cold-start an existing repo (memory scraper)
Store one durable fact per Markdown file:
First time connecting the vault on a repo that already has harness memory? Scrape it in:
```md
---
name: preferred-language
description: The project's preferred implementation language
---
```sh
node scripts/scrape.mjs <repo-path> [--dry-run] # writes into memory/<project>/
Use TypeScript for new application code.
```
Per-harness adapters, each stamping its own `source` (`scrape:<adapter>`) and origin path:
Add a pointer to the space's `MEMORY.md`:
| Adapter | Scrapes |
|---|---|
| `claude-memory` | `~/.claude/projects/<repo-slug>/memory/*.md` (Claude Code auto-memory) |
| `claude-instructions` | `CLAUDE.md`, `CLAUDE.local.md`, `.claude/CLAUDE.md` |
| `claude-skills` | `.claude/skills/*` (whole folders, into `skills/<name>/`; add `--include-user-skills` for `~/.claude/skills/*`), `.claude/commands/*.md`, `.claude/agents/*.md` |
| `codex` | `AGENTS.md` |
| `cursor` | `.cursorrules`, `.cursor/rules/*` |
```md
- [preferred-language](preferred-language.md) — use TypeScript for new application code
```
Everything lands in `memory/<project>/` (project = repo dir name, override with `--project`) and only that project's `MEMORY.md` is rebuilt. Names are stable, so re-running is an idempotent refresh. `--only <a,b>` runs a subset of adapters (`skills` = `claude-skills`). User-level files (`~/.claude/CLAUDE.md`, `~/.claude/skills/`) are personal, not org memory — opt in with `--include-user` / `--include-user-skills`. `--dry-run` previews without storing; `--claude-home` overrides `~/.claude` (mainly for tests). Adding a harness = adding one adapter block in `scripts/scrape.mjs`.
## Status
The server instructs agents to check for an existing memory before creating one, update facts instead of duplicating them, and remove memories that become incorrect.
MVP above is the read/write pipe. Write governance (extraction, dedup, contradiction handling, review) comes next. See `docs/roadmap.md` for the path from local MVP to org deployment, and `docs/architecture.md` for the architecture sketch.
## Current scope
The current release is a local Markdown store, an MCP interface, and a cross-harness setup command. There is no automatic extraction, semantic search, deduplication, authentication, or remote deployment.
## Package
- npm: [`memory-vault`](https://www.npmjs.com/package/memory-vault)
- MCP registry: `io.github.apurv101/memory-vault`
- License: MIT
+429
-75

@@ -5,5 +5,8 @@ #!/usr/bin/env node

// node server.mjs (or: npm start) — serve the vault
// npx memory-vault connect — wire the current repo to the vault
// (starts the server if down, writes each
// detected harness's MCP config + rules file)
// npx memory-vault install — wire the current repo to the vault
// (starts the server if down, lets you pick
// harnesses, writes each one's MCP config +
// the shared rules files; alias: connect)
// npx memory-vault uninstall — undo install for the repo; memories are
// never touched (alias: disconnect)
//

@@ -36,2 +39,3 @@ // The store is a plain directory of markdown files (default ./memory), one

rm,
rmdir,
stat,

@@ -49,3 +53,3 @@ } from "node:fs/promises";

const PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
const SERVER_INFO = { name: "memory-vault", version: "0.3.0" };
const SERVER_INFO = { name: "memory-vault", version: "0.3.1" };

@@ -386,2 +390,34 @@ const instructionsFor = (scope) =>

// Markers around the written section let uninstall remove it verbatim even if
// the section text changes in a future version. (Sections written before the
// markers existed are removed by exact-text match instead.)
const MARK_BEGIN = "<!-- memory-vault:begin -->";
const MARK_END = "<!-- memory-vault:end -->";
const MARKED_SECTION = `${MARK_BEGIN}\n${MEMORY_SECTION}${MARK_END}\n`;
const projectSlug = (name) =>
name
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^[^a-z0-9]+/, "")
.slice(0, 64);
// Machine-level record of which repos ran install — advisory, not
// authoritative (repos move and vanish), so readers treat entries as hints.
// Lives outside the store: it describes this machine's wiring, not memories.
const CONNECTIONS_PATH = join(homedir(), ".memory-vault-connections.json");
async function readConnections() {
try {
const parsed = JSON.parse(await readFile(CONNECTIONS_PATH, "utf8"));
return Array.isArray(parsed.connections) ? parsed.connections : [];
} catch {
return [];
}
}
async function writeConnections(connections) {
await writeFile(CONNECTIONS_PATH, JSON.stringify({ connections }, null, 2) + "\n");
}
async function serverUp() {

@@ -419,18 +455,274 @@ try {

async function connect(argv) {
// Reverse of mergeMcpJson: drop the vault entry, delete the file if that was
// all it held. Returns a status string for the report.
async function removeMcpJson(path, dryRun) {
const raw = await readFile(path, "utf8").catch(() => null);
if (raw === null) return "not present";
let config;
try {
config = JSON.parse(raw);
} catch {
return "not valid JSON — remove the vault entry by hand";
}
if (!config.mcpServers?.vault) return "no vault entry";
delete config.mcpServers.vault;
const empty = Object.keys(config).length === 1 && Object.keys(config.mcpServers).length === 0;
if (!dryRun) {
if (empty) await rm(path);
else await writeFile(path, JSON.stringify(config, null, 2) + "\n");
}
return empty ? "deleted (only held the vault entry)" : "vault entry removed";
}
// ── harness registry ──────────────────────────────────────────────────────────
//
// Each harness owns three hooks: detect (is it on this machine / in this
// repo), install (write its MCP registration), and uninstall (remove exactly
// what install wrote, leaving the repo as it was). The shared rules files
// (AGENTS.md + CLAUDE.md) are a separate step because every harness reads the
// same ritual text. Adding a harness = adding one entry here.
const HARNESSES = [
{
key: "claude",
title: "Claude Code",
where: ".mcp.json",
// .mcp.json is the repo-level MCP convention, useful beyond Claude Code —
// always on.
detect: async () => true,
async install({ cwd, url, dryRun }) {
const status = await mergeMcpJson(join(cwd, ".mcp.json"), { type: "http", url }, dryRun);
return [`claude .mcp.json ${status} (vault → ${url})`];
},
async uninstall({ cwd, dryRun }) {
return [`claude .mcp.json ${await removeMcpJson(join(cwd, ".mcp.json"), dryRun)}`];
},
},
{
key: "cursor",
title: "Cursor",
where: ".cursor/mcp.json",
detect: async (cwd) => (await exists(join(homedir(), ".cursor"))) || (await exists(join(cwd, ".cursor"))),
async install({ cwd, url, dryRun }) {
const status = await mergeMcpJson(join(cwd, ".cursor", "mcp.json"), { url }, dryRun);
return [`cursor .cursor/mcp.json ${status}`];
},
async uninstall({ cwd, dryRun }) {
const status = await removeMcpJson(join(cwd, ".cursor", "mcp.json"), dryRun);
if (!dryRun && status.startsWith("deleted")) await rmdir(join(cwd, ".cursor")).catch(() => {});
return [`cursor .cursor/mcp.json ${status}`];
},
},
{
key: "codex",
title: "Codex CLI",
where: ".codex/config.toml",
detect: async (cwd) => (await exists(join(homedir(), ".codex"))) || (await exists(join(cwd, ".codex"))),
async install({ cwd, url, dryRun }) {
// Project-scoped Codex config (trusted projects). TOML is appended, not
// parsed — if a vault block already exists we only verify the URL.
const tomlPath = join(cwd, ".codex", "config.toml");
const toml = await readFile(tomlPath, "utf8").catch(() => null);
if (toml === null || !toml.includes("[mcp_servers.vault]")) {
if (!dryRun) {
await mkdir(dirname(tomlPath), { recursive: true });
await writeFile(tomlPath, `${toml?.trimEnd() ? toml.trimEnd() + "\n\n" : ""}[mcp_servers.vault]\nurl = "${url}"\n`);
}
return [`codex .codex/config.toml ${toml === null ? "created" : "updated"} (trusted projects only)`];
}
return [
toml.includes(`url = "${url}"`)
? "codex .codex/config.toml unchanged"
: `codex .codex/config.toml already has a vault entry with a different url — update it by hand to ${url}`,
];
},
async uninstall({ cwd, dryRun }) {
const tomlPath = join(cwd, ".codex", "config.toml");
const toml = await readFile(tomlPath, "utf8").catch(() => null);
if (toml === null) return ["codex .codex/config.toml not present"];
if (!toml.includes("[mcp_servers.vault]")) return ["codex .codex/config.toml no vault entry"];
const cleaned = toml.replace(/(?:^|\n)\[mcp_servers\.vault\]\n(?:(?!\[).*(?:\n|$))*/, "\n").replace(/^\n+/, "");
if (cleaned.trim() === "") {
if (!dryRun) {
await rm(tomlPath);
await rmdir(join(cwd, ".codex")).catch(() => {});
}
return ["codex .codex/config.toml deleted (only held the vault entry)"];
}
if (!dryRun) await writeFile(tomlPath, cleaned.trimEnd() + "\n");
return ["codex .codex/config.toml vault entry removed"];
},
},
{
key: "dsh",
title: "DeepSeek Harness",
where: "dsh-cordis.patch.yml",
detect: async () => exists(join(homedir(), ".dsh")),
async install({ cwd, url, project, dryRun }) {
// Repo-local Cordis patch with the project-scoped URL. dsh has no repo-level
// auto-loaded config, so the patch is applied per-session via --patch (or
// copied into a profile to make it permanent — the file header says how).
const patchPath = join(cwd, "dsh-cordis.patch.yml");
const patchEntry = `- id: mcp-vault\n name: '@deepseek-ai/dsh-mcp-client'\n config:\n serverName: vault\n transport: streamable-http\n url: ${url}\n`;
const patchContent = `# dsh Cordis patch — load the memory-vault MCP server (project "${project}").\n# Per-session: dsh --patch ./dsh-cordis.patch.yml [--profile <name>] ["your task"]\n# Permanent: copy the entry below into ~/.dsh/profiles/<name>/cordis.patch.yml\n# The vault server must be running (npx memory-vault install starts it if down).\n${patchEntry}`;
const patch = await readFile(patchPath, "utf8").catch(() => null);
if (patch === patchContent || (patch !== null && patch.includes("id: mcp-vault") && patch.includes(`url: ${url}`))) {
return ["dsh dsh-cordis.patch.yml unchanged — run: dsh --patch ./dsh-cordis.patch.yml"];
}
if (patch !== null && patch.includes("id: mcp-vault")) {
return [`dsh dsh-cordis.patch.yml already has a vault entry with a different url — update it by hand to ${url}`];
}
if (patch !== null) {
if (!dryRun) await writeFile(patchPath, `${patch.trimEnd()}\n\n${patchEntry}`);
return ["dsh dsh-cordis.patch.yml updated — run: dsh --patch ./dsh-cordis.patch.yml"];
}
if (!dryRun) await writeFile(patchPath, patchContent);
return ["dsh dsh-cordis.patch.yml created — run: dsh --patch ./dsh-cordis.patch.yml"];
},
async uninstall({ cwd, dryRun }) {
const patchPath = join(cwd, "dsh-cordis.patch.yml");
const patch = await readFile(patchPath, "utf8").catch(() => null);
if (patch === null) return ["dsh dsh-cordis.patch.yml not present"];
if (!patch.includes("id: mcp-vault")) return ["dsh dsh-cordis.patch.yml no vault entry"];
const cleaned = patch.replace(/(?:^|\n)- id: mcp-vault\n(?:[ \t].*(?:\n|$))*/, "\n");
const onlyComments = cleaned.split("\n").every((l) => l.trim() === "" || l.trim().startsWith("#"));
if (onlyComments) {
if (!dryRun) await rm(patchPath);
return ["dsh dsh-cordis.patch.yml deleted"];
}
if (!dryRun) await writeFile(patchPath, cleaned.trimEnd() + "\n");
return ["dsh dsh-cordis.patch.yml vault entry removed"];
},
},
];
// The ritual. AGENTS.md carries it (Codex, Cursor, and the growing
// cross-harness convention); CLAUDE.md imports it via @AGENTS.md so the
// text lives in one place.
async function installRules({ cwd, project, dryRun }) {
const lines = [];
const agentsPath = join(cwd, "AGENTS.md");
const agents = await readFile(agentsPath, "utf8").catch(() => null);
if (agents === null) {
if (!dryRun) await writeFile(agentsPath, `# ${project}\n\n${MARKED_SECTION}`);
lines.push("rules AGENTS.md created with the memory section");
} else if (!/vault/i.test(agents)) {
if (!dryRun) await writeFile(agentsPath, `${agents.trimEnd()}\n\n${MARKED_SECTION}`);
lines.push("rules AGENTS.md memory section appended");
} else {
lines.push("rules AGENTS.md unchanged");
}
const claudeMdPath = join(cwd, "CLAUDE.md");
const claudeMd = await readFile(claudeMdPath, "utf8").catch(() => null);
if (claudeMd === null) {
if (!dryRun) await writeFile(claudeMdPath, "@AGENTS.md\n");
lines.push("rules CLAUDE.md created (imports @AGENTS.md)");
} else if (!/vault/i.test(claudeMd) && !claudeMd.includes("@AGENTS.md")) {
if (!dryRun) await writeFile(claudeMdPath, `${claudeMd.trimEnd()}\n\n@AGENTS.md\n`);
lines.push("rules CLAUDE.md @AGENTS.md import appended");
} else {
lines.push("rules CLAUDE.md unchanged");
}
return lines;
}
async function uninstallRules({ cwd, project, dryRun }) {
const lines = [];
const agentsPath = join(cwd, "AGENTS.md");
const agents = await readFile(agentsPath, "utf8").catch(() => null);
let sectionRemoved = false;
let agentsDeleted = false;
if (agents === null) {
lines.push("rules AGENTS.md not present");
} else {
// Marker-delimited sections first (written by install going forward),
// exact text of the current section as fallback for older installs.
const marked = /(?:^|\n)<!-- memory-vault:begin -->\n[\s\S]*?<!-- memory-vault:end -->\n?/;
let cleaned = null;
if (marked.test(agents)) cleaned = agents.replace(marked, "\n");
else if (agents.includes(MEMORY_SECTION)) cleaned = agents.replace(MEMORY_SECTION, "");
if (cleaned === null) {
lines.push(
/vault/i.test(agents)
? "rules AGENTS.md mentions the vault but not the standard section — edit by hand"
: "rules AGENTS.md no memory section",
);
} else {
sectionRemoved = true;
const rest = cleaned.trim();
if (rest === "" || rest === `# ${project}`) {
if (!dryRun) await rm(agentsPath);
agentsDeleted = true;
lines.push("rules AGENTS.md deleted (only held the memory section)");
} else {
if (!dryRun) await writeFile(agentsPath, cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n");
lines.push("rules AGENTS.md memory section removed");
}
}
}
const claudeMdPath = join(cwd, "CLAUDE.md");
const claudeMd = await readFile(claudeMdPath, "utf8").catch(() => null);
if (claudeMd === null) {
lines.push("rules CLAUDE.md not present");
} else if (claudeMd.trim() === "@AGENTS.md" && (agentsDeleted || sectionRemoved)) {
if (!dryRun) await rm(claudeMdPath);
lines.push("rules CLAUDE.md deleted (only imported AGENTS.md)");
} else if ((agentsDeleted || sectionRemoved) && /\n@AGENTS\.md\s*$/.test(claudeMd)) {
if (!dryRun) await writeFile(claudeMdPath, claudeMd.replace(/\n+@AGENTS\.md\s*$/, "\n"));
lines.push("rules CLAUDE.md @AGENTS.md import removed");
} else if (/vault/i.test(claudeMd)) {
lines.push("rules CLAUDE.md mentions the vault — review by hand");
} else {
lines.push("rules CLAUDE.md unchanged");
}
return lines;
}
// Interactive harness picker — plain readline, no dependencies. Only used
// when stdin/stdout are a terminal and no --harness/--yes flag was given.
async function pickHarnesses(detected) {
console.log("Harnesses to wire to the vault:\n");
HARNESSES.forEach((h, i) => {
const mark = detected.includes(h.key) ? "detected" : "not detected";
console.log(` ${i + 1}. ${h.key.padEnd(8)} ${h.title.padEnd(18)} ${h.where.padEnd(24)} ${mark}`);
});
const { createInterface } = await import("node:readline/promises");
const rl = createInterface({ input: process.stdin, output: process.stdout });
const answer = (
await rl.question(`\nInstall for [${detected.join(", ")}] — Enter to accept, or list keys/numbers, or "all": `)
).trim();
rl.close();
if (answer === "") return detected;
if (answer.toLowerCase() === "all") return HARNESSES.map((h) => h.key);
const keys = [];
for (const tok of answer.split(/[,\s]+/).filter(Boolean)) {
const h = /^\d+$/.test(tok) ? HARNESSES[Number(tok) - 1] : HARNESSES.find((x) => x.key === tok.toLowerCase());
if (!h) throw new Error(`unknown harness: ${tok} (known: ${HARNESSES.map((x) => x.key).join(", ")})`);
if (!keys.includes(h.key)) keys.push(h.key);
}
return keys;
}
async function install(argv) {
let project = null;
let dryRun = false;
let harnessFlag = null;
let yes = false;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === "--project") project = argv[++i];
else if (argv[i] === "--dry-run") dryRun = true;
else throw new Error(`unknown flag: ${argv[i]} (usage: memory-vault connect [--project <name>] [--dry-run])`);
else if (argv[i] === "--harness") harnessFlag = argv[++i];
else if (argv[i] === "--yes" || argv[i] === "-y") yes = true;
else
throw new Error(
`unknown flag: ${argv[i]} (usage: memory-vault install [--project <name>] [--harness <keys>] [--yes] [--dry-run])`,
);
}
const cwd = process.cwd();
project = (project ?? basename(cwd))
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^[^a-z0-9]+/, "")
.slice(0, 64);
project = projectSlug(project ?? basename(cwd));
if (!project) throw new Error("could not derive a project name from the directory — pass --project <name>");
const url = `http://localhost:${PORT}/mcp/${project}`;
const ctx = { cwd, url, project, dryRun };
const lines = [];

@@ -457,71 +749,68 @@

// 2. MCP registration, one config per harness. Claude Code's .mcp.json is
// always written — it's the repo-level MCP convention; the others only when
// the harness is installed (user-level dotdir) or already used in this repo.
const claudeStatus = await mergeMcpJson(join(cwd, ".mcp.json"), { type: "http", url }, dryRun);
lines.push(`claude .mcp.json ${claudeStatus} (vault → ${url})`);
if ((await exists(join(homedir(), ".cursor"))) || (await exists(join(cwd, ".cursor")))) {
const status = await mergeMcpJson(join(cwd, ".cursor", "mcp.json"), { url }, dryRun);
lines.push(`cursor .cursor/mcp.json ${status}`);
// 2. Pick harnesses: --harness wins; otherwise the detected set, confirmed
// interactively when there's a terminal to ask.
const detected = [];
for (const h of HARNESSES) if (await h.detect(cwd)) detected.push(h.key);
let selected;
if (harnessFlag !== null) {
selected = harnessFlag.split(/[,\s]+/).filter(Boolean);
for (const key of selected) {
if (!HARNESSES.some((h) => h.key === key))
throw new Error(`unknown harness: ${key} (known: ${HARNESSES.map((h) => h.key).join(", ")})`);
}
} else if (yes || dryRun || !process.stdin.isTTY || !process.stdout.isTTY) {
selected = detected;
} else {
lines.push("cursor not detected — skipped");
selected = await pickHarnesses(detected);
}
if ((await exists(join(homedir(), ".codex"))) || (await exists(join(cwd, ".codex")))) {
// Project-scoped Codex config (trusted projects). TOML is appended, not
// parsed — if a vault block already exists we only verify the URL.
const tomlPath = join(cwd, ".codex", "config.toml");
const toml = await readFile(tomlPath, "utf8").catch(() => null);
if (toml === null || !toml.includes("[mcp_servers.vault]")) {
if (!dryRun) {
await mkdir(dirname(tomlPath), { recursive: true });
await writeFile(tomlPath, `${toml?.trimEnd() ? toml.trimEnd() + "\n\n" : ""}[mcp_servers.vault]\nurl = "${url}"\n`);
}
lines.push(`codex .codex/config.toml ${toml === null ? "created" : "updated"} (trusted projects only)`);
} else {
lines.push(
toml.includes(`url = "${url}"`)
? "codex .codex/config.toml unchanged"
: `codex .codex/config.toml already has a vault entry with a different url — update it by hand to ${url}`,
);
}
} else {
lines.push("codex not detected — skipped");
// 3. MCP registration for each selected harness, then the shared ritual.
for (const h of HARNESSES) {
if (selected.includes(h.key)) lines.push(...(await h.install(ctx)));
else lines.push(`${h.key.padEnd(9)}${detected.includes(h.key) ? "skipped" : "not detected — skipped"}`);
}
if (selected.length > 0) lines.push(...(await installRules(ctx)));
if (await exists(join(homedir(), ".dsh"))) {
lines.push("dsh detected — apply dsh-cordis.patch.yml to your profile (see README)");
// 4. Record the connection so status / a future uninstall --all can find it.
if (selected.length > 0 && !dryRun) {
const connections = (await readConnections()).filter((c) => c.repo !== cwd);
connections.push({ repo: cwd, project, url, harnesses: selected, installedAt: new Date().toISOString() });
await writeConnections(connections);
lines.push(`registry ${CONNECTIONS_PATH} recorded`);
}
// 3. The ritual. AGENTS.md carries it (Codex, Cursor, and the growing
// cross-harness convention); CLAUDE.md imports it via @AGENTS.md so the
// text lives in one place.
const agentsPath = join(cwd, "AGENTS.md");
const agents = await readFile(agentsPath, "utf8").catch(() => null);
if (agents === null) {
if (!dryRun) await writeFile(agentsPath, `# ${project}\n\n${MEMORY_SECTION}`);
lines.push("rules AGENTS.md created with the memory section");
} else if (!/vault/i.test(agents)) {
if (!dryRun) await writeFile(agentsPath, `${agents.trimEnd()}\n\n${MEMORY_SECTION}`);
lines.push("rules AGENTS.md memory section appended");
} else {
lines.push("rules AGENTS.md unchanged");
console.log(`memory-vault install — project "${project}"${dryRun ? " (dry run)" : ""}\n`);
for (const l of lines) console.log(` ${l}`);
console.log("\nRestart your session and approve the vault MCP server when prompted.");
}
async function uninstall(argv) {
let project = null;
let dryRun = false;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === "--project") project = argv[++i];
else if (argv[i] === "--dry-run") dryRun = true;
else throw new Error(`unknown flag: ${argv[i]} (usage: memory-vault uninstall [--project <name>] [--dry-run])`);
}
const cwd = process.cwd();
project = projectSlug(project ?? basename(cwd));
const ctx = { cwd, project, dryRun };
const lines = [];
for (const h of HARNESSES) lines.push(...(await h.uninstall(ctx)));
lines.push(...(await uninstallRules(ctx)));
const claudeMdPath = join(cwd, "CLAUDE.md");
const claudeMd = await readFile(claudeMdPath, "utf8").catch(() => null);
if (claudeMd === null) {
if (!dryRun) await writeFile(claudeMdPath, "@AGENTS.md\n");
lines.push("rules CLAUDE.md created (imports @AGENTS.md)");
} else if (!/vault/i.test(claudeMd) && !claudeMd.includes("@AGENTS.md")) {
if (!dryRun) await writeFile(claudeMdPath, `${claudeMd.trimEnd()}\n\n@AGENTS.md\n`);
lines.push("rules CLAUDE.md @AGENTS.md import appended");
const connections = await readConnections();
if (connections.some((c) => c.repo === cwd)) {
if (!dryRun) await writeConnections(connections.filter((c) => c.repo !== cwd));
lines.push("registry connection entry removed");
} else {
lines.push("rules CLAUDE.md unchanged");
lines.push("registry no entry for this repo");
}
console.log(`memory-vault connect — project "${project}"${dryRun ? " (dry run)" : ""}\n`);
console.log(`memory-vault uninstall — project "${project}"${dryRun ? " (dry run)" : ""}\n`);
for (const l of lines) console.log(` ${l}`);
console.log("\nRestart your session and approve the vault MCP server when prompted.");
console.log(
"\nYour memories are untouched — the store stays in the vault directory (default ~/.memory-vault).\n" +
"The server keeps running for other projects; restart your agent session to drop the vault tools.",
);
}

@@ -531,18 +820,83 @@

// status — the three-layer diagnosis: server up? repo wired? and if both,
// the remaining failure mode is session attachment, which only a session
// restart / approval can fix, so say exactly that.
async function status() {
const cwd = process.cwd();
console.log("memory-vault status\n");
let banner = null;
try {
const res = await fetch(`http://127.0.0.1:${PORT}/`, { signal: AbortSignal.timeout(1500) });
banner = (await res.text()).trim().split("\n")[0];
} catch {}
console.log(
banner !== null
? ` server up on port ${PORT} — ${banner}`
: ` server down (port ${PORT}) — start it: npx memory-vault install (or serve)`,
);
console.log(`\n this repo (${cwd}):`);
for (const h of HARNESSES) {
const raw = await readFile(join(cwd, h.where), "utf8").catch(() => null);
const state = raw === null ? "not present" : /vault/i.test(raw) ? "wired" : "present, no vault entry";
console.log(` ${h.key.padEnd(9)}${h.where} ${state}`);
}
const agents = await readFile(join(cwd, "AGENTS.md"), "utf8").catch(() => null);
console.log(
` rules AGENTS.md ${agents === null ? "not present" : /vault/i.test(agents) ? "has the memory section" : "no memory section"}`,
);
const connections = await readConnections();
if (connections.length === 0) {
console.log("\n connected repos: none recorded (installs record here from v0.3.1 on)");
} else {
console.log(`\n connected repos (${connections.length}):`);
for (const c of connections) {
const there = await exists(c.repo);
console.log(` ${c.repo} → ${c.project}${there ? "" : " (missing — moved or deleted)"}`);
}
}
console.log(
"\n If the server is up and the repo is wired but your agent session has no vault\n" +
" tools: MCP servers attach at session start — restart the session and approve\n" +
" the vault server. Claude Code remembers a declined approval: run /mcp in the\n" +
" session, or `claude mcp reset-project-choices` in the repo, then restart.",
);
}
const USAGE = `memory-vault — Claude-style memory over a local folder, via MCP
memory-vault [serve] serve the vault (MEMORY_DIR, VAULT_PORT)
memory-vault connect wire the current repo to the vault:
start the server if down, write each detected
harness's MCP config and rules file
[--project <name>] [--dry-run]`;
memory-vault [serve] serve the vault (MEMORY_DIR, VAULT_PORT)
memory-vault install wire the current repo to the vault: start the
server if down, pick harnesses (interactive in
a terminal), write each one's MCP config and
the shared rules files (alias: connect)
[--project <name>] [--harness <keys>] [--yes] [--dry-run]
memory-vault uninstall undo install for this repo: remove the vault
wiring from every harness config and rules
file; memories are never touched
(alias: disconnect)
[--project <name>] [--dry-run]
memory-vault status show server state, this repo's wiring, and every
repo recorded by install`;
const cmd = process.argv[2];
if (cmd === "connect") {
if (cmd === "install" || cmd === "connect") {
try {
await connect(process.argv.slice(3));
await install(process.argv.slice(3));
} catch (err) {
console.error(`memory-vault connect: ${err.message}`);
console.error(`memory-vault install: ${err.message}`);
process.exit(1);
}
} else if (cmd === "uninstall" || cmd === "disconnect") {
try {
await uninstall(process.argv.slice(3));
} catch (err) {
console.error(`memory-vault uninstall: ${err.message}`);
process.exit(1);
}
} else if (cmd === "status") {
await status();
} else if (cmd === undefined || cmd === "serve") {

@@ -549,0 +903,0 @@ await mkdir(MEMORY_DIR, { recursive: true });

#!/usr/bin/env node
// Memory scraper — cold-start a pre-existing repo into the vault folder.
//
// Walks the harness-specific places where memory already lives on this machine
// (Claude Code auto-memory, CLAUDE.md files, skills/commands/agents, Codex
// AGENTS.md, Cursor rules) and writes each as a markdown memory file into the
// vault's per-project space (memory/<project>/). Skills are copied as whole
// folders to memory/<project>/skills/<name>/. Refreshes that project's
// MEMORY.md index only.
//
// node scripts/scrape.mjs <repo-path> [options]
//
// Options:
// --memory-dir <dir> Vault folder (default <this repo>/memory, env MEMORY_DIR)
// --project <name> Project space to write into (default: repo dir name)
// --claude-home <dir> Claude config dir (default ~/.claude)
// --only <a,b> Run only these adapters (claude-memory, claude-instructions,
// claude-skills, codex, cursor; "skills" = "claude-skills")
// --include-user Also scrape user-level files (~/.claude/CLAUDE.md) — off by
// default because user-level memory is personal, not org memory
// --include-user-skills Also copy user-level skills (~/.claude/skills/*)
// --dry-run Print what would be written; write nothing
//
// Idempotent: names are stable — re-running refreshes (created date preserved).
import { cp, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
// ── Helpers ───────────────────────────────────────────────────────────────────
function parseArgs(argv, booleanFlags = []) {
const args = { positional: [] };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith("--")) {
const key = a.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
if (booleanFlags.includes(a)) args[key] = true;
else args[key] = argv[++i];
} else args.positional.push(a);
}
return args;
}
function parseFrontmatter(raw) {
const meta = {};
let body = raw;
const m = raw.match(/^---\n([\s\S]*?)\n---\n?/);
if (m) {
body = raw.slice(m[0].length);
for (const line of m[1].split("\n")) {
const kv = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
if (kv) meta[kv[1]] = kv[2].trim();
}
}
return { meta, body: body.trim() };
}
const kebab = (s) =>
String(s)
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+|-+$/g, "");
function firstProseLine(body) {
for (const line of body.split("\n")) {
const t = line.trim();
if (t && !t.startsWith("#")) return t.slice(0, 200);
}
return "";
}
// ── Args ──────────────────────────────────────────────────────────────────────
const args = parseArgs(process.argv, ["--dry-run", "--include-user", "--include-user-skills"]);
const [repoArg] = args.positional;
if (!repoArg) {
console.error(
"usage: node scripts/scrape.mjs <repo-path> [--memory-dir <dir>] [--project <name>] [--claude-home <dir>] [--only <adapters>] [--include-user] [--include-user-skills] [--dry-run]",
);
process.exit(1);
}
const repo = resolve(repoArg);
const project = kebab(args.project ?? basename(repo));
const claudeHome = resolve(args.claudeHome ?? join(homedir(), ".claude"));
const memoryDir = resolve(
args.memoryDir ??
process.env.MEMORY_DIR ??
join(dirname(fileURLToPath(import.meta.url)), "..", "memory"),
);
const projectDir = join(memoryDir, project);
const only = args.only ? new Set(args.only.split(",").map((s) => s.trim())) : null;
const runs = (adapter) => !only || only.has(adapter) || only.has(adapter.replace(/^claude-/, ""));
const exists = (p) => stat(p).then(() => true, () => false);
const listDir = (p) => readdir(p).catch(() => []);
const readIf = async (p) => ((await exists(p)) ? readFile(p, "utf8") : null);
// ── Collect candidates ────────────────────────────────────────────────────────
const found = [];
function add(adapter, origin, name, description, content) {
if (!content?.trim()) return;
found.push({
name: kebab(name),
description: description || firstProseLine(content) || name,
content: content.trim(),
source: `scrape:${adapter}`,
origin,
});
}
// Skills are copied as whole folders (SKILL.md + references/scripts/templates),
// not flattened into memory files.
const skills = [];
async function addSkillDir(dir) {
const raw = await readIf(join(dir, "SKILL.md"));
if (!raw) return;
const { meta } = parseFrontmatter(raw);
skills.push({
name: kebab(meta.name ?? basename(dir)),
description: (meta.description || "skill").slice(0, 200),
dir,
});
}
// claude-memory — ~/.claude/projects/<slug>/memory/*.md
const slug = repo.replace(/[/.]/g, "-");
const autoMemDir = join(claudeHome, "projects", slug, "memory");
if (runs("claude-memory")) {
for (const file of await listDir(autoMemDir)) {
if (!file.endsWith(".md") || file === "MEMORY.md") continue; // MEMORY.md is an index
const raw = await readFile(join(autoMemDir, file), "utf8");
const { meta, body } = parseFrontmatter(raw);
add("claude-memory", join(autoMemDir, file), meta.name ?? basename(file, ".md"), meta.description, body);
}
}
// claude-instructions — CLAUDE.md variants
if (runs("claude-instructions")) {
for (const relPath of ["CLAUDE.md", "CLAUDE.local.md", ".claude/CLAUDE.md"]) {
const raw = await readIf(join(repo, relPath));
if (raw) {
add(
"claude-instructions",
join(repo, relPath),
relPath === "CLAUDE.local.md" ? "claude-local-instructions" : "claude-instructions",
`Project instructions (${relPath}) for ${project}`,
raw,
);
}
}
}
// claude-skills — .claude/{skills,commands,agents}, plus ~/.claude/skills with
// --include-user-skills (user-level skills are personal, so opt-in)
if (runs("claude-skills")) {
for (const dir of await listDir(join(repo, ".claude", "skills"))) {
await addSkillDir(join(repo, ".claude", "skills", dir));
}
if (args.includeUserSkills) {
for (const dir of await listDir(join(claudeHome, "skills"))) {
await addSkillDir(join(claudeHome, "skills", dir));
}
}
for (const kind of ["commands", "agents"]) {
for (const file of await listDir(join(repo, ".claude", kind))) {
if (!file.endsWith(".md")) continue;
const raw = await readFile(join(repo, ".claude", kind, file), "utf8");
const { meta, body } = parseFrontmatter(raw);
add(
"claude-skills",
join(repo, ".claude", kind, file),
`${kind.replace(/s$/, "")}-${meta.name ?? basename(file, ".md")}`,
meta.description,
body,
);
}
}
}
// codex — AGENTS.md
if (runs("codex")) {
const raw = await readIf(join(repo, "AGENTS.md"));
if (raw) add("codex", join(repo, "AGENTS.md"), "codex-instructions", `Codex instructions (AGENTS.md) for ${project}`, raw);
}
// cursor — .cursorrules, .cursor/rules/*
if (runs("cursor")) {
const raw = await readIf(join(repo, ".cursorrules"));
if (raw) add("cursor", join(repo, ".cursorrules"), "cursor-rules", `Cursor rules for ${project}`, raw);
for (const file of await listDir(join(repo, ".cursor", "rules"))) {
const raw2 = await readFile(join(repo, ".cursor", "rules", file), "utf8");
const { meta, body } = parseFrontmatter(raw2);
add("cursor", join(repo, ".cursor", "rules", file), `cursor-rule-${basename(file).replace(/\.[^.]+$/, "")}`, meta.description, body);
}
}
// claude-user (opt-in) — user-level CLAUDE.md
if (runs("claude-user") && args.includeUser) {
const raw = await readIf(join(claudeHome, "CLAUDE.md"));
if (raw) add("claude-user", join(claudeHome, "CLAUDE.md"), "user-claude-md", "User-level Claude instructions", raw);
}
// ── Report / write ────────────────────────────────────────────────────────────
if (!found.length && !skills.length) {
console.log(`Nothing to scrape in ${repo} (checked ${autoMemDir}, CLAUDE.md, .claude/, AGENTS.md, .cursor*).`);
process.exit(0);
}
if (found.length) {
console.log(`${found.length} memories found in ${repo}:\n`);
for (const m of found) {
console.log(` ${m.name} [${m.source}]`);
console.log(` ${m.origin}`);
}
}
if (skills.length) {
console.log(`${skills.length} skill folder(s) found:\n`);
for (const s of skills) {
console.log(` skills/${s.name}/`);
console.log(` ${s.dir}`);
}
}
if (args.dryRun) {
console.log("\nDry run — nothing written.");
process.exit(0);
}
await mkdir(projectDir, { recursive: true });
const now = new Date().toISOString();
console.log("");
for (const m of found) {
const target = join(projectDir, `${m.name}.md`);
const prior = await readIf(target);
const created = prior ? (parseFrontmatter(prior).meta.created ?? now) : now;
const fm = [
`name: ${m.name}`,
`description: ${m.description}`,
`source: ${m.source}`,
`origin: ${m.origin}`,
`created: ${created}`,
`updated: ${now}`,
].join("\n");
await writeFile(target, `---\n${fm}\n---\n\n${m.content}\n`);
console.log(`${prior ? "↻" : "✓"} ${m.name}.md`);
}
for (const s of skills) {
await cp(s.dir, join(projectDir, "skills", s.name), { recursive: true });
console.log(`✓ skills/${s.name}/`);
}
// Refresh this project's MEMORY.md index from everything in its space.
const entries = [];
for (const file of (await readdir(projectDir)).sort()) {
const full = join(projectDir, file);
const st = await stat(full);
if (st.isFile() && file.endsWith(".md") && file !== "MEMORY.md") {
const { meta, body } = parseFrontmatter(await readFile(full, "utf8"));
entries.push(`- [${meta.name ?? basename(file, ".md")}](${file}) — ${meta.description ?? firstProseLine(body)}`);
} else if (st.isDirectory() && file === "skills") {
for (const s of (await readdir(full)).sort()) {
const raw = await readIf(join(full, s, "SKILL.md"));
if (!raw) continue;
const { meta } = parseFrontmatter(raw);
entries.push(`- [skill-${s}](skills/${s}/SKILL.md) — ${(meta.description || "skill").slice(0, 200)}`);
}
}
}
await writeFile(join(projectDir, "MEMORY.md"), `# Memory index — ${project}\n\n${entries.join("\n")}\n`);
console.log(`\n${found.length} memories + ${skills.length} skill folder(s) written to ${projectDir}; MEMORY.md refreshed.`);