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,120 @@
1
+ //
2
+ // CJK-aware segmentation for the BM25 tokenizer. CJK text has no spaces, so a
3
+ // run of Han/Kana/Hangul is segmented before tokenizing. Chinese and Japanese
4
+ // use optional native segmenters (@node-rs/jieba, tiny-segmenter) loaded
5
+ // lazily via createRequire; when they are absent the run is kept whole (search
6
+ // still works, just coarser) so there are no required new dependencies. Korean
7
+ // is split into Hangul blocks with a plain regex (no dependency).
8
+ import { createRequire } from "node:module";
9
+ const req = createRequire(import.meta.url);
10
+ const ANY_CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
11
+ const HAN = /\p{Script=Han}/u;
12
+ const KANA = /[\p{Script=Hiragana}\p{Script=Katakana}]/u;
13
+ const HANGUL = /\p{Script=Hangul}/u;
14
+ const CJK_RUN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu;
15
+ const HANGUL_BLOCK = /[가-힯]+/g;
16
+ export function hasCjk(text) {
17
+ return ANY_CJK.test(text);
18
+ }
19
+ export function detectScript(text) {
20
+ if (HAN.test(text))
21
+ return "han";
22
+ if (KANA.test(text))
23
+ return "kana";
24
+ if (HANGUL.test(text))
25
+ return "hangul";
26
+ return "other";
27
+ }
28
+ const hinted = new Set();
29
+ function hintOnce(key, message) {
30
+ if (hinted.has(key))
31
+ return;
32
+ hinted.add(key);
33
+ process?.stderr?.write?.(`memwarden: ${message}\n`);
34
+ }
35
+ const loaded = new Map();
36
+ function loadJieba() {
37
+ if (loaded.has("jieba"))
38
+ return loaded.get("jieba") ?? null;
39
+ let cut = null;
40
+ try {
41
+ const mod = req("@node-rs/jieba");
42
+ let inst;
43
+ try {
44
+ inst = mod.Jieba.withDict(req("@node-rs/jieba/dict").dict);
45
+ }
46
+ catch {
47
+ inst = new mod.Jieba();
48
+ }
49
+ cut = (t) => inst.cut(t, true);
50
+ }
51
+ catch {
52
+ hintOnce("jieba", "install @node-rs/jieba to improve Chinese search; using whole-string tokens for now");
53
+ }
54
+ loaded.set("jieba", cut);
55
+ return cut;
56
+ }
57
+ function loadJa() {
58
+ if (loaded.has("ja"))
59
+ return loaded.get("ja") ?? null;
60
+ let cut = null;
61
+ try {
62
+ const Ctor = req("tiny-segmenter");
63
+ const inst = new Ctor();
64
+ cut = (t) => inst.segment(t);
65
+ }
66
+ catch {
67
+ hintOnce("tiny-segmenter", "install tiny-segmenter to improve Japanese search; using whole-string tokens for now");
68
+ }
69
+ loaded.set("ja", cut);
70
+ return cut;
71
+ }
72
+ function nonEmpty(tokens) {
73
+ const out = [];
74
+ for (const t of tokens) {
75
+ const v = t.trim();
76
+ if (v)
77
+ out.push(v);
78
+ }
79
+ return out;
80
+ }
81
+ function segmentRun(run) {
82
+ if (HANGUL.test(run)) {
83
+ return [...run.matchAll(HANGUL_BLOCK)].map((m) => m[0]);
84
+ }
85
+ const cut = KANA.test(run) ? loadJa() : loadJieba();
86
+ if (!cut)
87
+ return [run];
88
+ try {
89
+ return nonEmpty(cut(run));
90
+ }
91
+ catch {
92
+ return [run];
93
+ }
94
+ }
95
+ export function segmentCjk(text) {
96
+ if (!hasCjk(text))
97
+ return [text];
98
+ const out = [];
99
+ let at = 0;
100
+ for (const m of text.matchAll(CJK_RUN)) {
101
+ const start = m.index ?? 0;
102
+ if (start > at) {
103
+ const gap = text.slice(at, start).trim();
104
+ if (gap)
105
+ out.push(gap);
106
+ }
107
+ out.push(...segmentRun(m[0]));
108
+ at = start + m[0].length;
109
+ }
110
+ if (at < text.length) {
111
+ const tail = text.slice(at).trim();
112
+ if (tail)
113
+ out.push(tail);
114
+ }
115
+ return out;
116
+ }
117
+ export function __resetCjkSegmenterStateForTests() {
118
+ hinted.clear();
119
+ loaded.clear();
120
+ }
@@ -0,0 +1,2 @@
1
+ import type { RawObservation, CompressedObservation } from "./types.js";
2
+ export declare function buildSyntheticCompression(raw: RawObservation): CompressedObservation;
@@ -0,0 +1,104 @@
1
+ //
2
+ // Zero-LLM compression: turn a RawObservation into a CompressedObservation
3
+ // with heuristics only (no model call, no token spend). This is the default
4
+ // observe path until an LLM provider is wired in. It classifies the tool,
5
+ // pulls out any file paths, and builds a short narrative.
6
+ // tool-name keyword -> observation type, in priority order
7
+ const TOOL_KINDS = [
8
+ ["web_fetch", ["fetch", "http", "web"]],
9
+ ["search", ["grep", "search", "glob", "find"]],
10
+ ["command_run", ["bash", "shell", "exec", "run"]],
11
+ ["file_edit", ["edit", "update", "patch", "replace"]],
12
+ ["file_write", ["write", "create"]],
13
+ ["file_read", ["read", "view"]],
14
+ ["subagent", ["task", "agent"]],
15
+ ];
16
+ const FILE_KEYS = ["file_path", "filepath", "path", "filePath", "file", "pattern"];
17
+ // split camelCase / kebab / spaces into a normalized underscore form
18
+ function normalizeToolName(name) {
19
+ return name
20
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
21
+ .replace(/[-\s]+/g, "_")
22
+ .toLowerCase();
23
+ }
24
+ function mentions(normalized, word) {
25
+ return (new RegExp(`(^|_)${word}(_|$)`).test(normalized) ||
26
+ normalized === word ||
27
+ normalized.startsWith(word) ||
28
+ normalized.endsWith(word));
29
+ }
30
+ function classify(toolName, hookType) {
31
+ if (hookType === "post_tool_failure")
32
+ return "error";
33
+ if (hookType === "prompt_submit")
34
+ return "conversation";
35
+ if (hookType === "subagent_stop" || hookType === "task_completed")
36
+ return "subagent";
37
+ if (hookType === "notification")
38
+ return "notification";
39
+ if (!toolName)
40
+ return "other";
41
+ const n = normalizeToolName(toolName);
42
+ for (const [kind, words] of TOOL_KINDS) {
43
+ if (words.some((w) => mentions(n, w)))
44
+ return kind;
45
+ }
46
+ return "other";
47
+ }
48
+ function filePaths(input) {
49
+ if (!input || typeof input !== "object")
50
+ return [];
51
+ const o = input;
52
+ const found = new Set();
53
+ for (const key of FILE_KEYS) {
54
+ const v = o[key];
55
+ if (typeof v === "string" && v.length > 0 && v.length < 512)
56
+ found.add(v);
57
+ }
58
+ return [...found];
59
+ }
60
+ function asText(v) {
61
+ if (v == null)
62
+ return "";
63
+ if (typeof v === "string")
64
+ return v;
65
+ try {
66
+ return JSON.stringify(v);
67
+ }
68
+ catch {
69
+ return String(v);
70
+ }
71
+ }
72
+ function clip(s, max) {
73
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
74
+ }
75
+ export function buildSyntheticCompression(raw) {
76
+ const toolName = raw.toolName ?? raw.hookType;
77
+ const inputText = asText(raw.toolInput);
78
+ const outputText = asText(raw.toolOutput);
79
+ const narrative = [raw.userPrompt ?? "", inputText, outputText]
80
+ .filter((s) => s.length > 0)
81
+ .join(" | ");
82
+ const result = {
83
+ id: raw.id,
84
+ sessionId: raw.sessionId,
85
+ timestamp: raw.timestamp,
86
+ type: classify(toolName, raw.hookType),
87
+ title: clip(toolName || "observation", 80),
88
+ facts: [],
89
+ narrative: clip(narrative, 400),
90
+ concepts: [],
91
+ files: filePaths(raw.toolInput),
92
+ importance: 5,
93
+ confidence: 0.3,
94
+ };
95
+ if (inputText)
96
+ result.subtitle = clip(inputText, 120);
97
+ if (raw.modality)
98
+ result.modality = raw.modality;
99
+ if (raw.imageData)
100
+ result.imageData = raw.imageData;
101
+ if (raw.agentId)
102
+ result.agentId = raw.agentId;
103
+ return result;
104
+ }
@@ -0,0 +1,68 @@
1
+ /** Default per-request context token budget. */
2
+ export declare function getTokenBudget(): number;
3
+ /** Max observations retained per session before mem::observe refuses more. */
4
+ export declare function getMaxObservationsPerSession(): number;
5
+ /**
6
+ * The session's agentId source when no session row exists yet. Trimmed and
7
+ * capped at 128 chars.
8
+ */
9
+ export declare function getAgentId(): string | undefined;
10
+ /**
11
+ * Per-observation LLM compression is opt-in. When false (the default)
12
+ * mem::observe takes the zero-LLM synthetic-compression path. No LLM
13
+ * provider is wired in the core, so this is effectively always false here,
14
+ * but the flag is reserved for when the model layer lands.
15
+ */
16
+ export declare function isAutoCompressEnabled(): boolean;
17
+ /** Memory slots are an optional context-injection feature, off by default. */
18
+ export declare function isSlotsEnabled(): boolean;
19
+ /** Auto-injection (SessionStart hook, Déjà Fix hook, proxy). Default on. */
20
+ export declare function isInjectEnabled(): boolean;
21
+ /** Auto-capture (PostToolUse hook, proxy tee). Default on. */
22
+ export declare function isCaptureEnabled(): boolean;
23
+ /** Where the brain lives. */
24
+ export declare function getDataDir(): string;
25
+ /**
26
+ * Per-project exclusion: `<dataDir>/excluded` holds one absolute path per
27
+ * line (written by `memwarden exclude`). A cwd inside any excluded path is
28
+ * excluded — capture AND injection, every automatic surface, so an excluded
29
+ * project never reaches the brain and the brain never reaches it. Read on
30
+ * every call (the file is tiny and the daemon is long-lived; a stale cache
31
+ * here would mean "exclude" takes effect only on restart — the classic
32
+ * excluded-but-not-really bug).
33
+ */
34
+ export declare function isProjectExcluded(cwd: string | undefined): boolean;
35
+ /**
36
+ * The shared secret used by the api-auth middleware. When unset (no env var and
37
+ * no persisted secret file) the API is open (absent secret = continue). The
38
+ * daemon and every local client resolve it identically through this function.
39
+ */
40
+ export declare function getSecret(): string | undefined;
41
+ /**
42
+ * TurboQuant vector quantization (arXiv:2504.19874): the vector index
43
+ * stores 2/4-bit codes instead of full Float32 embeddings (~8-16x smaller).
44
+ * This is memwarden's distinguishing storage layer, so it is ON BY DEFAULT
45
+ * whenever an embedding provider is active — there is no reason to hold
46
+ * full-precision vectors once semantic memory is on. Set
47
+ * MEMWARDEN_QUANT_VECTOR=false to force the full-precision baseline, or
48
+ * =true to force it on even without a provider (e.g. tests).
49
+ */
50
+ export declare function isQuantizedVectorEnabled(): boolean;
51
+ /** Bits per dimension for the quantized index. 4 (default) or 2. */
52
+ export declare function getQuantBits(): 2 | 4;
53
+ /**
54
+ * Rescore depth: how many asymmetric-pass candidates get re-ranked with
55
+ * exact cosine. 0 (default) disables rescore and drops full vectors from
56
+ * memory entirely — the max-compression configuration.
57
+ */
58
+ export declare function getQuantRescoreDepth(): number;
59
+ /** Rotation seed; same seed reproduces the identical rotation everywhere. */
60
+ export declare function getQuantSeed(): string;
61
+ /** Upstream OpenAI-compatible base URL, e.g. https://api.openai.com/v1. */
62
+ export declare function getUpstreamUrl(): string | undefined;
63
+ /** API key forwarded to the upstream as `Authorization: Bearer`. */
64
+ export declare function getUpstreamKey(): string | undefined;
65
+ /** Port the memory proxy listens on. Defaults to 3113. */
66
+ export declare function getProxyPort(): number;
67
+ /** The proxy runs only once an upstream is configured to forward to. */
68
+ export declare function isProxyEnabled(): boolean;
@@ -0,0 +1,231 @@
1
+ //
2
+ // Config shim. Reads process.env directly; only the handful of
3
+ // flags the core functions consult are modelled here.
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { canonicalizePath } from "./paths.js";
8
+ function env(name) {
9
+ const v = process.env[name];
10
+ return v !== undefined ? v : undefined;
11
+ }
12
+ function flag(name) {
13
+ return env(name) === "true";
14
+ }
15
+ /** Default per-request context token budget. */
16
+ export function getTokenBudget() {
17
+ const raw = env("MEMWARDEN_TOKEN_BUDGET");
18
+ if (!raw)
19
+ return 2000;
20
+ const n = parseInt(raw, 10);
21
+ return Number.isFinite(n) && n > 0 ? n : 2000;
22
+ }
23
+ /** Max observations retained per session before mem::observe refuses more. */
24
+ export function getMaxObservationsPerSession() {
25
+ const raw = env("MEMWARDEN_MAX_OBSERVATIONS_PER_SESSION");
26
+ if (!raw)
27
+ return 10000;
28
+ const n = parseInt(raw, 10);
29
+ return Number.isFinite(n) && n > 0 ? n : 10000;
30
+ }
31
+ /**
32
+ * The session's agentId source when no session row exists yet. Trimmed and
33
+ * capped at 128 chars.
34
+ */
35
+ export function getAgentId() {
36
+ const raw = env("MEMWARDEN_AGENT_ID");
37
+ if (!raw)
38
+ return undefined;
39
+ const agentId = raw.trim().slice(0, 128);
40
+ return agentId || undefined;
41
+ }
42
+ /**
43
+ * Per-observation LLM compression is opt-in. When false (the default)
44
+ * mem::observe takes the zero-LLM synthetic-compression path. No LLM
45
+ * provider is wired in the core, so this is effectively always false here,
46
+ * but the flag is reserved for when the model layer lands.
47
+ */
48
+ export function isAutoCompressEnabled() {
49
+ return flag("MEMWARDEN_AUTO_COMPRESS");
50
+ }
51
+ /** Memory slots are an optional context-injection feature, off by default. */
52
+ export function isSlotsEnabled() {
53
+ return flag("MEMWARDEN_SLOTS");
54
+ }
55
+ // --- injection / capture switches -----------------------------------
56
+ //
57
+ // Users must be able to turn the automatic paths off — per environment via
58
+ // MEMWARDEN_INJECT / MEMWARDEN_CAPTURE, and per project via the excluded
59
+ // list. The switches gate the AUTOMATIC paths only (hooks, proxy); explicit
60
+ // asks (/recall, the MCP tools, the CLI) always work — turning off
61
+ // auto-inject must not lobotomize deliberate recall.
62
+ function offFlag(name) {
63
+ const v = (env(name) ?? "").trim().toLowerCase();
64
+ return v === "off" || v === "false" || v === "0";
65
+ }
66
+ /** Auto-injection (SessionStart hook, Déjà Fix hook, proxy). Default on. */
67
+ export function isInjectEnabled() {
68
+ return !offFlag("MEMWARDEN_INJECT");
69
+ }
70
+ /** Auto-capture (PostToolUse hook, proxy tee). Default on. */
71
+ export function isCaptureEnabled() {
72
+ return !offFlag("MEMWARDEN_CAPTURE");
73
+ }
74
+ /** Where the brain lives. */
75
+ export function getDataDir() {
76
+ return env("MEMWARDEN_DATA_DIR") ?? join(homedir(), ".memwarden");
77
+ }
78
+ /**
79
+ * Per-project exclusion: `<dataDir>/excluded` holds one absolute path per
80
+ * line (written by `memwarden exclude`). A cwd inside any excluded path is
81
+ * excluded — capture AND injection, every automatic surface, so an excluded
82
+ * project never reaches the brain and the brain never reaches it. Read on
83
+ * every call (the file is tiny and the daemon is long-lived; a stale cache
84
+ * here would mean "exclude" takes effect only on restart — the classic
85
+ * excluded-but-not-really bug).
86
+ */
87
+ export function isProjectExcluded(cwd) {
88
+ if (!cwd)
89
+ return false;
90
+ let lines;
91
+ try {
92
+ const path = join(getDataDir(), "excluded");
93
+ if (!existsSync(path))
94
+ return false;
95
+ lines = readFileSync(path, "utf8").split("\n");
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ // Canonicalize both sides so /tmp vs /private/tmp (and trailing-slash)
101
+ // spellings of the same directory still match — the same rule recall
102
+ // scoping uses (see paths.ts).
103
+ const target = canonicalizePath(cwd);
104
+ if (!target)
105
+ return false;
106
+ for (const line of lines) {
107
+ const raw = line.trim();
108
+ if (!raw || raw.startsWith("#"))
109
+ continue;
110
+ const ex = canonicalizePath(raw);
111
+ if (!ex)
112
+ continue;
113
+ if (target === ex || target.startsWith(ex.endsWith("/") ? ex : ex + "/"))
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ // The CLI (`memwarden up`) persists the generated secret to <dataDir>/secret.
119
+ // The daemon spawned by `up` inherits MEMWARDEN_SECRET via env, but short-lived
120
+ // clients launched by the agent host — the Claude Code hook runs in the user's
121
+ // shell, manually-started MCP servers — won't have that env. Without a fallback
122
+ // they'd call a secured daemon with no Bearer and silently 401 (auto-recall dies
123
+ // with no error). So resolution is: env first, then the persisted secret file.
124
+ // Read the file at most once per process (the secret is stable for its lifetime).
125
+ let cachedFileSecret = null; // null = not yet read
126
+ function persistedSecret() {
127
+ if (cachedFileSecret !== null)
128
+ return cachedFileSecret;
129
+ cachedFileSecret = undefined;
130
+ try {
131
+ const dataDir = env("MEMWARDEN_DATA_DIR") ?? join(homedir(), ".memwarden");
132
+ const path = join(dataDir, "secret");
133
+ if (existsSync(path)) {
134
+ const s = readFileSync(path, "utf8").trim();
135
+ if (s)
136
+ cachedFileSecret = s;
137
+ }
138
+ }
139
+ catch {
140
+ // Best-effort: an unreadable/absent file leaves the API open, exactly as
141
+ // before this fallback existed.
142
+ }
143
+ return cachedFileSecret;
144
+ }
145
+ /**
146
+ * The shared secret used by the api-auth middleware. When unset (no env var and
147
+ * no persisted secret file) the API is open (absent secret = continue). The
148
+ * daemon and every local client resolve it identically through this function.
149
+ */
150
+ export function getSecret() {
151
+ const fromEnv = env("MEMWARDEN_SECRET");
152
+ if (fromEnv && fromEnv.trim())
153
+ return fromEnv.trim();
154
+ return persistedSecret();
155
+ }
156
+ /**
157
+ * TurboQuant vector quantization (arXiv:2504.19874): the vector index
158
+ * stores 2/4-bit codes instead of full Float32 embeddings (~8-16x smaller).
159
+ * This is memwarden's distinguishing storage layer, so it is ON BY DEFAULT
160
+ * whenever an embedding provider is active — there is no reason to hold
161
+ * full-precision vectors once semantic memory is on. Set
162
+ * MEMWARDEN_QUANT_VECTOR=false to force the full-precision baseline, or
163
+ * =true to force it on even without a provider (e.g. tests).
164
+ */
165
+ export function isQuantizedVectorEnabled() {
166
+ const raw = env("MEMWARDEN_QUANT_VECTOR");
167
+ if (raw === "true")
168
+ return true;
169
+ if (raw === "false")
170
+ return false;
171
+ // Unset: follow the embedding provider. Read the env directly to avoid a
172
+ // config<->embedding import cycle.
173
+ const provider = (process.env.MEMWARDEN_EMBEDDING_PROVIDER ?? "local")
174
+ .trim()
175
+ .toLowerCase();
176
+ return provider !== "none";
177
+ }
178
+ /** Bits per dimension for the quantized index. 4 (default) or 2. */
179
+ export function getQuantBits() {
180
+ return env("MEMWARDEN_QUANT_BITS") === "2" ? 2 : 4;
181
+ }
182
+ /**
183
+ * Rescore depth: how many asymmetric-pass candidates get re-ranked with
184
+ * exact cosine. 0 (default) disables rescore and drops full vectors from
185
+ * memory entirely — the max-compression configuration.
186
+ */
187
+ export function getQuantRescoreDepth() {
188
+ const raw = env("MEMWARDEN_QUANT_RESCORE");
189
+ if (!raw)
190
+ return 0;
191
+ const n = parseInt(raw, 10);
192
+ return Number.isFinite(n) && n > 0 ? n : 0;
193
+ }
194
+ /** Rotation seed; same seed reproduces the identical rotation everywhere. */
195
+ export function getQuantSeed() {
196
+ return env("MEMWARDEN_QUANT_SEED") ?? "memwarden-tq-v1";
197
+ }
198
+ // --- memory proxy (the universal cross-tool layer) -----------------
199
+ //
200
+ // The OpenAI-compatible gateway. Any tool that lets you point at a custom
201
+ // base URL — Cursor, Continue, Cline, raw SDK apps — routes its model
202
+ // calls through memwarden, which injects relevant memory and captures the
203
+ // exchange. The model behind it can be local (Ollama :11434/v1, LM Studio
204
+ // :1234/v1) or paid (OpenAI, OpenRouter, Together); the proxy is blind to
205
+ // which, so it is one memory layer for all of them. The proxy is OFF until
206
+ // an upstream is configured (it has nothing to forward to otherwise).
207
+ /** Upstream OpenAI-compatible base URL, e.g. https://api.openai.com/v1. */
208
+ export function getUpstreamUrl() {
209
+ const raw = env("MEMWARDEN_UPSTREAM_URL");
210
+ if (!raw)
211
+ return undefined;
212
+ const trimmed = raw.trim().replace(/\/+$/, "");
213
+ return trimmed || undefined;
214
+ }
215
+ /** API key forwarded to the upstream as `Authorization: Bearer`. */
216
+ export function getUpstreamKey() {
217
+ const raw = env("MEMWARDEN_UPSTREAM_KEY");
218
+ return raw && raw.trim() ? raw.trim() : undefined;
219
+ }
220
+ /** Port the memory proxy listens on. Defaults to 3113. */
221
+ export function getProxyPort() {
222
+ const raw = env("MEMWARDEN_PROXY_PORT");
223
+ if (!raw)
224
+ return 3113;
225
+ const n = parseInt(raw, 10);
226
+ return Number.isFinite(n) && n > 0 ? n : 3113;
227
+ }
228
+ /** The proxy runs only once an upstream is configured to forward to. */
229
+ export function isProxyEnabled() {
230
+ return getUpstreamUrl() !== undefined;
231
+ }
@@ -0,0 +1,19 @@
1
+ import type { CompressedObservation } from "./types.js";
2
+ export interface MemoryConflict {
3
+ olderId: string;
4
+ olderTitle: string;
5
+ newerId: string;
6
+ newerTitle: string;
7
+ subject: string;
8
+ olderClaim: string;
9
+ newerClaim: string;
10
+ reason: string;
11
+ }
12
+ /**
13
+ * Advisory contradiction report for mem::doctor. Deliberately conservative:
14
+ * simple subject/relation/value claims, clause-local negation, and a high bar
15
+ * for value conflicts so reworded facts, abbreviations, and different
16
+ * attributes of one subject don't fire. NEVER used to drop memory from recall
17
+ * — recall only firewalls STALE memory.
18
+ */
19
+ export declare function detectConflicts(observations: CompressedObservation[], limit?: number): MemoryConflict[];