memoryrail 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MemoryRail contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,197 @@
1
- # Temporary Holding Version
1
+ # MemoryRail
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/memoryrail)](https://www.npmjs.com/package/memoryrail)
4
+ [![CI](https://github.com/OutVersus/memoryrail/actions/workflows/ci.yml/badge.svg)](https://github.com/OutVersus/memoryrail/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **Git-native project memory for coding agents.** Website: [memoryrail.si](https://memoryrail.si)
8
+
9
+ Decisions, constraints, gotchas and failed attempts live in your repo as plain Markdown. People can read and review them in a PR. Any agent that speaks MCP (Claude Code, Cursor, Codex, Copilot, Aider, ...) can read and write them, and gets warned before it repeats a mistake.
10
+
11
+ - **Warns before a mistake repeats.** `memoryrail precheck` takes what an agent is about to do and the files it will touch, and returns the rules, failed attempts, gotchas and decisions that apply.
12
+ - **Plain files, no database.** `.memoryrail/memories/*.md`, one memory per file. Diff it, grep it, review it.
13
+ - **Travels with the repo.** Clone it and the memory comes along. Branches carry their own memory.
14
+ - **One source, every agent.** `memoryrail sync` keeps `AGENTS.md`, `CLAUDE.md` and `.cursor/rules` in step, so you maintain one memory instead of three config files.
15
+ - **Safe to commit.** Text that looks like a secret is refused. An optional review gate holds agent-written memories until you approve them.
16
+ - **Local and private.** No account, no server, no network calls.
17
+ - **Stays honest.** Memories link to files; `memoryrail lint` flags the ones whose files changed or vanished. New decisions *supersede* old ones instead of overwriting them.
18
+
19
+ > Status: **0.1, early.** The format is specified ([docs/SPEC.md](docs/SPEC.md)) and may change before 1.0.
20
+
21
+ ## What it looks like
22
+
23
+ ```
24
+ $ memoryrail precheck "cache images on disk" --file src/images/a.ts
25
+ Check these before you proceed. Cite what you rely on as [per REF]. If your plan
26
+ conflicts with a constraint or decision, stop and ask.
27
+
28
+ - [CON-5413] (constraint) Never commit .env files — pinned rule
29
+ - [ATT-f485] (attempt) Caching images locally violates the provider TOS — about
30
+ src/images/a.ts; matches your plan
31
+ We cached originals to speed up the gallery. Provider flagged it; had to purge.
32
+ ```
33
+
34
+ Each memory is a small file you can read and edit by hand:
35
+
36
+ ```markdown
37
+ ---
38
+ id: "20261010-caching-images-locally-violates-the-provider-tos"
39
+ type: "attempt"
40
+ title: "Caching images locally violates the provider TOS"
41
+ status: "active"
42
+ links: ["src/images"]
43
+ ---
44
+
45
+ We cached originals to speed up the gallery. The provider flagged it and we had to purge.
46
+ ```
47
+
48
+ Six types: `decision`, `constraint`, `gotcha`, `attempt` (tried and failed, with why), `thread` (unfinished work), `session` (end-of-session handoff).
49
+
50
+ ## Install
51
+
52
+ Needs Node 20 or newer.
53
+
54
+ ```sh
55
+ npm install -g memoryrail # puts `memoryrail` on your PATH
56
+ ```
57
+
58
+ Or run any command without installing, as `npx memoryrail <command>`. Note that `memoryrail install` registers the server as the command `memoryrail`, so that needs the global install; use `memoryrail install claude --npx` to register `npx -y memoryrail serve` instead.
59
+
60
+ ## Quick start
61
+
62
+ ```sh
63
+ memoryrail init
64
+ memoryrail remember "Use Drizzle over Prisma" --type decision \
65
+ --body "Edge deploys need a small runtime." --link src/db.ts
66
+ memoryrail remember "Never commit .env files" --type constraint --pin
67
+ memoryrail sync # writes AGENTS.md, CLAUDE.md, .cursor/rules/memoryrail.mdc
68
+ memoryrail install claude # registers the MCP server in .mcp.json (or: cursor, all)
69
+ memoryrail doctor # checks that everything is valid, safe, synced and wired up
70
+ git add .memoryrail AGENTS.md CLAUDE.md .mcp.json && git commit -m "Add project memory"
71
+ ```
72
+
73
+ ## Connect an agent (MCP)
74
+
75
+ Setup for **Claude Code, Cursor, OpenAI Codex and GitHub Copilot** (VS Code, CLI and the cloud agent) is in [docs/CLIENTS.md](docs/CLIENTS.md). The short version for Claude Code and Cursor:
76
+
77
+ ```sh
78
+ # Claude Code or Cursor, written into the repo's own config (keeps other servers)
79
+ memoryrail install claude # .mcp.json
80
+ memoryrail install cursor # .cursor/mcp.json
81
+ ```
82
+
83
+ Or by hand in any MCP client config (Claude Desktop, ...):
84
+
85
+ ```json
86
+ {
87
+ "mcpServers": {
88
+ "memoryrail": { "command": "memoryrail", "args": ["serve"] }
89
+ }
90
+ }
91
+ ```
92
+
93
+ The server uses the nearest `.memoryrail/` above its working directory, or pass `--root <path>`.
94
+
95
+ | Tool | Use |
96
+ |---|---|
97
+ | `resume` | Call first in a session: last handoff, open threads, constraints, recent decisions, gotchas |
98
+ | `precheck` | Call before changing code: the rules, failed attempts, gotchas and decisions that apply to a plan or files |
99
+ | `recall` | Search memory for what you are about to work on (token-budgeted) |
100
+ | `remember` | Record a decision, constraint, gotcha, failed attempt or thread; `supersedes` replaces an old one |
101
+ | `handoff` | Call last: what was done and the next steps |
102
+ | `list_memories`, `forget`, `resolve_thread` | Housekeeping |
103
+
104
+ ## The workflow
105
+
106
+ 1. **Start:** the agent calls `resume` (or you run `memoryrail resume` and paste the output).
107
+ 2. **Work:** it calls `precheck` before changing code, `recall "<area>"` for detail, and `remember` when it decides something, hits a trap, or watches an approach fail. It cites what it relies on as `[per DEC-a3f9]` and stops to ask if its plan conflicts with a constraint or decision.
108
+ 3. **Finish:** it calls `handoff` with a summary and next steps.
109
+ 4. **Review:** memory changes show up in the git diff like any other change. `memoryrail sync` and `memoryrail lint` run in CI.
110
+
111
+ ## CLI
112
+
113
+ ```
114
+ memoryrail init
115
+ memoryrail remember <title> [--type decision|constraint|gotcha|attempt|thread|session] [--body ..] [--stdin]
116
+ [--tag ..] [--link <path>] [--pin] [--supersedes <id>] [--propose]
117
+ memoryrail precheck [plan] [--file <path>]... [--staged] [--fail] [--json]
118
+ memoryrail recall [query] [--type ..] [--limit n] [--budget tokens] [--all] [--json]
119
+ memoryrail list | show <id> | resolve <id> | forget <id> [--delete]
120
+ memoryrail review | approve <id>... [--all] | reject <id>...
121
+ memoryrail config review [off|agents]
122
+ memoryrail handoff --summary <text> [--next <step>]... [--title ..] [--link ..]
123
+ memoryrail resume
124
+ memoryrail sync [--check] [--also <path>]...
125
+ memoryrail lint [--strict] [--no-git] [--json]
126
+ memoryrail doctor
127
+ memoryrail install <claude|cursor|all> [--npx]
128
+ memoryrail serve [--root <path>]
129
+ ```
130
+
131
+ Anywhere an id is accepted you can use the id, any unambiguous prefix, or the short ref shown in output (`DEC-a3f9`).
132
+
133
+ ### Review gate
134
+
135
+ By default an agent's memories are active immediately and you review them in git. For more control:
136
+
137
+ ```sh
138
+ memoryrail config review agents
139
+ ```
140
+
141
+ Now decisions, constraints, gotchas and failed attempts written over MCP are saved as *proposed*. They are ignored by `recall`, `precheck` and `sync` until you run `memoryrail review` and then `approve` or `reject`. A proposal that supersedes an older memory leaves it untouched until approved.
142
+
143
+ ### Git hook
144
+
145
+ `precheck --staged --fail` exits 3 when a staged file has a relevant failed attempt, gotcha or decision. Pinned rules alone never fail it.
146
+
147
+ ```sh
148
+ # .git/hooks/pre-commit
149
+ memoryrail precheck --staged --fail || echo "Review the memories above before committing." >&2
150
+ ```
151
+
152
+ ### CI
153
+
154
+ ```yaml
155
+ - run: npx memoryrail sync --check # generated agent files are up to date
156
+ - run: npx memoryrail lint --strict # no stale links, duplicates, secrets or broken files
157
+ ```
158
+
159
+ ## Limitations
160
+
161
+ - **Search is lexical** (BM25 over title, tags, links and body, with a mild recency boost). It has no synonym or semantic matching, so `recall "database"` will not find a memory that only says "Postgres". Write titles that state the conclusion in the words you would search for, and use tags. Optional embeddings are planned.
162
+ - `maybe-stale` relies on git history; it is skipped outside a git repository.
163
+ - **The secret scanner is a pattern check, not a guarantee.** It catches common key formats and `password = ...` assignments, and it will miss secrets in unusual shapes. Treat it as a safety net, and don't paste credentials into memory.
164
+ - `install` configures Claude Code and Cursor only. For Codex and Copilot, follow [docs/CLIENTS.md](docs/CLIENTS.md); those snippets come from the vendors' documentation and haven't been run against MemoryRail by us.
165
+ - Codex stops reading `AGENTS.md` after 32 KiB by default. `memoryrail doctor` warns when yours gets that large.
166
+ - This is an early release (0.x). The format and commands may change before 1.0, and it has had little use on real projects so far. Please open issues.
167
+ - The memory is only as good as what agents and people record. MemoryRail makes recording cheap and reviewable; it does not do it for you.
168
+
169
+ ## Library
170
+
171
+ ```ts
172
+ import { openStore, recall, precheck, sync } from "memoryrail";
173
+
174
+ const store = openStore();
175
+ store.add({ type: "gotcha", title: "Run migrations before seeding" });
176
+ console.log(recall(store.list(), { query: "migrations" }).rendered);
177
+ console.log(precheck(store.list(), { plan: "seed the database", files: ["db/seed.ts"] }).rendered);
178
+ sync(store);
179
+ ```
180
+
181
+ ## Credits
182
+
183
+ MemoryRail borrows ideas, not code, from three open-source projects: [projectmem](https://github.com/riponcm/projectmem) (warn before a failed approach is repeated; a doctor command), [agent-memory](https://github.com/xChuCx/agent-memory) (a human review gate, secret scanning, one-command client setup) and [Memory Trail](https://github.com/frmoretto/memory-trail) (short citable decision ids, "stop if it conflicts"). Look at them too; they take different approaches.
184
+
185
+ ## Contributing
186
+
187
+ ```sh
188
+ npm install
189
+ npm test
190
+ npm run build
191
+ ```
192
+
193
+ Issues and PRs welcome. Changes to the on-disk format must update [docs/SPEC.md](docs/SPEC.md) and the JSON Schema.
194
+
195
+ ## License
196
+
197
+ MIT. See [LICENSE](LICENSE).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ import { doctor } from "./doctor.js";
6
+ import { stagedFiles } from "./git.js";
7
+ import { handoffToMemory, resumePrompt } from "./handoff.js";
8
+ import { CLIENT_FILES, installMcp } from "./install.js";
9
+ import { lint } from "./lint.js";
10
+ import { precheck } from "./precheck.js";
11
+ import { refOf } from "./refs.js";
12
+ import { recall, renderMemory } from "./search.js";
13
+ import { Store, DIR_NAME, initRoot, openStore, writeConfig } from "./store.js";
14
+ import { DEFAULT_TARGETS, sync } from "./sync.js";
15
+ import { VERSION } from "./version.js";
16
+ import { isMemoryType, MEMORY_TYPES } from "./types.js";
17
+ const HELP = `memoryrail ${VERSION} — git-native project memory for coding agents
18
+
19
+ Usage: memoryrail <command> [options]
20
+
21
+ Commands:
22
+ init Create .memoryrail/ in the current directory
23
+ remember <title> Record a memory
24
+ --type <t> ${MEMORY_TYPES.join(" | ")} (default: decision)
25
+ --body <text> Details (or --stdin to read from stdin)
26
+ --tag <t> Repeatable
27
+ --link <path> Repo file this is about (repeatable); enables staleness checks
28
+ --pin Always include in recall and sync
29
+ --supersedes <id> Replace an earlier memory, keeping its history
30
+ --propose Save as proposed; takes effect after \`approve\`
31
+ --allow-secrets Skip the secret scan (humans only)
32
+ precheck [plan] Before acting: which rules, failed attempts and gotchas apply?
33
+ --file <path> Repeatable --staged (use files staged in git)
34
+ --fail Exit 3 if anything relevant is found (for git hooks) --json
35
+ recall [query] Search memories, ranked and token-budgeted
36
+ --type <t> Repeatable filter
37
+ --limit <n> --budget <tokens> --all (include superseded/archived) --json
38
+ list List memories (--type, --all, --json)
39
+ show <id> Print one memory (id, prefix, or ref like DEC-a3f9)
40
+ review Show memories waiting for approval
41
+ approve <id>... | --all Accept proposed memories
42
+ reject <id>... Discard proposed memories
43
+ config [review [off|agents]] Show or set the review gate for agent-written memories
44
+ resolve <id> Mark a thread resolved
45
+ forget <id> Archive a memory (--delete to remove the file)
46
+ handoff Record an end-of-session summary
47
+ --summary <text> (or --stdin) --title <t> --next <step> (repeatable) --link <path>
48
+ resume Print a prompt that brings a fresh agent up to speed
49
+ sync Write AGENTS.md, CLAUDE.md and .cursor/rules from memory
50
+ --check Exit 1 if generated files are out of date (for CI)
51
+ --also <path> Also write the block into this file (repeatable), e.g.
52
+ .github/copilot-instructions.md
53
+ lint Find stale links, duplicates, and broken files
54
+ --strict Treat warnings as errors --no-git --json
55
+ doctor Health check: valid, safe, synced, and reachable by agents
56
+ install <claude|cursor|all> Register the MCP server in the repo's client config (--npx)
57
+ serve Run the MCP server over stdio
58
+
59
+ Run from anywhere inside a repo; the nearest .memoryrail/ is used.
60
+ Docs: https://memoryrail.si`;
61
+ class UsageError extends Error {
62
+ }
63
+ function readStdin() {
64
+ return fs.readFileSync(0, "utf8");
65
+ }
66
+ function parseTypes(values) {
67
+ if (!values?.length)
68
+ return undefined;
69
+ return values.map((v) => {
70
+ if (!isMemoryType(v))
71
+ throw new UsageError(`unknown type "${v}". Use one of: ${MEMORY_TYPES.join(", ")}`);
72
+ return v;
73
+ });
74
+ }
75
+ function toInt(v, name) {
76
+ if (v === undefined)
77
+ return undefined;
78
+ const n = Number(v);
79
+ if (!Number.isInteger(n) || n <= 0)
80
+ throw new UsageError(`--${name} must be a positive integer`);
81
+ return n;
82
+ }
83
+ function out(s) {
84
+ process.stdout.write(s.endsWith("\n") ? s : s + "\n");
85
+ }
86
+ function line(m) {
87
+ const status = m.status === "active" ? "" : ` [${m.status}]`;
88
+ return `${refOf(m).padEnd(8)} ${m.type.padEnd(10)} ${m.title}${status}`;
89
+ }
90
+ async function run(argv) {
91
+ const [cmd, ...rest] = argv;
92
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
93
+ out(HELP);
94
+ return 0;
95
+ }
96
+ if (cmd === "--version" || cmd === "-v") {
97
+ out(VERSION);
98
+ return 0;
99
+ }
100
+ const parse = (options) => parseArgs({ args: rest, options, allowPositionals: true, strict: true });
101
+ switch (cmd) {
102
+ case "init": {
103
+ const { created } = initRoot(process.cwd());
104
+ out(created ? `Created ${DIR_NAME}/ — try: memoryrail remember "Use X over Y" --type decision` : `${DIR_NAME}/ already exists`);
105
+ return 0;
106
+ }
107
+ case "remember": {
108
+ const { values, positionals } = parse({
109
+ type: { type: "string" },
110
+ body: { type: "string" },
111
+ stdin: { type: "boolean" },
112
+ tag: { type: "string", multiple: true },
113
+ link: { type: "string", multiple: true },
114
+ pin: { type: "boolean" },
115
+ supersedes: { type: "string" },
116
+ propose: { type: "boolean" },
117
+ "allow-secrets": { type: "boolean" },
118
+ });
119
+ const title = positionals.join(" ").trim();
120
+ if (!title)
121
+ throw new UsageError('usage: memoryrail remember "<title>" [--type decision] [--body ...]');
122
+ const type = parseTypes([values.type ?? "decision"])[0];
123
+ const body = values.stdin ? readStdin() : values.body;
124
+ const m = openStore().add({
125
+ type,
126
+ title,
127
+ body,
128
+ tags: values.tag,
129
+ links: values.link,
130
+ pinned: values.pin,
131
+ supersedes: values.supersedes,
132
+ status: values.propose ? "proposed" : "active",
133
+ allowSecrets: values["allow-secrets"],
134
+ });
135
+ out(m.status === "proposed"
136
+ ? `Proposed ${m.type} ${refOf(m)}: ${m.id}\nIt takes effect after \`memoryrail approve ${refOf(m)}\`.`
137
+ : `Saved ${m.type} ${refOf(m)}: ${m.id}`);
138
+ return 0;
139
+ }
140
+ case "precheck": {
141
+ const { values, positionals } = parse({
142
+ file: { type: "string", multiple: true },
143
+ staged: { type: "boolean" },
144
+ fail: { type: "boolean" },
145
+ json: { type: "boolean" },
146
+ });
147
+ const store = openStore();
148
+ const files = [...(values.file ?? []), ...(values.staged ? stagedFiles(store.root) : [])];
149
+ const plan = positionals.join(" ");
150
+ if (!plan.trim() && !files.length) {
151
+ throw new UsageError('usage: memoryrail precheck "<what you plan to do>" [--file <path>]... | --staged');
152
+ }
153
+ const result = precheck(store.list(), { plan, files });
154
+ out(values.json ? JSON.stringify(result.warnings.map((w) => ({ ref: refOf(w.memory), reasons: w.reasons, memory: w.memory })), null, 2) : result.rendered);
155
+ // Pinned rules are always listed; they alone must not fail a hook, or every commit would be blocked.
156
+ const specific = result.warnings.some((w) => w.reasons.some((r) => r !== "pinned rule"));
157
+ return values.fail && specific ? 3 : 0;
158
+ }
159
+ case "review": {
160
+ parse({});
161
+ const proposed = openStore()
162
+ .list()
163
+ .filter((m) => m.status === "proposed")
164
+ .sort((a, b) => Date.parse(a.created) - Date.parse(b.created));
165
+ if (!proposed.length) {
166
+ out("Nothing waiting for review.");
167
+ return 0;
168
+ }
169
+ out(proposed.map((m) => `${renderMemory(m)}${m.supersedes ? `\n_would supersede: ${m.supersedes}_` : ""}`).join("\n\n"));
170
+ out(`\n${proposed.length} waiting. \`memoryrail approve <ref>\` to accept, \`memoryrail reject <ref>\` to discard.`);
171
+ return 0;
172
+ }
173
+ case "approve": {
174
+ const { values, positionals } = parse({ all: { type: "boolean" } });
175
+ const store = openStore();
176
+ const ids = values.all ? store.list().filter((m) => m.status === "proposed").map((m) => m.id) : positionals;
177
+ if (!ids.length)
178
+ throw new UsageError("usage: memoryrail approve <id|ref>... | --all");
179
+ for (const id of ids) {
180
+ const m = store.approve(id);
181
+ out(`Approved ${refOf(m)}: ${m.title}`);
182
+ }
183
+ return 0;
184
+ }
185
+ case "reject": {
186
+ const { positionals } = parse({});
187
+ if (!positionals.length)
188
+ throw new UsageError("usage: memoryrail reject <id|ref>...");
189
+ const store = openStore();
190
+ for (const id of positionals)
191
+ out(`Rejected ${store.reject(id).title}`);
192
+ return 0;
193
+ }
194
+ case "config": {
195
+ const { positionals } = parse({});
196
+ const store = openStore();
197
+ const [key, value] = positionals;
198
+ if (!key) {
199
+ out(JSON.stringify(store.config(), null, 2));
200
+ return 0;
201
+ }
202
+ if (key !== "review")
203
+ throw new UsageError(`unknown setting "${key}". Available: review`);
204
+ if (value === undefined) {
205
+ out(store.config().review);
206
+ return 0;
207
+ }
208
+ if (value !== "off" && value !== "agents")
209
+ throw new UsageError("review must be `off` or `agents`");
210
+ writeConfig(store.root, { ...store.config(), review: value });
211
+ out(value === "agents"
212
+ ? "Review gate on: decisions, constraints, gotchas and failed attempts written by agents over MCP wait for `memoryrail approve`."
213
+ : "Review gate off: agent-written memories take effect immediately (review them in git).");
214
+ return 0;
215
+ }
216
+ case "doctor": {
217
+ parse({});
218
+ const checks = doctor(openStore());
219
+ const mark = { ok: "ok ", warn: "warn", fail: "FAIL" };
220
+ for (const c of checks)
221
+ out(`${mark[c.status]} ${c.name.padEnd(10)} ${c.detail}`);
222
+ return checks.some((c) => c.status === "fail") ? 1 : 0;
223
+ }
224
+ case "install": {
225
+ const { values, positionals } = parse({ npx: { type: "boolean" } });
226
+ const target = positionals[0];
227
+ const clients = target === "all" ? Object.keys(CLIENT_FILES) : target && target in CLIENT_FILES ? [target] : [];
228
+ if (!clients.length)
229
+ throw new UsageError(`usage: memoryrail install <${Object.keys(CLIENT_FILES).join("|")}|all> [--npx]`);
230
+ const store = openStore();
231
+ for (const c of clients) {
232
+ const r = installMcp(store.root, c, { npx: values.npx });
233
+ out(`${r.action}: ${r.file}`);
234
+ }
235
+ out("Restart the client so it picks up the new MCP server.");
236
+ return 0;
237
+ }
238
+ case "recall": {
239
+ const { values, positionals } = parse({
240
+ type: { type: "string", multiple: true },
241
+ limit: { type: "string" },
242
+ budget: { type: "string" },
243
+ all: { type: "boolean" },
244
+ json: { type: "boolean" },
245
+ });
246
+ const result = recall(openStore().list(), {
247
+ query: positionals.join(" "),
248
+ types: parseTypes(values.type),
249
+ limit: toInt(values.limit, "limit"),
250
+ budget: toInt(values.budget, "budget"),
251
+ includeInactive: values.all,
252
+ });
253
+ if (values.json) {
254
+ out(JSON.stringify(result.hits.map((h) => h.memory), null, 2));
255
+ }
256
+ else {
257
+ out(result.rendered || "No matching memories.");
258
+ if (result.truncated)
259
+ out("\n(more results available: raise --limit or --budget)");
260
+ }
261
+ return 0;
262
+ }
263
+ case "list": {
264
+ const { values } = parse({ type: { type: "string", multiple: true }, all: { type: "boolean" }, json: { type: "boolean" } });
265
+ const types = parseTypes(values.type);
266
+ const items = openStore()
267
+ .list()
268
+ .filter((m) => (values.all || m.status === "active") && (!types || types.includes(m.type)))
269
+ .sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated));
270
+ out(values.json ? JSON.stringify(items, null, 2) : items.length ? items.map(line).join("\n") : "No memories yet.");
271
+ return 0;
272
+ }
273
+ case "show": {
274
+ const { positionals } = parse({});
275
+ if (!positionals[0])
276
+ throw new UsageError("usage: memoryrail show <id>");
277
+ out(renderMemory(openStore().resolveId(positionals[0])));
278
+ return 0;
279
+ }
280
+ case "resolve": {
281
+ const { positionals } = parse({});
282
+ if (!positionals[0])
283
+ throw new UsageError("usage: memoryrail resolve <id>");
284
+ const store = openStore();
285
+ const target = store.resolveId(positionals[0]);
286
+ if (target.type !== "thread")
287
+ throw new UsageError(`only threads can be resolved; ${target.id} is a ${target.type}`);
288
+ out(`Resolved ${store.setStatus(target.id, "resolved").id}`);
289
+ return 0;
290
+ }
291
+ case "forget": {
292
+ const { values, positionals } = parse({ delete: { type: "boolean" } });
293
+ if (!positionals[0])
294
+ throw new UsageError("usage: memoryrail forget <id> [--delete]");
295
+ const store = openStore();
296
+ if (values.delete)
297
+ out(`Deleted ${store.remove(positionals[0]).id}`);
298
+ else
299
+ out(`Archived ${store.setStatus(store.resolveId(positionals[0]).id, "archived").id}`);
300
+ return 0;
301
+ }
302
+ case "handoff": {
303
+ const { values } = parse({
304
+ summary: { type: "string" },
305
+ stdin: { type: "boolean" },
306
+ title: { type: "string" },
307
+ next: { type: "string", multiple: true },
308
+ link: { type: "string", multiple: true },
309
+ tag: { type: "string", multiple: true },
310
+ });
311
+ const summary = values.stdin ? readStdin() : values.summary;
312
+ if (!summary?.trim())
313
+ throw new UsageError('usage: memoryrail handoff --summary "what you did" [--next "step"]...');
314
+ const m = openStore().add(handoffToMemory({ title: values.title, summary, next: values.next, links: values.link, tags: values.tag }));
315
+ out(`Recorded handoff: ${m.id}\nNext session: run \`memoryrail resume\` for a prompt.`);
316
+ return 0;
317
+ }
318
+ case "resume": {
319
+ parse({});
320
+ out(resumePrompt(openStore().list()));
321
+ return 0;
322
+ }
323
+ case "sync": {
324
+ const { values } = parse({ check: { type: "boolean" }, also: { type: "string", multiple: true } });
325
+ const targets = [...DEFAULT_TARGETS, ...(values.also ?? []).map((p) => ({ path: p }))];
326
+ const changes = sync(openStore(), { check: values.check, targets });
327
+ const pending = changes.filter((c) => c.action !== "unchanged");
328
+ for (const c of changes)
329
+ out(`${values.check && c.action !== "unchanged" ? "out of date" : c.action}: ${c.path}`);
330
+ if (values.check && pending.length) {
331
+ process.stderr.write("Generated files are out of date. Run `memoryrail sync` and commit the result.\n");
332
+ return 1;
333
+ }
334
+ return 0;
335
+ }
336
+ case "lint": {
337
+ const { values } = parse({ strict: { type: "boolean" }, "no-git": { type: "boolean" }, json: { type: "boolean" } });
338
+ const issues = lint(openStore(), { git: !values["no-git"] });
339
+ if (values.json)
340
+ out(JSON.stringify(issues, null, 2));
341
+ else if (!issues.length)
342
+ out("No issues found.");
343
+ else
344
+ for (const i of issues)
345
+ out(`${i.severity.padEnd(7)} ${i.rule.padEnd(20)} ${i.id ?? i.file ?? ""} ${i.message}`);
346
+ const failing = issues.some((i) => i.severity === "error" || (values.strict && i.severity === "warning"));
347
+ return failing ? 1 : 0;
348
+ }
349
+ case "serve": {
350
+ const { values } = parse({ root: { type: "string" } });
351
+ const root = values.root ? path.resolve(values.root) : undefined;
352
+ const { serveStdio } = await import("./mcp.js");
353
+ await serveStdio(root ? new Store(root) : openStore());
354
+ return -1; // keep the process alive; the transport owns it now
355
+ }
356
+ default:
357
+ throw new UsageError(`unknown command "${cmd}". Run \`memoryrail help\`.`);
358
+ }
359
+ }
360
+ run(process.argv.slice(2)).then((code) => {
361
+ if (code >= 0)
362
+ process.exitCode = code;
363
+ }, (err) => {
364
+ const msg = err instanceof Error ? err.message : String(err);
365
+ process.stderr.write(`memoryrail: ${msg}\n`);
366
+ process.exitCode = err instanceof UsageError ? 2 : 1;
367
+ });
@@ -0,0 +1,8 @@
1
+ import { type Store } from "./store.js";
2
+ export interface Check {
3
+ status: "ok" | "warn" | "fail";
4
+ name: string;
5
+ detail: string;
6
+ }
7
+ /** One-shot health report: is memory present, valid, safe, synced and reachable by agents? */
8
+ export declare function doctor(store: Store): Check[];
package/dist/doctor.js ADDED
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CLIENT_FILES, isInstalled } from "./install.js";
4
+ import { lint } from "./lint.js";
5
+ import { CODEX_DOC_LIMIT_BYTES, sync } from "./sync.js";
6
+ import { DIR_NAME, FORMAT_VERSION } from "./store.js";
7
+ /** One-shot health report: is memory present, valid, safe, synced and reachable by agents? */
8
+ export function doctor(store) {
9
+ const checks = [];
10
+ const add = (status, name, detail) => checks.push({ status, name, detail });
11
+ const config = store.config();
12
+ if (config.version === FORMAT_VERSION)
13
+ add("ok", "format", `${DIR_NAME}/ uses format version ${config.version}`);
14
+ else
15
+ add("fail", "format", `config says version ${config.version}; this tool reads version ${FORMAT_VERSION}`);
16
+ const { memories, errors } = store.loadAll();
17
+ if (errors.length)
18
+ add("fail", "files", `${errors.length} unreadable memory file(s); run \`memoryrail lint\``);
19
+ else
20
+ add("ok", "files", `${memories.length} memor${memories.length === 1 ? "y" : "ies"}, all valid`);
21
+ const issues = lint(store);
22
+ const secrets = issues.filter((i) => i.rule === "secret");
23
+ if (secrets.length)
24
+ add("fail", "secrets", `${secrets.length} memory file(s) look like they contain a secret; remove it and rotate the credential`);
25
+ else
26
+ add("ok", "secrets", "no secrets detected");
27
+ const otherErrors = issues.filter((i) => i.severity === "error" && i.rule !== "secret" && i.rule !== "invalid-file");
28
+ const warnings = issues.filter((i) => i.severity === "warning");
29
+ if (otherErrors.length)
30
+ add("fail", "integrity", `${otherErrors.length} error(s); run \`memoryrail lint\``);
31
+ else if (warnings.length)
32
+ add("warn", "freshness", `${warnings.length} warning(s) (stale links, duplicates); run \`memoryrail lint\``);
33
+ else
34
+ add("ok", "freshness", "no stale links or duplicates");
35
+ const proposed = memories.filter((m) => m.status === "proposed").length;
36
+ if (proposed)
37
+ add("warn", "review", `${proposed} proposed memor${proposed === 1 ? "y is" : "ies are"} waiting; run \`memoryrail review\``);
38
+ else
39
+ add("ok", "review", config.review === "agents" ? "review gate on, nothing waiting" : "review gate off (agent writes go live; review them in git)");
40
+ const drift = sync(store, { check: true }).filter((c) => c.action !== "unchanged");
41
+ if (drift.length)
42
+ add("warn", "sync", `${drift.map((d) => d.path).join(", ")} out of date; run \`memoryrail sync\``);
43
+ else
44
+ add("ok", "sync", "AGENTS.md, CLAUDE.md and Cursor rules are up to date");
45
+ try {
46
+ const bytes = fs.statSync(path.join(store.root, "AGENTS.md")).size;
47
+ if (bytes > CODEX_DOC_LIMIT_BYTES) {
48
+ add("warn", "size", `AGENTS.md is ${Math.round(bytes / 1024)} KiB; Codex stops reading it after 32 KiB by default. Archive stale memories or raise project_doc_max_bytes`);
49
+ }
50
+ }
51
+ catch {
52
+ /* no AGENTS.md yet; the sync check above already says so */
53
+ }
54
+ const wired = Object.keys(CLIENT_FILES).filter((c) => isInstalled(store.root, c));
55
+ if (wired.length)
56
+ add("ok", "agents", `MCP server registered for: ${wired.join(", ")}`);
57
+ else
58
+ add("warn", "agents", "no MCP client configured in this repo; run `memoryrail install claude` or `install cursor`");
59
+ if (fs.existsSync(path.join(store.root, ".git")))
60
+ add("ok", "git", "inside a git repository");
61
+ else
62
+ add("warn", "git", "not a git repository; memory will not be versioned or reviewable");
63
+ return checks;
64
+ }