memwarden 0.0.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.
Files changed (119) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +402 -0
  3. package/dist/bundle/bundle.d.ts +28 -0
  4. package/dist/bundle/bundle.js +85 -0
  5. package/dist/cli/bin.d.ts +2 -0
  6. package/dist/cli/bin.js +593 -0
  7. package/dist/cli/connect.d.ts +63 -0
  8. package/dist/cli/connect.js +121 -0
  9. package/dist/cli/hook.d.ts +24 -0
  10. package/dist/cli/hook.js +186 -0
  11. package/dist/cli/tools.d.ts +47 -0
  12. package/dist/cli/tools.js +246 -0
  13. package/dist/daemon/ensure.d.ts +12 -0
  14. package/dist/daemon/ensure.js +54 -0
  15. package/dist/daemon/service.d.ts +15 -0
  16. package/dist/daemon/service.js +210 -0
  17. package/dist/embedding/index.d.ts +10 -0
  18. package/dist/embedding/index.js +33 -0
  19. package/dist/embedding/local-embedding.d.ts +14 -0
  20. package/dist/embedding/local-embedding.js +80 -0
  21. package/dist/functions/access-tracker.d.ts +13 -0
  22. package/dist/functions/access-tracker.js +92 -0
  23. package/dist/functions/audit.d.ts +46 -0
  24. package/dist/functions/audit.js +0 -0
  25. package/dist/functions/cjk-segmenter.d.ts +6 -0
  26. package/dist/functions/cjk-segmenter.js +120 -0
  27. package/dist/functions/compress-synthetic.d.ts +2 -0
  28. package/dist/functions/compress-synthetic.js +104 -0
  29. package/dist/functions/config.d.ts +68 -0
  30. package/dist/functions/config.js +231 -0
  31. package/dist/functions/conflicts.d.ts +19 -0
  32. package/dist/functions/conflicts.js +328 -0
  33. package/dist/functions/context.d.ts +3 -0
  34. package/dist/functions/context.js +155 -0
  35. package/dist/functions/dedup.d.ts +11 -0
  36. package/dist/functions/dedup.js +51 -0
  37. package/dist/functions/dejafix.d.ts +96 -0
  38. package/dist/functions/dejafix.js +356 -0
  39. package/dist/functions/doctor.d.ts +29 -0
  40. package/dist/functions/doctor.js +137 -0
  41. package/dist/functions/forget.d.ts +3 -0
  42. package/dist/functions/forget.js +87 -0
  43. package/dist/functions/hybrid-search.d.ts +17 -0
  44. package/dist/functions/hybrid-search.js +205 -0
  45. package/dist/functions/index.d.ts +32 -0
  46. package/dist/functions/index.js +44 -0
  47. package/dist/functions/keyed-mutex.d.ts +1 -0
  48. package/dist/functions/keyed-mutex.js +21 -0
  49. package/dist/functions/logger.d.ts +6 -0
  50. package/dist/functions/logger.js +37 -0
  51. package/dist/functions/memory-utils.d.ts +2 -0
  52. package/dist/functions/memory-utils.js +29 -0
  53. package/dist/functions/observe.d.ts +5 -0
  54. package/dist/functions/observe.js +326 -0
  55. package/dist/functions/paths.d.ts +1 -0
  56. package/dist/functions/paths.js +38 -0
  57. package/dist/functions/privacy.d.ts +1 -0
  58. package/dist/functions/privacy.js +30 -0
  59. package/dist/functions/provenance.d.ts +9 -0
  60. package/dist/functions/provenance.js +57 -0
  61. package/dist/functions/quantized-vector-index.d.ts +60 -0
  62. package/dist/functions/quantized-vector-index.js +275 -0
  63. package/dist/functions/receipt.d.ts +31 -0
  64. package/dist/functions/receipt.js +95 -0
  65. package/dist/functions/search-index.d.ts +27 -0
  66. package/dist/functions/search-index.js +217 -0
  67. package/dist/functions/search.d.ts +25 -0
  68. package/dist/functions/search.js +523 -0
  69. package/dist/functions/stemmer.d.ts +1 -0
  70. package/dist/functions/stemmer.js +110 -0
  71. package/dist/functions/synonyms.d.ts +1 -0
  72. package/dist/functions/synonyms.js +69 -0
  73. package/dist/functions/turboquant.d.ts +53 -0
  74. package/dist/functions/turboquant.js +278 -0
  75. package/dist/functions/types.d.ts +217 -0
  76. package/dist/functions/types.js +8 -0
  77. package/dist/functions/vector-index.d.ts +25 -0
  78. package/dist/functions/vector-index.js +125 -0
  79. package/dist/functions/vector-persistence.d.ts +14 -0
  80. package/dist/functions/vector-persistence.js +75 -0
  81. package/dist/functions/verify.d.ts +13 -0
  82. package/dist/functions/verify.js +104 -0
  83. package/dist/index.d.ts +1 -0
  84. package/dist/index.js +219 -0
  85. package/dist/kernel/http.d.ts +24 -0
  86. package/dist/kernel/http.js +261 -0
  87. package/dist/kernel/index.d.ts +19 -0
  88. package/dist/kernel/index.js +21 -0
  89. package/dist/kernel/kernel.d.ts +80 -0
  90. package/dist/kernel/kernel.js +297 -0
  91. package/dist/kernel/pubsub.d.ts +21 -0
  92. package/dist/kernel/pubsub.js +38 -0
  93. package/dist/kernel/types.d.ts +139 -0
  94. package/dist/kernel/types.js +20 -0
  95. package/dist/mcp/bin.d.ts +2 -0
  96. package/dist/mcp/bin.js +27 -0
  97. package/dist/mcp/server.d.ts +34 -0
  98. package/dist/mcp/server.js +377 -0
  99. package/dist/observability/metrics.d.ts +26 -0
  100. package/dist/observability/metrics.js +104 -0
  101. package/dist/proxy/server.d.ts +30 -0
  102. package/dist/proxy/server.js +331 -0
  103. package/dist/state/kv.d.ts +41 -0
  104. package/dist/state/kv.js +50 -0
  105. package/dist/state/oplog.d.ts +25 -0
  106. package/dist/state/oplog.js +57 -0
  107. package/dist/state/schema.d.ts +60 -0
  108. package/dist/state/schema.js +88 -0
  109. package/dist/state/store-libsql.d.ts +46 -0
  110. package/dist/state/store-libsql.js +263 -0
  111. package/dist/state/store-memory.d.ts +23 -0
  112. package/dist/state/store-memory.js +121 -0
  113. package/dist/state/store.d.ts +87 -0
  114. package/dist/state/store.js +58 -0
  115. package/dist/triggers/api.d.ts +14 -0
  116. package/dist/triggers/api.js +510 -0
  117. package/dist/triggers/auth.d.ts +1 -0
  118. package/dist/triggers/auth.js +13 -0
  119. package/package.json +58 -0
@@ -0,0 +1,85 @@
1
+ //
2
+ // Brain Bundle — portable export/import of a memwarden store. Lets a user
3
+ // move their memory between machines, or seed a fresh instance, without a
4
+ // vendor in the loop. The bundle is plain JSON of the durable state:
5
+ // sessions, memories, per-session observations, and (if present) the
6
+ // TurboQuant index blob so vectors survive the move without re-embedding.
7
+ //
8
+ // Pure functions over a StateKV; the caller stamps the timestamp and does
9
+ // the file/HTTP I/O. Ed25519 signing and encryption layer on top later
10
+ // without changing this shape.
11
+ import { KV } from "../state/schema.js";
12
+ export const BRAIN_BUNDLE_KIND = "memwarden.brain";
13
+ export const BRAIN_BUNDLE_VERSION = 1;
14
+ const QUANT_BLOB_KEY = "index-blob"; // mirrors vector-persistence.ts
15
+ function countObservations(map) {
16
+ let n = 0;
17
+ for (const k of Object.keys(map))
18
+ n += map[k]?.length ?? 0;
19
+ return n;
20
+ }
21
+ /** Gather the durable store into a portable bundle. */
22
+ export async function exportBundle(kv) {
23
+ const sessions = await kv.list(KV.sessions).catch(() => []);
24
+ const memories = await kv.list(KV.memories).catch(() => []);
25
+ const observations = {};
26
+ for (const s of sessions) {
27
+ observations[s.id] = await kv
28
+ .list(KV.observations(s.id))
29
+ .catch(() => []);
30
+ }
31
+ const quantBlob = await kv
32
+ .get(KV.quantParams, QUANT_BLOB_KEY)
33
+ .catch(() => null);
34
+ const bundle = {
35
+ kind: BRAIN_BUNDLE_KIND,
36
+ version: BRAIN_BUNDLE_VERSION,
37
+ sessions,
38
+ memories,
39
+ observations,
40
+ };
41
+ if (typeof quantBlob === "string" && quantBlob.length > 0) {
42
+ bundle.quantBlob = quantBlob;
43
+ }
44
+ return bundle;
45
+ }
46
+ /** Validate a parsed object is a bundle we can import. */
47
+ export function isBrainBundle(value) {
48
+ const b = value;
49
+ return (!!b &&
50
+ b.kind === BRAIN_BUNDLE_KIND &&
51
+ typeof b.version === "number" &&
52
+ Array.isArray(b.sessions) &&
53
+ Array.isArray(b.memories) &&
54
+ typeof b.observations === "object" &&
55
+ b.observations !== null);
56
+ }
57
+ /**
58
+ * Write a bundle into a (typically fresh) store. Existing keys are
59
+ * overwritten (last-write-wins), matching the store's own semantics. The
60
+ * search/vector indexes rebuild lazily on the next mem::search.
61
+ */
62
+ export async function importBundle(kv, bundle) {
63
+ if (bundle.version !== BRAIN_BUNDLE_VERSION) {
64
+ throw new Error(`unsupported brain bundle version ${bundle.version} (expected ${BRAIN_BUNDLE_VERSION})`);
65
+ }
66
+ for (const s of bundle.sessions) {
67
+ await kv.set(KV.sessions, s.id, s);
68
+ }
69
+ for (const m of bundle.memories) {
70
+ await kv.set(KV.memories, m.id, m);
71
+ }
72
+ for (const sessionId of Object.keys(bundle.observations)) {
73
+ for (const o of bundle.observations[sessionId] ?? []) {
74
+ await kv.set(KV.observations(sessionId), o.id, o);
75
+ }
76
+ }
77
+ if (bundle.quantBlob) {
78
+ await kv.set(KV.quantParams, QUANT_BLOB_KEY, bundle.quantBlob);
79
+ }
80
+ return {
81
+ sessions: bundle.sessions.length,
82
+ memories: bundle.memories.length,
83
+ observations: countObservations(bundle.observations),
84
+ };
85
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,593 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // memwarden CLI.
4
+ //
5
+ // memwarden connect [tool] # wire an MCP client to the local brain
6
+ // memwarden export <file> # write a portable Brain Bundle
7
+ // memwarden import <file> # load a Brain Bundle into the daemon
8
+ //
9
+ // connect writes ./.mcp.json so any MCP client shares the one local brain;
10
+ // export/import move your memory between machines via the daemon's API.
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, } from "node:fs";
12
+ import { randomBytes } from "node:crypto";
13
+ import { fileURLToPath } from "node:url";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { homedir } from "node:os";
16
+ import { writeMcpConfig, mcpConfigPathFor, writeClaudeHooks, claudeSettingsPathFor, } from "./connect.js";
17
+ import { TOOLS, writeTool, writeAgentsMd } from "./tools.js";
18
+ import { handleSessionStart, handleCapture, readStdin } from "./hook.js";
19
+ import { ensureDaemon, daemonAlive, DAEMON_ENTRY } from "../daemon/ensure.js";
20
+ import { installService, uninstallService } from "../daemon/service.js";
21
+ import { getSecret } from "../functions/config.js";
22
+ const DAEMON_URL = process.env.MEMWARDEN_URL ?? "http://localhost:3111";
23
+ // Absolute paths to the installed CLI and MCP bins, so the configs/hooks we
24
+ // write run today (pre-publish) regardless of cwd. dist/cli/bin.js -> here.
25
+ const SELF = fileURLToPath(import.meta.url);
26
+ const MCP_BIN = join(dirname(SELF), "..", "mcp", "bin.js");
27
+ const HOOK_BASE = `"${process.execPath}" "${SELF}"`;
28
+ function authHeaders() {
29
+ const h = { "content-type": "application/json" };
30
+ // getSecret() resolves env first, then the persisted <dataDir>/secret file —
31
+ // so CLI commands run from a plain shell still authenticate to a secured daemon.
32
+ const secret = getSecret();
33
+ if (secret)
34
+ h["authorization"] = `Bearer ${secret}`;
35
+ return h;
36
+ }
37
+ // The CLI persists the generated secret here so repeat `up` runs reuse the same
38
+ // value (re-generating would orphan already-wired clients). The DAEMON resolves
39
+ // its secret from the MEMWARDEN_SECRET env var (config.ts getSecret), so `up`
40
+ // loads this file into process.env and bakes it into the service environment —
41
+ // the file is the CLI's source of truth, the env var is the runtime mechanism.
42
+ function secretFilePath(dataDir) {
43
+ return join(dataDir, "secret");
44
+ }
45
+ function readPersistedSecret(dataDir) {
46
+ const path = secretFilePath(dataDir);
47
+ if (!existsSync(path))
48
+ return undefined;
49
+ try {
50
+ const s = readFileSync(path, "utf8").trim();
51
+ return s || undefined;
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ function persistSecret(dataDir, secret) {
58
+ try {
59
+ mkdirSync(dataDir, { recursive: true });
60
+ }
61
+ catch {
62
+ // best-effort; the write below surfaces a real error
63
+ }
64
+ const path = secretFilePath(dataDir);
65
+ writeFileSync(path, secret + "\n", "utf8");
66
+ // Owner read/write only — this is a credential.
67
+ try {
68
+ chmodSync(path, 0o600);
69
+ }
70
+ catch {
71
+ // chmod can fail on some filesystems; the secret is still written
72
+ }
73
+ }
74
+ /**
75
+ * Resolve the API secret for `up`, generating one on first run. Priority:
76
+ * 1. an explicit --secret flag,
77
+ * 2. MEMWARDEN_SECRET already in the environment,
78
+ * 3. a previously-persisted secret under the data dir,
79
+ * 4. a freshly generated 32-byte random secret (persisted for next time).
80
+ * The resolved value is written back into process.env so any daemon spawned by
81
+ * ensureDaemon (which copies process.env) and every wired client inherit it.
82
+ */
83
+ function resolveSecret(flagSecret, dataDir) {
84
+ const existing = (flagSecret && flagSecret.trim()) ||
85
+ (process.env.MEMWARDEN_SECRET && process.env.MEMWARDEN_SECRET.trim()) ||
86
+ readPersistedSecret(dataDir);
87
+ let secret;
88
+ let generated = false;
89
+ if (existing) {
90
+ secret = existing;
91
+ }
92
+ else {
93
+ secret = randomBytes(32).toString("base64url");
94
+ generated = true;
95
+ }
96
+ persistSecret(dataDir, secret);
97
+ // Propagate to this process so the detached daemon spawn (ensureDaemon copies
98
+ // process.env) enforces it, and so authHeaders() can call the daemon.
99
+ process.env.MEMWARDEN_SECRET = secret;
100
+ return { secret, generated };
101
+ }
102
+ function parseFlags(argv) {
103
+ let target = "claude-code";
104
+ let url;
105
+ let secret;
106
+ for (let i = 0; i < argv.length; i++) {
107
+ const a = argv[i];
108
+ if (a === "--url")
109
+ url = argv[++i];
110
+ else if (a === "--secret")
111
+ secret = argv[++i];
112
+ else if (a && !a.startsWith("--"))
113
+ target = a;
114
+ }
115
+ return { target, url, secret };
116
+ }
117
+ function connect(rest) {
118
+ const { target, url, secret } = parseFlags(rest);
119
+ const withHooks = rest.includes("--with-hooks");
120
+ // Launch the local built MCP bin so the config works before publish.
121
+ const opts = {
122
+ ...(url ? { url } : {}),
123
+ ...(secret ? { secret } : {}),
124
+ mcpCommand: process.execPath,
125
+ mcpArgs: [MCP_BIN],
126
+ };
127
+ const path = mcpConfigPathFor(target, process.cwd());
128
+ const { created } = writeMcpConfig(path, opts);
129
+ console.log(`[memwarden] ${created ? "wrote" : "updated"} ${path} — '${target}' now shares the local brain.`);
130
+ if (withHooks) {
131
+ const settings = claudeSettingsPathFor(process.cwd());
132
+ writeClaudeHooks(settings, HOOK_BASE);
133
+ console.log(`[memwarden] wrote ${settings} — SessionStart auto-injects this project's memory; PostToolUse auto-captures.`);
134
+ }
135
+ else {
136
+ console.log(`[memwarden] tip: add --with-hooks to auto-inject context on session start and capture automatically.`);
137
+ }
138
+ }
139
+ async function hook(rest) {
140
+ const event = rest[0];
141
+ const raw = await readStdin();
142
+ const secret = getSecret();
143
+ const deps = {
144
+ baseUrl: DAEMON_URL,
145
+ ...(secret ? { secret } : {}),
146
+ };
147
+ let out = "";
148
+ if (event === "session-start")
149
+ out = await handleSessionStart(raw, deps);
150
+ else if (event === "capture")
151
+ out = await handleCapture(raw, deps);
152
+ if (out)
153
+ process.stdout.write(out);
154
+ }
155
+ async function exportBrain(file) {
156
+ if (!file)
157
+ throw new Error("usage: memwarden export <file>");
158
+ const res = await fetch(`${DAEMON_URL}/memwarden/export`, {
159
+ headers: authHeaders(),
160
+ });
161
+ if (!res.ok)
162
+ throw new Error(`export failed: HTTP ${res.status}`);
163
+ const bundle = (await res.json());
164
+ writeFileSync(file, JSON.stringify(bundle, null, 2) + "\n", "utf8");
165
+ console.log(`[memwarden] exported brain to ${file} (${bundle.sessions?.length ?? 0} sessions).`);
166
+ }
167
+ async function doctor(rest) {
168
+ const path = rest.find((a) => !a.startsWith("--")) ?? ".";
169
+ const root = path === "." ? process.cwd() : path;
170
+ // Scope the audit to THIS project by default so `doctor .` in repo A never
171
+ // pools stale/conflict findings against unrelated repos. project is derived
172
+ // the same way the capture path does it: the project IS the cwd/root (see
173
+ // cli/hook.ts, which sends `project: cwd`). Pass --all-projects for a
174
+ // whole-brain audit across every project.
175
+ const allProjects = rest.includes("--all-projects");
176
+ const body = { root };
177
+ if (!allProjects)
178
+ body.project = root;
179
+ const res = await fetch(`${DAEMON_URL}/memwarden/doctor`, {
180
+ method: "POST",
181
+ headers: authHeaders(),
182
+ body: JSON.stringify(body),
183
+ });
184
+ if (!res.ok)
185
+ throw new Error(`doctor failed: HTTP ${res.status}`);
186
+ const r = (await res.json());
187
+ console.log(`\nmemwarden doctor — ${root}${allProjects ? " (all projects)" : " (this project)"}\n`);
188
+ console.log(` VERIFIED: ${r.verified} memories (code-backed, current)`);
189
+ console.log(` SOURCED: ${r.sourcedUnverified} memories (sourced, not content-verified)`);
190
+ console.log(` STALE: ${r.stale.length} memories reference files that changed/deleted`);
191
+ console.log(` UNSOURCED: ${r.unsourced.length} memories have no evidence`);
192
+ console.log(` CONFLICTS: ${r.conflicts.length} possible contradictions\n`);
193
+ for (const s of r.stale.slice(0, 5))
194
+ console.log(` [stale] ${s.title} — ${s.reason}`);
195
+ for (const u of r.unsourced.slice(0, 5))
196
+ console.log(` [unsourced] ${u.title} — ${u.reason}`);
197
+ for (const c of r.conflicts.slice(0, 5)) {
198
+ console.log(` [conflict] ${c.newerTitle} may contradict ${c.olderTitle} — ${c.reason}`);
199
+ }
200
+ if (r.footprint) {
201
+ const mb = r.footprint.bytesOnDisk / (1024 * 1024);
202
+ const size = mb >= 1 ? `${mb.toFixed(1)} MB` : `${Math.ceil(r.footprint.bytesOnDisk / 1024)} KB`;
203
+ console.log(`\n FOOTPRINT: ${size} on disk at ${r.footprint.dataDir} · ${r.footprint.oplogEntries} oplog entries`);
204
+ }
205
+ console.log(`\n ${r.total} memories audited.\n`);
206
+ }
207
+ // memwarden forget <obsId> — delete one memory and print the tamper-evident
208
+ // receipt: the oplog entries proving the write and the delete, chain status,
209
+ // and a hash over the receipt itself. An id that doesn't exist reports
210
+ // exactly that — never a fake success.
211
+ async function forget(rest) {
212
+ const obsId = rest.find((a) => !a.startsWith("--"));
213
+ if (!obsId)
214
+ throw new Error("usage: memwarden forget <observationId> [--json]");
215
+ const res = await fetch(`${DAEMON_URL}/memwarden/forget`, {
216
+ method: "POST",
217
+ headers: authHeaders(),
218
+ body: JSON.stringify({ observation_id: obsId }),
219
+ });
220
+ if (!res.ok)
221
+ throw new Error(`forget failed: HTTP ${res.status}`);
222
+ const r = (await res.json());
223
+ if (rest.includes("--json")) {
224
+ console.log(JSON.stringify(r, null, 2));
225
+ return;
226
+ }
227
+ if (!r.deleted) {
228
+ console.log(`\n Not deleted: ${r.reason ?? "unknown reason"}\n`);
229
+ process.exitCode = 1;
230
+ return;
231
+ }
232
+ const rec = r.receipt;
233
+ console.log(`\n Deleted "${rec.title}" (${rec.obsId})\n`);
234
+ console.log(` delete receipt`);
235
+ if (rec.createEntry) {
236
+ console.log(` written oplog #${rec.createEntry.id} at ${rec.createEntry.ts}`);
237
+ console.log(` hash ${rec.createEntry.hash.slice(0, 16)}…`);
238
+ }
239
+ if (rec.deleteEntry) {
240
+ console.log(` deleted oplog #${rec.deleteEntry.id} at ${rec.deleteEntry.ts}`);
241
+ console.log(` hash ${rec.deleteEntry.hash.slice(0, 16)}…`);
242
+ }
243
+ console.log(` chain ${rec.chainIntact ? "intact (verified end to end)" : "BROKEN — run memwarden doctor"}`);
244
+ console.log(` receipt ${rec.receiptHash}`);
245
+ console.log(`\n The deletion is recorded in the hash-chained oplog: removing or editing\n` +
246
+ ` the record would break the chain. Keep --json output as a shareable proof\n` +
247
+ ` the deletion happened (it contains hashes, never the deleted content).\n`);
248
+ }
249
+ // memwarden exclude/include — per-project firewall holes. An excluded
250
+ // project is invisible to every automatic surface: no capture, no
251
+ // injection, hooks and proxy alike. Takes effect immediately (the list is
252
+ // re-read per request), no daemon restart.
253
+ function excludedListPath() {
254
+ const dataDir = process.env.MEMWARDEN_DATA_DIR ?? join(homedir(), ".memwarden");
255
+ return join(dataDir, "excluded");
256
+ }
257
+ function readExcluded() {
258
+ const path = excludedListPath();
259
+ if (!existsSync(path))
260
+ return [];
261
+ return readFileSync(path, "utf8")
262
+ .split("\n")
263
+ .map((l) => l.trim())
264
+ .filter((l) => l && !l.startsWith("#"));
265
+ }
266
+ function writeExcluded(lines) {
267
+ const path = excludedListPath();
268
+ mkdirSync(dirname(path), { recursive: true });
269
+ writeFileSync(path, lines.join("\n") + (lines.length ? "\n" : ""), "utf8");
270
+ }
271
+ function exclude(rest) {
272
+ if (rest.includes("--list")) {
273
+ const lines = readExcluded();
274
+ if (lines.length === 0) {
275
+ console.log("[memwarden] no excluded projects.");
276
+ return;
277
+ }
278
+ console.log("[memwarden] excluded projects (no capture, no injection):");
279
+ for (const l of lines)
280
+ console.log(` ${l}`);
281
+ return;
282
+ }
283
+ const target = resolve(rest.find((a) => !a.startsWith("--")) ?? process.cwd());
284
+ const lines = readExcluded();
285
+ if (lines.includes(target)) {
286
+ console.log(`[memwarden] already excluded: ${target}`);
287
+ return;
288
+ }
289
+ writeExcluded([...lines, target]);
290
+ console.log(`[memwarden] excluded ${target} — memwarden will not capture from or inject into this project. Undo: memwarden include ${target}`);
291
+ }
292
+ function include(rest) {
293
+ const target = resolve(rest.find((a) => !a.startsWith("--")) ?? process.cwd());
294
+ const lines = readExcluded();
295
+ const next = lines.filter((l) => resolve(l) !== target);
296
+ if (next.length === lines.length) {
297
+ console.log(`[memwarden] not excluded: ${target}`);
298
+ return;
299
+ }
300
+ writeExcluded(next);
301
+ console.log(`[memwarden] re-included ${target}.`);
302
+ }
303
+ // memwarden audit <store> — run the memory doctor against a FOREIGN store
304
+ // (claude-mem/any SQLite, CLAUDE.md piles, Mem0-style JSON exports) without a
305
+ // daemon or any setup. `npx memwarden audit ~/.claude-mem/claude-mem.db` is
306
+ // the whole onboarding.
307
+ async function audit(rest) {
308
+ const { auditStore } = await import("../functions/audit.js");
309
+ const positional = rest.filter((a, i) => !a.startsWith("--") && rest[i - 1] !== "--root");
310
+ const storePath = positional[0];
311
+ if (!storePath) {
312
+ throw new Error("usage: memwarden audit <store.db|store.json|CLAUDE.md|dir> [--root repo] [--json]");
313
+ }
314
+ const rootIdx = rest.indexOf("--root");
315
+ const root = rootIdx >= 0 && rest[rootIdx + 1] ? rest[rootIdx + 1] : process.cwd();
316
+ const report = await auditStore(storePath, root);
317
+ if (rest.includes("--json")) {
318
+ console.log(JSON.stringify(report, null, 2));
319
+ return;
320
+ }
321
+ const tty = process.stdout.isTTY === true;
322
+ const paint = (code, s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
323
+ const red = (s) => paint("31", s);
324
+ const yellow = (s) => paint("33", s);
325
+ const green = (s) => paint("32", s);
326
+ const gray = (s) => paint("90", s);
327
+ const bold = (s) => paint("1", s);
328
+ const pct = (n) => report.total > 0 ? `${Math.round((n / report.total) * 100)}%` : "0%";
329
+ const row = (label, n, note) => ` ${label.padEnd(12)} ${String(n).padStart(5)} ${pct(n).padStart(4)} ${note}`;
330
+ console.log(`\n${bold("memwarden audit")} — ${report.store} (${report.kind})`);
331
+ console.log(`${" ".repeat(18)}vs ${report.root}\n`);
332
+ console.log(` ${report.total} memories scanned · ${report.anchored} anchored to ${report.uniqueFiles} file(s)\n`);
333
+ console.log(row(red("MISSING"), report.missing.length, "reference files that no longer exist"));
334
+ console.log(row(yellow("DRIFTED"), report.drifted.length, report.driftCheckable
335
+ ? "files changed after the memory was recorded"
336
+ : gray("not checkable — this store records no timestamps")));
337
+ console.log(row(green("PRESENT"), report.present, "files exist — existence is this store's best case"));
338
+ console.log(row(gray("UNANCHORED"), report.unanchored, "no file evidence at all"));
339
+ console.log("");
340
+ for (const f of report.missing.slice(0, 5)) {
341
+ console.log(` ${red("[missing]")} ${f.title} — ${f.detail}`);
342
+ }
343
+ for (const f of report.drifted.slice(0, 5)) {
344
+ console.log(` ${yellow("[drifted]")} ${f.title} — ${f.detail}`);
345
+ }
346
+ if (report.missing.length > 0 || report.drifted.length > 0)
347
+ console.log("");
348
+ const badCount = report.missing.length + report.drifted.length;
349
+ if (report.anchored > 0) {
350
+ const badPct = Math.round((badCount / report.anchored) * 100);
351
+ console.log(` ${bold(`${badCount} of ${report.anchored}`)} anchored memories (${badPct}%) are red or yellow: under\n` +
352
+ ` memwarden's Verified Recall they would be classified STALE and never\n` +
353
+ ` injected. PRESENT still isn't "verified" — that requires capture-time\n` +
354
+ ` content hashes, which this store does not record.\n`);
355
+ }
356
+ else {
357
+ console.log(` Nothing in this store references a file at all — none of it can be\n` +
358
+ ` verified against the code. memwarden records file provenance with\n` +
359
+ ` content hashes at capture, so recall can prove what is still true.\n`);
360
+ }
361
+ }
362
+ // memwarden dejafix lookup|record — the cross-agent "don't re-solve a fixed
363
+ // error" surface, scriptable from the shell. Error text comes from stdin (so you
364
+ // can pipe a failing command's output straight in) or trailing args.
365
+ async function dejafix(rest) {
366
+ const sub = rest[0];
367
+ const flags = rest.slice(1);
368
+ const flagVal = (name) => {
369
+ const i = flags.indexOf(name);
370
+ return i >= 0 ? flags[i + 1] : undefined;
371
+ };
372
+ const cwd = flagVal("--cwd") ?? process.cwd();
373
+ const positional = flags.filter((a, i) => !a.startsWith("--") && !(i > 0 && flags[i - 1]?.startsWith("--")));
374
+ const piped = process.stdin.isTTY ? "" : await readStdin();
375
+ const errorText = (piped.trim() || positional.join(" ")).trim();
376
+ if (sub === "lookup") {
377
+ if (!errorText)
378
+ throw new Error("usage: memwarden dejafix lookup [--cwd dir] < error.txt");
379
+ const res = await fetch(`${DAEMON_URL}/memwarden/dejafix/lookup`, {
380
+ method: "POST",
381
+ headers: authHeaders(),
382
+ body: JSON.stringify({ error_text: errorText, cwd }),
383
+ });
384
+ if (!res.ok)
385
+ throw new Error(`dejafix lookup failed: HTTP ${res.status}`);
386
+ const r = (await res.json());
387
+ if (!r.signature) {
388
+ console.log("\n No error signature found in the input.\n");
389
+ return;
390
+ }
391
+ console.log(`\nDéjà Fix — signature: ${r.signature}\n`);
392
+ if (r.fixes.length === 0) {
393
+ console.log(" No verified prior fix for this error in this project.\n");
394
+ return;
395
+ }
396
+ for (const f of r.fixes) {
397
+ const who = f.tool ? `${f.tool}, ` : "";
398
+ console.log(` [${f.badge}] (${who}${f.timestamp.slice(0, 10)})`);
399
+ if (f.rootCause)
400
+ console.log(` root cause: ${f.rootCause}`);
401
+ console.log(` fix: ${f.fix}\n`);
402
+ }
403
+ return;
404
+ }
405
+ if (sub === "record") {
406
+ const fix = flagVal("--fix");
407
+ if (!fix)
408
+ throw new Error('usage: memwarden dejafix record --fix "<what fixed it>" [--root-cause s] [--file f]... [--cwd dir] < error.txt');
409
+ if (!errorText)
410
+ throw new Error("dejafix record needs the error text (pipe it in or pass it as args)");
411
+ const files = [];
412
+ for (let i = 0; i < flags.length; i++)
413
+ if (flags[i] === "--file" && flags[i + 1])
414
+ files.push(flags[i + 1]);
415
+ const body = { error_text: errorText, fix, cwd };
416
+ const rootCause = flagVal("--root-cause");
417
+ if (rootCause)
418
+ body["root_cause"] = rootCause;
419
+ if (files.length > 0)
420
+ body["files"] = files;
421
+ const res = await fetch(`${DAEMON_URL}/memwarden/dejafix/record`, {
422
+ method: "POST",
423
+ headers: authHeaders(),
424
+ body: JSON.stringify(body),
425
+ });
426
+ if (!res.ok)
427
+ throw new Error(`dejafix record failed: HTTP ${res.status}`);
428
+ const r = (await res.json());
429
+ console.log(r.recorded
430
+ ? `\n Recorded fix for signature: ${r.signature}\n`
431
+ : "\n Nothing recorded (no error signature could be derived).\n");
432
+ return;
433
+ }
434
+ throw new Error("usage: memwarden dejafix <lookup|record> …");
435
+ }
436
+ async function importBrain(file) {
437
+ if (!file)
438
+ throw new Error("usage: memwarden import <file>");
439
+ const bundle = JSON.parse(readFileSync(file, "utf8"));
440
+ const res = await fetch(`${DAEMON_URL}/memwarden/import`, {
441
+ method: "POST",
442
+ headers: authHeaders(),
443
+ body: JSON.stringify(bundle),
444
+ });
445
+ const out = (await res.json());
446
+ if (!res.ok)
447
+ throw new Error(`import failed: ${JSON.stringify(out)}`);
448
+ console.log(`[memwarden] imported brain from ${file}:`, out.imported);
449
+ }
450
+ // --- `memwarden up` -----------------------------------------------
451
+ async function up(rest) {
452
+ const { url, secret: flagSecret } = parseFlags(rest);
453
+ const daemonUrl = url ?? DAEMON_URL;
454
+ const home = homedir();
455
+ const all = rest.includes("--all");
456
+ const dataDir = process.env.MEMWARDEN_DATA_DIR ?? join(home, ".memwarden");
457
+ console.log(`\nmemwarden up\n`);
458
+ // 0. secret — generate one on first run (defense-in-depth alongside the
459
+ // Host-header firewall). resolveSecret persists it under the data dir,
460
+ // reuses an existing one across runs, and puts it in process.env so the
461
+ // detached daemon spawn inherits it and the service install can bake it in.
462
+ const { secret, generated } = resolveSecret(flagSecret, dataDir);
463
+ console.log(` secret ✓ ${generated ? "generated" : "loaded"} (${secretFilePath(dataDir)})`);
464
+ // 1. daemon — install a self-healing OS service (starts at login, restarts
465
+ // on crash). Fall back to a detached spawn if there's no service manager.
466
+ // Either way the wiring continues; the configs point at this daemon.
467
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
468
+ const svc = installService(dataDir, secret);
469
+ if (svc.ok) {
470
+ let alive = await daemonAlive(daemonUrl);
471
+ for (let i = 0; i < 40 && !alive; i++) {
472
+ await sleep(250);
473
+ alive = await daemonAlive(daemonUrl);
474
+ }
475
+ console.log(` daemon ${alive ? "✓" : "⚠"} ${daemonUrl} ${svc.kind}: ${svc.message}` +
476
+ (alive ? ` brain: ${dataDir}` : " (starting…)"));
477
+ }
478
+ else {
479
+ const state = await ensureDaemon(daemonUrl, dataDir);
480
+ if (state === "failed") {
481
+ console.log(` daemon ⚠ could not start at ${daemonUrl} (port in use?). ` +
482
+ `Wiring tools anyway; start it with: node ${DAEMON_ENTRY}`);
483
+ }
484
+ else {
485
+ const note = svc.kind === "unsupported"
486
+ ? process.platform === "win32"
487
+ ? "Windows service supervision is not supported yet — the daemon runs " +
488
+ "for this login session and self-heals on next use; rerun `memwarden up` after a reboot"
489
+ : "background"
490
+ : `service skipped: ${svc.message}`;
491
+ console.log(` daemon ✓ ${daemonUrl} brain: ${dataDir} (${note})`);
492
+ }
493
+ }
494
+ // 2. wire each detected tool's MCP config (point it at this daemon)
495
+ const launch = {
496
+ command: process.execPath,
497
+ args: [MCP_BIN],
498
+ env: {
499
+ MEMWARDEN_URL: daemonUrl,
500
+ ...(secret ? { MEMWARDEN_SECRET: secret } : {}),
501
+ },
502
+ };
503
+ const targets = all ? TOOLS : TOOLS.filter((t) => t.detect(home));
504
+ console.log("");
505
+ if (targets.length === 0) {
506
+ console.log(" no supported tools detected. Re-run with --all to wire them all anyway.");
507
+ }
508
+ else {
509
+ console.log(` wiring ${targets.length} tool(s):`);
510
+ for (const t of targets) {
511
+ const r = writeTool(t, home, launch);
512
+ if (r.status === "skipped") {
513
+ console.log(` - ${t.label.padEnd(13)} skipped (${r.reason})`);
514
+ continue;
515
+ }
516
+ // Claude Code also gets real hooks: SessionStart auto-inject +
517
+ // PostToolUse auto-capture (true automatic memory, no agent needed).
518
+ let how = "agent recalls/saves via MCP + AGENTS.md";
519
+ if (t.id === "claude-code") {
520
+ writeClaudeHooks(join(home, ".claude", "settings.json"), HOOK_BASE);
521
+ how = "hooks — auto inject + auto capture";
522
+ }
523
+ console.log(` ✓ ${t.label.padEnd(13)} ${r.path}`);
524
+ console.log(` ${" ".repeat(13)} ${how}`);
525
+ }
526
+ }
527
+ // 3. AGENTS.md in this project: tells the hook-less tools to use memory
528
+ // at task boundaries (the cross-tool auto-recall lever).
529
+ const agents = writeAgentsMd(process.cwd());
530
+ console.log("");
531
+ console.log(` AGENTS.md ✓ ${agents.created ? "wrote" : "updated"} ${agents.path}`);
532
+ console.log(`\n Done. Restart each tool once so it loads the memwarden MCP server.\n` +
533
+ ` Recall in any tool: type /recall, or just ask. Claude Code captures\n` +
534
+ ` and recalls automatically; for global auto-capture on other tools,\n` +
535
+ ` point them at the memory proxy (see README).\n`);
536
+ }
537
+ function down() {
538
+ const r = uninstallService();
539
+ if (r.ok) {
540
+ console.log(`[memwarden] stopped and removed the ${r.kind} service.`);
541
+ }
542
+ else {
543
+ console.log(`[memwarden] no service to remove (${r.message}). ` +
544
+ `A daemon started in the background will exit when you log out.`);
545
+ }
546
+ }
547
+ async function main() {
548
+ const [cmd, ...rest] = process.argv.slice(2);
549
+ switch (cmd) {
550
+ case "up":
551
+ return up(rest);
552
+ case "down":
553
+ return down();
554
+ case "connect":
555
+ return connect(rest);
556
+ case "hook":
557
+ return hook(rest);
558
+ case "doctor":
559
+ return doctor(rest);
560
+ case "audit":
561
+ return audit(rest);
562
+ case "exclude":
563
+ return exclude(rest);
564
+ case "include":
565
+ return include(rest);
566
+ case "forget":
567
+ return forget(rest);
568
+ case "dejafix":
569
+ return dejafix(rest);
570
+ case "export":
571
+ return exportBrain(rest[0]);
572
+ case "import":
573
+ return importBrain(rest[0]);
574
+ default:
575
+ console.log("usage:\n" +
576
+ " memwarden up [--all] [--url URL] [--secret S] # start daemon + wire every installed tool\n" +
577
+ " memwarden down # stop + remove the daemon service\n" +
578
+ " memwarden connect [claude-code|cursor|cline|windsurf] [--with-hooks] [--url URL] [--secret S]\n" +
579
+ " memwarden doctor [path] [--all-projects] # audit this project (or the whole brain)\n" +
580
+ " memwarden audit <store> [--root repo] [--json] # audit a FOREIGN store (claude-mem db, CLAUDE.md, Mem0 json)\n" +
581
+ " memwarden exclude [path] | include [path] | exclude --list # per-project: no capture, no injection\n" +
582
+ " memwarden forget <observationId> [--json] # delete one memory, get a tamper-evident receipt\n" +
583
+ " memwarden dejafix lookup [--cwd dir] < err.txt # find a verified prior fix for an error\n" +
584
+ ' memwarden dejafix record --fix "…" [--file f] [--root-cause s] < err.txt\n' +
585
+ " memwarden export <file>\n" +
586
+ " memwarden import <file>");
587
+ process.exit(cmd ? 1 : 0);
588
+ }
589
+ }
590
+ main().catch((err) => {
591
+ console.error(`[memwarden] ${err instanceof Error ? err.message : err}`);
592
+ process.exit(1);
593
+ });