memoryrail 0.0.0-stage → 0.1.0

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,190 @@
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
+ **Git-native project memory for coding agents.**
4
+
5
+ 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.
6
+
7
+ - **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.
8
+ - **Plain files, no database.** `.memoryrail/memories/*.md`, one memory per file. Diff it, grep it, review it.
9
+ - **Travels with the repo.** Clone it and the memory comes along. Branches carry their own memory.
10
+ - **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.
11
+ - **Safe to commit.** Text that looks like a secret is refused. An optional review gate holds agent-written memories until you approve them.
12
+ - **Local and private.** No account, no server, no network calls.
13
+ - **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.
14
+
15
+ > Status: **0.1, early.** The format is specified ([docs/SPEC.md](docs/SPEC.md)) and may change before 1.0.
16
+
17
+ ## What it looks like
18
+
19
+ ```
20
+ $ memoryrail precheck "cache images on disk" --file src/images/a.ts
21
+ Check these before you proceed. Cite what you rely on as [per REF]. If your plan
22
+ conflicts with a constraint or decision, stop and ask.
23
+
24
+ - [CON-5413] (constraint) Never commit .env files — pinned rule
25
+ - [ATT-f485] (attempt) Caching images locally violates the provider TOS — about
26
+ src/images/a.ts; matches your plan
27
+ We cached originals to speed up the gallery. Provider flagged it; had to purge.
28
+ ```
29
+
30
+ Each memory is a small file you can read and edit by hand:
31
+
32
+ ```markdown
33
+ ---
34
+ id: "20261010-caching-images-locally-violates-the-provider-tos"
35
+ type: "attempt"
36
+ title: "Caching images locally violates the provider TOS"
37
+ status: "active"
38
+ links: ["src/images"]
39
+ ---
40
+
41
+ We cached originals to speed up the gallery. The provider flagged it and we had to purge.
42
+ ```
43
+
44
+ Six types: `decision`, `constraint`, `gotcha`, `attempt` (tried and failed, with why), `thread` (unfinished work), `session` (end-of-session handoff).
45
+
46
+ ## Install
47
+
48
+ Not published to npm yet. From a clone:
49
+
50
+ ```sh
51
+ git clone https://github.com/OutVersus/memoryrail && cd memoryrail
52
+ npm install && npm run build && npm link # puts `memoryrail` on your PATH
53
+ ```
54
+
55
+ Once published, every `memoryrail ...` below can be run as `npx memoryrail ...`.
56
+
57
+ ## Quick start
58
+
59
+ ```sh
60
+ memoryrail init
61
+ memoryrail remember "Use Drizzle over Prisma" --type decision \
62
+ --body "Edge deploys need a small runtime." --link src/db.ts
63
+ memoryrail remember "Never commit .env files" --type constraint --pin
64
+ memoryrail sync # writes AGENTS.md, CLAUDE.md, .cursor/rules/memoryrail.mdc
65
+ memoryrail install claude # registers the MCP server in .mcp.json (or: cursor, all)
66
+ memoryrail doctor # checks that everything is valid, safe, synced and wired up
67
+ git add .memoryrail AGENTS.md CLAUDE.md .mcp.json && git commit -m "Add project memory"
68
+ ```
69
+
70
+ ## Connect an agent (MCP)
71
+
72
+ ```sh
73
+ # Claude Code or Cursor, written into the repo's own config (keeps other servers)
74
+ memoryrail install claude # .mcp.json
75
+ memoryrail install cursor # .cursor/mcp.json
76
+ ```
77
+
78
+ Or by hand in any MCP client config (Claude Desktop, ...):
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "memoryrail": { "command": "memoryrail", "args": ["serve"] }
84
+ }
85
+ }
86
+ ```
87
+
88
+ The server uses the nearest `.memoryrail/` above its working directory, or pass `--root <path>`.
89
+
90
+ | Tool | Use |
91
+ |---|---|
92
+ | `resume` | Call first in a session: last handoff, open threads, constraints, recent decisions, gotchas |
93
+ | `precheck` | Call before changing code: the rules, failed attempts, gotchas and decisions that apply to a plan or files |
94
+ | `recall` | Search memory for what you are about to work on (token-budgeted) |
95
+ | `remember` | Record a decision, constraint, gotcha, failed attempt or thread; `supersedes` replaces an old one |
96
+ | `handoff` | Call last: what was done and the next steps |
97
+ | `list_memories`, `forget`, `resolve_thread` | Housekeeping |
98
+
99
+ ## The workflow
100
+
101
+ 1. **Start:** the agent calls `resume` (or you run `memoryrail resume` and paste the output).
102
+ 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.
103
+ 3. **Finish:** it calls `handoff` with a summary and next steps.
104
+ 4. **Review:** memory changes show up in the git diff like any other change. `memoryrail sync` and `memoryrail lint` run in CI.
105
+
106
+ ## CLI
107
+
108
+ ```
109
+ memoryrail init
110
+ memoryrail remember <title> [--type decision|constraint|gotcha|attempt|thread|session] [--body ..] [--stdin]
111
+ [--tag ..] [--link <path>] [--pin] [--supersedes <id>] [--propose]
112
+ memoryrail precheck [plan] [--file <path>]... [--staged] [--fail] [--json]
113
+ memoryrail recall [query] [--type ..] [--limit n] [--budget tokens] [--all] [--json]
114
+ memoryrail list | show <id> | resolve <id> | forget <id> [--delete]
115
+ memoryrail review | approve <id>... [--all] | reject <id>...
116
+ memoryrail config review [off|agents]
117
+ memoryrail handoff --summary <text> [--next <step>]... [--title ..] [--link ..]
118
+ memoryrail resume
119
+ memoryrail sync [--check]
120
+ memoryrail lint [--strict] [--no-git] [--json]
121
+ memoryrail doctor
122
+ memoryrail install <claude|cursor|all> [--npx]
123
+ memoryrail serve [--root <path>]
124
+ ```
125
+
126
+ Anywhere an id is accepted you can use the id, any unambiguous prefix, or the short ref shown in output (`DEC-a3f9`).
127
+
128
+ ### Review gate
129
+
130
+ By default an agent's memories are active immediately and you review them in git. For more control:
131
+
132
+ ```sh
133
+ memoryrail config review agents
134
+ ```
135
+
136
+ 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.
137
+
138
+ ### Git hook
139
+
140
+ `precheck --staged --fail` exits 3 when a staged file has a relevant failed attempt, gotcha or decision. Pinned rules alone never fail it.
141
+
142
+ ```sh
143
+ # .git/hooks/pre-commit
144
+ memoryrail precheck --staged --fail || echo "Review the memories above before committing." >&2
145
+ ```
146
+
147
+ ### CI
148
+
149
+ ```yaml
150
+ - run: npx memoryrail sync --check # generated agent files are up to date
151
+ - run: npx memoryrail lint --strict # no stale links, duplicates, secrets or broken files
152
+ ```
153
+
154
+ ## Limitations
155
+
156
+ - **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.
157
+ - `maybe-stale` relies on git history; it is skipped outside a git repository.
158
+ - **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.
159
+ - `install` configures Claude Code and Cursor only. Other MCP clients need the config added by hand (see above).
160
+ - 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.
161
+
162
+ ## Library
163
+
164
+ ```ts
165
+ import { openStore, recall, precheck, sync } from "memoryrail";
166
+
167
+ const store = openStore();
168
+ store.add({ type: "gotcha", title: "Run migrations before seeding" });
169
+ console.log(recall(store.list(), { query: "migrations" }).rendered);
170
+ console.log(precheck(store.list(), { plan: "seed the database", files: ["db/seed.ts"] }).rendered);
171
+ sync(store);
172
+ ```
173
+
174
+ ## Credits
175
+
176
+ 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.
177
+
178
+ ## Contributing
179
+
180
+ ```sh
181
+ npm install
182
+ npm test
183
+ npm run build
184
+ ```
185
+
186
+ Issues and PRs welcome. Changes to the on-disk format must update [docs/SPEC.md](docs/SPEC.md) and the JSON Schema.
187
+
188
+ ## License
189
+
190
+ 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,364 @@
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 { 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
+ lint Find stale links, duplicates, and broken files
52
+ --strict Treat warnings as errors --no-git --json
53
+ doctor Health check: valid, safe, synced, and reachable by agents
54
+ install <claude|cursor|all> Register the MCP server in the repo's client config (--npx)
55
+ serve Run the MCP server over stdio
56
+
57
+ Run from anywhere inside a repo; the nearest .memoryrail/ is used.
58
+ Docs: https://memoryrail.si`;
59
+ class UsageError extends Error {
60
+ }
61
+ function readStdin() {
62
+ return fs.readFileSync(0, "utf8");
63
+ }
64
+ function parseTypes(values) {
65
+ if (!values?.length)
66
+ return undefined;
67
+ return values.map((v) => {
68
+ if (!isMemoryType(v))
69
+ throw new UsageError(`unknown type "${v}". Use one of: ${MEMORY_TYPES.join(", ")}`);
70
+ return v;
71
+ });
72
+ }
73
+ function toInt(v, name) {
74
+ if (v === undefined)
75
+ return undefined;
76
+ const n = Number(v);
77
+ if (!Number.isInteger(n) || n <= 0)
78
+ throw new UsageError(`--${name} must be a positive integer`);
79
+ return n;
80
+ }
81
+ function out(s) {
82
+ process.stdout.write(s.endsWith("\n") ? s : s + "\n");
83
+ }
84
+ function line(m) {
85
+ const status = m.status === "active" ? "" : ` [${m.status}]`;
86
+ return `${refOf(m).padEnd(8)} ${m.type.padEnd(10)} ${m.title}${status}`;
87
+ }
88
+ async function run(argv) {
89
+ const [cmd, ...rest] = argv;
90
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
91
+ out(HELP);
92
+ return 0;
93
+ }
94
+ if (cmd === "--version" || cmd === "-v") {
95
+ out(VERSION);
96
+ return 0;
97
+ }
98
+ const parse = (options) => parseArgs({ args: rest, options, allowPositionals: true, strict: true });
99
+ switch (cmd) {
100
+ case "init": {
101
+ const { created } = initRoot(process.cwd());
102
+ out(created ? `Created ${DIR_NAME}/ — try: memoryrail remember "Use X over Y" --type decision` : `${DIR_NAME}/ already exists`);
103
+ return 0;
104
+ }
105
+ case "remember": {
106
+ const { values, positionals } = parse({
107
+ type: { type: "string" },
108
+ body: { type: "string" },
109
+ stdin: { type: "boolean" },
110
+ tag: { type: "string", multiple: true },
111
+ link: { type: "string", multiple: true },
112
+ pin: { type: "boolean" },
113
+ supersedes: { type: "string" },
114
+ propose: { type: "boolean" },
115
+ "allow-secrets": { type: "boolean" },
116
+ });
117
+ const title = positionals.join(" ").trim();
118
+ if (!title)
119
+ throw new UsageError('usage: memoryrail remember "<title>" [--type decision] [--body ...]');
120
+ const type = parseTypes([values.type ?? "decision"])[0];
121
+ const body = values.stdin ? readStdin() : values.body;
122
+ const m = openStore().add({
123
+ type,
124
+ title,
125
+ body,
126
+ tags: values.tag,
127
+ links: values.link,
128
+ pinned: values.pin,
129
+ supersedes: values.supersedes,
130
+ status: values.propose ? "proposed" : "active",
131
+ allowSecrets: values["allow-secrets"],
132
+ });
133
+ out(m.status === "proposed"
134
+ ? `Proposed ${m.type} ${refOf(m)}: ${m.id}\nIt takes effect after \`memoryrail approve ${refOf(m)}\`.`
135
+ : `Saved ${m.type} ${refOf(m)}: ${m.id}`);
136
+ return 0;
137
+ }
138
+ case "precheck": {
139
+ const { values, positionals } = parse({
140
+ file: { type: "string", multiple: true },
141
+ staged: { type: "boolean" },
142
+ fail: { type: "boolean" },
143
+ json: { type: "boolean" },
144
+ });
145
+ const store = openStore();
146
+ const files = [...(values.file ?? []), ...(values.staged ? stagedFiles(store.root) : [])];
147
+ const plan = positionals.join(" ");
148
+ if (!plan.trim() && !files.length) {
149
+ throw new UsageError('usage: memoryrail precheck "<what you plan to do>" [--file <path>]... | --staged');
150
+ }
151
+ const result = precheck(store.list(), { plan, files });
152
+ out(values.json ? JSON.stringify(result.warnings.map((w) => ({ ref: refOf(w.memory), reasons: w.reasons, memory: w.memory })), null, 2) : result.rendered);
153
+ // Pinned rules are always listed; they alone must not fail a hook, or every commit would be blocked.
154
+ const specific = result.warnings.some((w) => w.reasons.some((r) => r !== "pinned rule"));
155
+ return values.fail && specific ? 3 : 0;
156
+ }
157
+ case "review": {
158
+ parse({});
159
+ const proposed = openStore()
160
+ .list()
161
+ .filter((m) => m.status === "proposed")
162
+ .sort((a, b) => Date.parse(a.created) - Date.parse(b.created));
163
+ if (!proposed.length) {
164
+ out("Nothing waiting for review.");
165
+ return 0;
166
+ }
167
+ out(proposed.map((m) => `${renderMemory(m)}${m.supersedes ? `\n_would supersede: ${m.supersedes}_` : ""}`).join("\n\n"));
168
+ out(`\n${proposed.length} waiting. \`memoryrail approve <ref>\` to accept, \`memoryrail reject <ref>\` to discard.`);
169
+ return 0;
170
+ }
171
+ case "approve": {
172
+ const { values, positionals } = parse({ all: { type: "boolean" } });
173
+ const store = openStore();
174
+ const ids = values.all ? store.list().filter((m) => m.status === "proposed").map((m) => m.id) : positionals;
175
+ if (!ids.length)
176
+ throw new UsageError("usage: memoryrail approve <id|ref>... | --all");
177
+ for (const id of ids) {
178
+ const m = store.approve(id);
179
+ out(`Approved ${refOf(m)}: ${m.title}`);
180
+ }
181
+ return 0;
182
+ }
183
+ case "reject": {
184
+ const { positionals } = parse({});
185
+ if (!positionals.length)
186
+ throw new UsageError("usage: memoryrail reject <id|ref>...");
187
+ const store = openStore();
188
+ for (const id of positionals)
189
+ out(`Rejected ${store.reject(id).title}`);
190
+ return 0;
191
+ }
192
+ case "config": {
193
+ const { positionals } = parse({});
194
+ const store = openStore();
195
+ const [key, value] = positionals;
196
+ if (!key) {
197
+ out(JSON.stringify(store.config(), null, 2));
198
+ return 0;
199
+ }
200
+ if (key !== "review")
201
+ throw new UsageError(`unknown setting "${key}". Available: review`);
202
+ if (value === undefined) {
203
+ out(store.config().review);
204
+ return 0;
205
+ }
206
+ if (value !== "off" && value !== "agents")
207
+ throw new UsageError("review must be `off` or `agents`");
208
+ writeConfig(store.root, { ...store.config(), review: value });
209
+ out(value === "agents"
210
+ ? "Review gate on: decisions, constraints, gotchas and failed attempts written by agents over MCP wait for `memoryrail approve`."
211
+ : "Review gate off: agent-written memories take effect immediately (review them in git).");
212
+ return 0;
213
+ }
214
+ case "doctor": {
215
+ parse({});
216
+ const checks = doctor(openStore());
217
+ const mark = { ok: "ok ", warn: "warn", fail: "FAIL" };
218
+ for (const c of checks)
219
+ out(`${mark[c.status]} ${c.name.padEnd(10)} ${c.detail}`);
220
+ return checks.some((c) => c.status === "fail") ? 1 : 0;
221
+ }
222
+ case "install": {
223
+ const { values, positionals } = parse({ npx: { type: "boolean" } });
224
+ const target = positionals[0];
225
+ const clients = target === "all" ? Object.keys(CLIENT_FILES) : target && target in CLIENT_FILES ? [target] : [];
226
+ if (!clients.length)
227
+ throw new UsageError(`usage: memoryrail install <${Object.keys(CLIENT_FILES).join("|")}|all> [--npx]`);
228
+ const store = openStore();
229
+ for (const c of clients) {
230
+ const r = installMcp(store.root, c, { npx: values.npx });
231
+ out(`${r.action}: ${r.file}`);
232
+ }
233
+ out("Restart the client so it picks up the new MCP server.");
234
+ return 0;
235
+ }
236
+ case "recall": {
237
+ const { values, positionals } = parse({
238
+ type: { type: "string", multiple: true },
239
+ limit: { type: "string" },
240
+ budget: { type: "string" },
241
+ all: { type: "boolean" },
242
+ json: { type: "boolean" },
243
+ });
244
+ const result = recall(openStore().list(), {
245
+ query: positionals.join(" "),
246
+ types: parseTypes(values.type),
247
+ limit: toInt(values.limit, "limit"),
248
+ budget: toInt(values.budget, "budget"),
249
+ includeInactive: values.all,
250
+ });
251
+ if (values.json) {
252
+ out(JSON.stringify(result.hits.map((h) => h.memory), null, 2));
253
+ }
254
+ else {
255
+ out(result.rendered || "No matching memories.");
256
+ if (result.truncated)
257
+ out("\n(more results available: raise --limit or --budget)");
258
+ }
259
+ return 0;
260
+ }
261
+ case "list": {
262
+ const { values } = parse({ type: { type: "string", multiple: true }, all: { type: "boolean" }, json: { type: "boolean" } });
263
+ const types = parseTypes(values.type);
264
+ const items = openStore()
265
+ .list()
266
+ .filter((m) => (values.all || m.status === "active") && (!types || types.includes(m.type)))
267
+ .sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated));
268
+ out(values.json ? JSON.stringify(items, null, 2) : items.length ? items.map(line).join("\n") : "No memories yet.");
269
+ return 0;
270
+ }
271
+ case "show": {
272
+ const { positionals } = parse({});
273
+ if (!positionals[0])
274
+ throw new UsageError("usage: memoryrail show <id>");
275
+ out(renderMemory(openStore().resolveId(positionals[0])));
276
+ return 0;
277
+ }
278
+ case "resolve": {
279
+ const { positionals } = parse({});
280
+ if (!positionals[0])
281
+ throw new UsageError("usage: memoryrail resolve <id>");
282
+ const store = openStore();
283
+ const target = store.resolveId(positionals[0]);
284
+ if (target.type !== "thread")
285
+ throw new UsageError(`only threads can be resolved; ${target.id} is a ${target.type}`);
286
+ out(`Resolved ${store.setStatus(target.id, "resolved").id}`);
287
+ return 0;
288
+ }
289
+ case "forget": {
290
+ const { values, positionals } = parse({ delete: { type: "boolean" } });
291
+ if (!positionals[0])
292
+ throw new UsageError("usage: memoryrail forget <id> [--delete]");
293
+ const store = openStore();
294
+ if (values.delete)
295
+ out(`Deleted ${store.remove(positionals[0]).id}`);
296
+ else
297
+ out(`Archived ${store.setStatus(store.resolveId(positionals[0]).id, "archived").id}`);
298
+ return 0;
299
+ }
300
+ case "handoff": {
301
+ const { values } = parse({
302
+ summary: { type: "string" },
303
+ stdin: { type: "boolean" },
304
+ title: { type: "string" },
305
+ next: { type: "string", multiple: true },
306
+ link: { type: "string", multiple: true },
307
+ tag: { type: "string", multiple: true },
308
+ });
309
+ const summary = values.stdin ? readStdin() : values.summary;
310
+ if (!summary?.trim())
311
+ throw new UsageError('usage: memoryrail handoff --summary "what you did" [--next "step"]...');
312
+ const m = openStore().add(handoffToMemory({ title: values.title, summary, next: values.next, links: values.link, tags: values.tag }));
313
+ out(`Recorded handoff: ${m.id}\nNext session: run \`memoryrail resume\` for a prompt.`);
314
+ return 0;
315
+ }
316
+ case "resume": {
317
+ parse({});
318
+ out(resumePrompt(openStore().list()));
319
+ return 0;
320
+ }
321
+ case "sync": {
322
+ const { values } = parse({ check: { type: "boolean" } });
323
+ const changes = sync(openStore(), { check: values.check });
324
+ const pending = changes.filter((c) => c.action !== "unchanged");
325
+ for (const c of changes)
326
+ out(`${values.check && c.action !== "unchanged" ? "out of date" : c.action}: ${c.path}`);
327
+ if (values.check && pending.length) {
328
+ process.stderr.write("Generated files are out of date. Run `memoryrail sync` and commit the result.\n");
329
+ return 1;
330
+ }
331
+ return 0;
332
+ }
333
+ case "lint": {
334
+ const { values } = parse({ strict: { type: "boolean" }, "no-git": { type: "boolean" }, json: { type: "boolean" } });
335
+ const issues = lint(openStore(), { git: !values["no-git"] });
336
+ if (values.json)
337
+ out(JSON.stringify(issues, null, 2));
338
+ else if (!issues.length)
339
+ out("No issues found.");
340
+ else
341
+ for (const i of issues)
342
+ out(`${i.severity.padEnd(7)} ${i.rule.padEnd(20)} ${i.id ?? i.file ?? ""} ${i.message}`);
343
+ const failing = issues.some((i) => i.severity === "error" || (values.strict && i.severity === "warning"));
344
+ return failing ? 1 : 0;
345
+ }
346
+ case "serve": {
347
+ const { values } = parse({ root: { type: "string" } });
348
+ const root = values.root ? path.resolve(values.root) : undefined;
349
+ const { serveStdio } = await import("./mcp.js");
350
+ await serveStdio(root ? new Store(root) : openStore());
351
+ return -1; // keep the process alive; the transport owns it now
352
+ }
353
+ default:
354
+ throw new UsageError(`unknown command "${cmd}". Run \`memoryrail help\`.`);
355
+ }
356
+ }
357
+ run(process.argv.slice(2)).then((code) => {
358
+ if (code >= 0)
359
+ process.exitCode = code;
360
+ }, (err) => {
361
+ const msg = err instanceof Error ? err.message : String(err);
362
+ process.stderr.write(`memoryrail: ${msg}\n`);
363
+ process.exitCode = err instanceof UsageError ? 2 : 1;
364
+ });
@@ -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,55 @@
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 { 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
+ const wired = Object.keys(CLIENT_FILES).filter((c) => isInstalled(store.root, c));
46
+ if (wired.length)
47
+ add("ok", "agents", `MCP server registered for: ${wired.join(", ")}`);
48
+ else
49
+ add("warn", "agents", "no MCP client configured in this repo; run `memoryrail install claude` or `install cursor`");
50
+ if (fs.existsSync(path.join(store.root, ".git")))
51
+ add("ok", "git", "inside a git repository");
52
+ else
53
+ add("warn", "git", "not a git repository; memory will not be versioned or reviewable");
54
+ return checks;
55
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Minimal frontmatter codec. Values are written as JSON, which is a valid
3
+ * subset of YAML, so the files stay readable by any YAML tooling while this
4
+ * package needs no YAML dependency.
5
+ */
6
+ export interface Parsed {
7
+ data: Record<string, unknown>;
8
+ body: string;
9
+ }
10
+ export declare function parse(text: string): Parsed;
11
+ export declare function stringify(data: Record<string, unknown>, body: string): string;