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.
- package/LICENSE +202 -0
- package/README.md +402 -0
- package/dist/bundle/bundle.d.ts +28 -0
- package/dist/bundle/bundle.js +85 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +593 -0
- package/dist/cli/connect.d.ts +63 -0
- package/dist/cli/connect.js +121 -0
- package/dist/cli/hook.d.ts +24 -0
- package/dist/cli/hook.js +186 -0
- package/dist/cli/tools.d.ts +47 -0
- package/dist/cli/tools.js +246 -0
- package/dist/daemon/ensure.d.ts +12 -0
- package/dist/daemon/ensure.js +54 -0
- package/dist/daemon/service.d.ts +15 -0
- package/dist/daemon/service.js +210 -0
- package/dist/embedding/index.d.ts +10 -0
- package/dist/embedding/index.js +33 -0
- package/dist/embedding/local-embedding.d.ts +14 -0
- package/dist/embedding/local-embedding.js +80 -0
- package/dist/functions/access-tracker.d.ts +13 -0
- package/dist/functions/access-tracker.js +92 -0
- package/dist/functions/audit.d.ts +46 -0
- package/dist/functions/audit.js +0 -0
- package/dist/functions/cjk-segmenter.d.ts +6 -0
- package/dist/functions/cjk-segmenter.js +120 -0
- package/dist/functions/compress-synthetic.d.ts +2 -0
- package/dist/functions/compress-synthetic.js +104 -0
- package/dist/functions/config.d.ts +68 -0
- package/dist/functions/config.js +231 -0
- package/dist/functions/conflicts.d.ts +19 -0
- package/dist/functions/conflicts.js +328 -0
- package/dist/functions/context.d.ts +3 -0
- package/dist/functions/context.js +155 -0
- package/dist/functions/dedup.d.ts +11 -0
- package/dist/functions/dedup.js +51 -0
- package/dist/functions/dejafix.d.ts +96 -0
- package/dist/functions/dejafix.js +356 -0
- package/dist/functions/doctor.d.ts +29 -0
- package/dist/functions/doctor.js +137 -0
- package/dist/functions/forget.d.ts +3 -0
- package/dist/functions/forget.js +87 -0
- package/dist/functions/hybrid-search.d.ts +17 -0
- package/dist/functions/hybrid-search.js +205 -0
- package/dist/functions/index.d.ts +32 -0
- package/dist/functions/index.js +44 -0
- package/dist/functions/keyed-mutex.d.ts +1 -0
- package/dist/functions/keyed-mutex.js +21 -0
- package/dist/functions/logger.d.ts +6 -0
- package/dist/functions/logger.js +37 -0
- package/dist/functions/memory-utils.d.ts +2 -0
- package/dist/functions/memory-utils.js +29 -0
- package/dist/functions/observe.d.ts +5 -0
- package/dist/functions/observe.js +326 -0
- package/dist/functions/paths.d.ts +1 -0
- package/dist/functions/paths.js +38 -0
- package/dist/functions/privacy.d.ts +1 -0
- package/dist/functions/privacy.js +30 -0
- package/dist/functions/provenance.d.ts +9 -0
- package/dist/functions/provenance.js +57 -0
- package/dist/functions/quantized-vector-index.d.ts +60 -0
- package/dist/functions/quantized-vector-index.js +275 -0
- package/dist/functions/receipt.d.ts +31 -0
- package/dist/functions/receipt.js +95 -0
- package/dist/functions/search-index.d.ts +27 -0
- package/dist/functions/search-index.js +217 -0
- package/dist/functions/search.d.ts +25 -0
- package/dist/functions/search.js +523 -0
- package/dist/functions/stemmer.d.ts +1 -0
- package/dist/functions/stemmer.js +110 -0
- package/dist/functions/synonyms.d.ts +1 -0
- package/dist/functions/synonyms.js +69 -0
- package/dist/functions/turboquant.d.ts +53 -0
- package/dist/functions/turboquant.js +278 -0
- package/dist/functions/types.d.ts +217 -0
- package/dist/functions/types.js +8 -0
- package/dist/functions/vector-index.d.ts +25 -0
- package/dist/functions/vector-index.js +125 -0
- package/dist/functions/vector-persistence.d.ts +14 -0
- package/dist/functions/vector-persistence.js +75 -0
- package/dist/functions/verify.d.ts +13 -0
- package/dist/functions/verify.js +104 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +219 -0
- package/dist/kernel/http.d.ts +24 -0
- package/dist/kernel/http.js +261 -0
- package/dist/kernel/index.d.ts +19 -0
- package/dist/kernel/index.js +21 -0
- package/dist/kernel/kernel.d.ts +80 -0
- package/dist/kernel/kernel.js +297 -0
- package/dist/kernel/pubsub.d.ts +21 -0
- package/dist/kernel/pubsub.js +38 -0
- package/dist/kernel/types.d.ts +139 -0
- package/dist/kernel/types.js +20 -0
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +27 -0
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/server.js +377 -0
- package/dist/observability/metrics.d.ts +26 -0
- package/dist/observability/metrics.js +104 -0
- package/dist/proxy/server.d.ts +30 -0
- package/dist/proxy/server.js +331 -0
- package/dist/state/kv.d.ts +41 -0
- package/dist/state/kv.js +50 -0
- package/dist/state/oplog.d.ts +25 -0
- package/dist/state/oplog.js +57 -0
- package/dist/state/schema.d.ts +60 -0
- package/dist/state/schema.js +88 -0
- package/dist/state/store-libsql.d.ts +46 -0
- package/dist/state/store-libsql.js +263 -0
- package/dist/state/store-memory.d.ts +23 -0
- package/dist/state/store-memory.js +121 -0
- package/dist/state/store.d.ts +87 -0
- package/dist/state/store.js +58 -0
- package/dist/triggers/api.d.ts +14 -0
- package/dist/triggers/api.js +510 -0
- package/dist/triggers/auth.d.ts +1 -0
- package/dist/triggers/auth.js +13 -0
- 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,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[];
|