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,21 @@
1
+ /** A stream item as emitted by `stream::set` / `stream::send`. */
2
+ export interface StreamItem {
3
+ stream_name?: string;
4
+ group_id?: string;
5
+ item_id?: string;
6
+ id?: string;
7
+ type?: string;
8
+ data?: unknown;
9
+ }
10
+ export declare class PubSub {
11
+ private emitter;
12
+ constructor();
13
+ /** Subscribe to a durable topic (used by `durable:subscriber`). */
14
+ subscribe(topic: string, handler: (payload: unknown) => void): void;
15
+ /** Publish to a durable topic. */
16
+ publish(topic: string, payload: unknown): void;
17
+ /** Subscribe to the live stream surface (viewer wiring). */
18
+ onStream(handler: (item: StreamItem) => void): void;
19
+ /** Emit a stream item. Mirrors `stream::set` / `stream::send`. */
20
+ emitStream(item: StreamItem): void;
21
+ }
@@ -0,0 +1,38 @@
1
+ //
2
+ // In-process pub/sub. In a single-process kernel a plain EventEmitter
3
+ // satisfies both the
4
+ // `durable:subscriber` topic triggers (events.ts) and the
5
+ // `stream::set` / `stream::send` surface that observe.ts fires for the
6
+ // live viewer. Durability is effectively unused: the four durable
7
+ // topics duplicate paths already reachable synchronously via trigger.
8
+ import { EventEmitter } from "node:events";
9
+ export class PubSub {
10
+ emitter = new EventEmitter();
11
+ constructor() {
12
+ // The viewer/stream listeners are best-effort; never let an absent
13
+ // listener or a throwing one crash the process.
14
+ this.emitter.setMaxListeners(0);
15
+ this.emitter.on("error", () => {
16
+ /* swallow: pub/sub is best-effort */
17
+ });
18
+ }
19
+ /** Subscribe to a durable topic (used by `durable:subscriber`). */
20
+ subscribe(topic, handler) {
21
+ this.emitter.on(topicKey(topic), handler);
22
+ }
23
+ /** Publish to a durable topic. */
24
+ publish(topic, payload) {
25
+ this.emitter.emit(topicKey(topic), payload);
26
+ }
27
+ /** Subscribe to the live stream surface (viewer wiring). */
28
+ onStream(handler) {
29
+ this.emitter.on("stream", handler);
30
+ }
31
+ /** Emit a stream item. Mirrors `stream::set` / `stream::send`. */
32
+ emitStream(item) {
33
+ this.emitter.emit("stream", item);
34
+ }
35
+ }
36
+ function topicKey(topic) {
37
+ return `topic:${topic}`;
38
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * HTTP methods the kernel's router understands. Mirrors the
3
+ * `http_method` values used across the wired route registrations.
4
+ */
5
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
6
+ /**
7
+ * Request object handed to an HTTP-bound function. The body is the
8
+ * already-JSON-parsed request payload; headers preserve both the
9
+ * lowercase and original-case spellings the app reads.
10
+ */
11
+ export interface ApiRequest<T = unknown> {
12
+ body?: T;
13
+ headers?: Record<string, string | undefined>;
14
+ query_params?: Record<string, string>;
15
+ }
16
+ /**
17
+ * Response shape every HTTP-bound function returns. The kernel sets the
18
+ * status, merges headers, and JSON-stringifies `body`.
19
+ */
20
+ export interface ApiResponse {
21
+ status_code: number;
22
+ headers?: Record<string, string>;
23
+ body: unknown;
24
+ }
25
+ /**
26
+ * Result of an auth/validation middleware function. Either let the
27
+ * request through, or short-circuit with a canned response.
28
+ */
29
+ export type MiddlewareResult = {
30
+ action: "continue";
31
+ } | {
32
+ action: "respond";
33
+ response: {
34
+ status_code: number;
35
+ body: unknown;
36
+ };
37
+ };
38
+ /** A registered function: a plain (possibly async) handler. */
39
+ export type FunctionHandler = (payload: any) => Promise<any> | any;
40
+ /**
41
+ * Fire-and-forget sentinel. `TriggerAction.Void()` marks a trigger call
42
+ * whose result the caller does not await; a rejected Void trigger must
43
+ * never crash the process.
44
+ */
45
+ export interface VoidAction {
46
+ __void: true;
47
+ }
48
+ /** Options accepted by `trigger`. */
49
+ export interface TriggerOptions<P = any> {
50
+ function_id: string;
51
+ payload: P;
52
+ action?: VoidAction;
53
+ }
54
+ /**
55
+ * Discriminated trigger registration config. Binds an
56
+ * already-registered function to an external surface.
57
+ */
58
+ export type TriggerConfig = {
59
+ type: "http";
60
+ function_id: string;
61
+ config: {
62
+ api_path: string;
63
+ http_method: HttpMethod;
64
+ middleware_function_ids?: string[];
65
+ };
66
+ } | {
67
+ type: "durable:subscriber";
68
+ function_id: string;
69
+ config: {
70
+ topic: string;
71
+ };
72
+ } | {
73
+ type: "state";
74
+ function_id: string;
75
+ config: {
76
+ scope: string;
77
+ };
78
+ };
79
+ /** Payload delivered to a `type:"state"` trigger on every KV mutation. */
80
+ export interface StateChangeEvent {
81
+ key: string;
82
+ event_type: "set" | "update" | "delete";
83
+ old_value?: unknown;
84
+ new_value?: unknown;
85
+ }
86
+ /** Minimal OTel-shaped meter. add/record may be no-ops. */
87
+ export interface Counter {
88
+ add: (n: number) => void;
89
+ }
90
+ export interface Histogram {
91
+ record: (v: number) => void;
92
+ }
93
+ export interface Meter {
94
+ createCounter: (name: string) => Counter;
95
+ createHistogram: (name: string) => Histogram;
96
+ }
97
+ /** Connection-state callback. Only `"connection_state"` is observed. */
98
+ export type ConnectionStateListener = (state?: unknown) => void;
99
+ /**
100
+ * Telemetry / otel metadata accepted by `registerWorker`. Ignored by
101
+ * the in-process kernel beyond being stored for introspection.
102
+ */
103
+ export interface RegisterWorkerOptions {
104
+ workerName: string;
105
+ invocationTimeoutMs?: number;
106
+ otel?: {
107
+ serviceName: string;
108
+ serviceVersion: string;
109
+ metricsExportIntervalMs: number;
110
+ };
111
+ telemetry?: {
112
+ project_name: string;
113
+ language: string;
114
+ framework: string;
115
+ };
116
+ }
117
+ /**
118
+ * The single object the rest of the app talks to. Exactly the members
119
+ * the wired call sites reference. `on` and `getMeter` are optional and
120
+ * feature-detected by callers.
121
+ */
122
+ export interface ISdk {
123
+ registerFunction(id: string, handler: FunctionHandler): void;
124
+ trigger<P = any, R = any>(opts: TriggerOptions<P>): Promise<R>;
125
+ registerTrigger(cfg: TriggerConfig): void;
126
+ on?(event: "connection_state", cb: ConnectionStateListener): void;
127
+ shutdown(): Promise<void>;
128
+ getMeter?(name: string): Meter;
129
+ }
130
+ /**
131
+ * Error carried by a rejected `trigger`. The process-level
132
+ * unhandledRejection handler in the boot entrypoint reads `code`,
133
+ * `function_id`, and `message`.
134
+ */
135
+ export declare class TriggerError extends Error {
136
+ code: string;
137
+ function_id: string;
138
+ constructor(message: string, code: string, functionId: string);
139
+ }
@@ -0,0 +1,20 @@
1
+ //
2
+ // Type surface for the memwarden kernel. Only the members the app code
3
+ // actually touches are modelled here; the kernel is an in-process,
4
+ // single-instance runtime, so heavier runtime concepts (otel transport,
5
+ // durable streams, worker fleets) collapse to no-ops or in-memory equivalents.
6
+ /**
7
+ * Error carried by a rejected `trigger`. The process-level
8
+ * unhandledRejection handler in the boot entrypoint reads `code`,
9
+ * `function_id`, and `message`.
10
+ */
11
+ export class TriggerError extends Error {
12
+ code;
13
+ function_id;
14
+ constructor(message, code, functionId) {
15
+ super(message);
16
+ this.name = "TriggerError";
17
+ this.code = code;
18
+ this.function_id = functionId;
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // memwarden MCP server entrypoint. Speaks JSON-RPC over stdio and proxies
4
+ // to a running memwarden daemon. Point any MCP client at this:
5
+ //
6
+ // { "command": "npx", "args": ["-y", "@memwarden/mcp"],
7
+ // "env": { "MEMWARDEN_URL": "http://localhost:3111" } }
8
+ import { createMcpServer, runStdio } from "./server.js";
9
+ import { ensureDaemon } from "../daemon/ensure.js";
10
+ import { getSecret } from "../functions/config.js";
11
+ const baseUrl = process.env.MEMWARDEN_URL ?? "http://localhost:3111";
12
+ // Resolve env first, then the persisted <dataDir>/secret file, so a server
13
+ // launched without the env var still authenticates to a secured daemon.
14
+ const secret = getSecret();
15
+ // Self-heal: revive the daemon on demand if a request finds it down.
16
+ const ensureUp = async () => {
17
+ await ensureDaemon(baseUrl);
18
+ };
19
+ const server = createMcpServer({
20
+ baseUrl,
21
+ ensureUp,
22
+ ...(secret ? { secret } : {}),
23
+ });
24
+ // Warm the daemon at startup so the first recall is instant, but never block
25
+ // the stdio handshake on it.
26
+ void ensureDaemon(baseUrl).catch(() => undefined);
27
+ runStdio(server);
@@ -0,0 +1,34 @@
1
+ export interface McpServerOptions {
2
+ baseUrl: string;
3
+ secret?: string;
4
+ fetchFn?: typeof fetch;
5
+ cwd?: string;
6
+ ensureUp?: () => Promise<void>;
7
+ }
8
+ interface JsonRpcRequest {
9
+ jsonrpc: "2.0";
10
+ id?: string | number | null;
11
+ method: string;
12
+ params?: unknown;
13
+ }
14
+ interface JsonRpcResponse {
15
+ jsonrpc: "2.0";
16
+ id: string | number | null;
17
+ result?: unknown;
18
+ error?: {
19
+ code: number;
20
+ message: string;
21
+ };
22
+ }
23
+ export declare function createMcpServer(opts: McpServerOptions): {
24
+ dispatch: (req: JsonRpcRequest) => Promise<JsonRpcResponse | null>;
25
+ toolNames: () => string[];
26
+ promptNames: () => string[];
27
+ };
28
+ export type McpServer = ReturnType<typeof createMcpServer>;
29
+ /**
30
+ * Wire a server to newline-delimited JSON-RPC over stdin/stdout. Kept thin
31
+ * and separate from dispatch() so the protocol logic stays testable.
32
+ */
33
+ export declare function runStdio(server: McpServer): void;
34
+ export {};
@@ -0,0 +1,377 @@
1
+ //
2
+ // Dependency-free MCP server for memwarden. A hand-rolled JSON-RPC 2.0
3
+ // dispatcher over stdio — no @modelcontextprotocol/sdk — so the core stays
4
+ // lean and self-contained, and the dispatcher is unit-testable without a
5
+ // host or a pipe. It proxies to a running memwarden daemon over HTTP, so
6
+ // every MCP client (Claude Code, Cursor, Claude Desktop, Cline, Windsurf)
7
+ // shares the one local brain.
8
+ //
9
+ // Beyond the usual save/search/context tools, it exposes the two memwarden
10
+ // has and others don't: memory_verify (tamper-evident oplog integrity) and
11
+ // memory_stats (live TurboQuant compression ratio).
12
+ //
13
+ // It also exposes an MCP *prompt*, `recall`, which clients surface as a
14
+ // slash command (in Claude Code: `/mcp__memwarden__recall <query>`). The
15
+ // user types it mid-chat, the server searches the project's memory and
16
+ // returns the matching context as a message the client injects into the
17
+ // conversation — on-demand recall from within any MCP-aware tool.
18
+ const PROTOCOL_VERSION = "2024-11-05";
19
+ const SERVER_NAME = "memwarden";
20
+ const SERVER_VERSION = "0.1.0";
21
+ function str(v, fallback = "") {
22
+ return typeof v === "string" && v.length > 0 ? v : fallback;
23
+ }
24
+ export function createMcpServer(opts) {
25
+ const base = opts.baseUrl.replace(/\/$/, "");
26
+ const doFetch = opts.fetchFn ?? fetch;
27
+ const serverCwd = opts.cwd ?? process.cwd();
28
+ async function api(method, path, body) {
29
+ const headers = { "content-type": "application/json" };
30
+ if (opts.secret)
31
+ headers["authorization"] = `Bearer ${opts.secret}`;
32
+ const init = {
33
+ method,
34
+ headers,
35
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
36
+ };
37
+ // One self-heal retry: a network error means the daemon is down; revive
38
+ // it and try again so the user's request just works.
39
+ for (let attempt = 0;; attempt++) {
40
+ try {
41
+ const res = await doFetch(`${base}${path}`, init);
42
+ const text = await res.text();
43
+ try {
44
+ return JSON.parse(text);
45
+ }
46
+ catch {
47
+ return { raw: text, status: res.status };
48
+ }
49
+ }
50
+ catch (err) {
51
+ if (attempt === 0 && opts.ensureUp) {
52
+ await opts.ensureUp();
53
+ continue;
54
+ }
55
+ throw err;
56
+ }
57
+ }
58
+ }
59
+ const tools = [
60
+ {
61
+ name: "memory_resume",
62
+ description: "Recall what was being worked on in THIS project, across every past session and every agent (Claude, Codex, Cursor, …). Call this whenever the user references earlier work — e.g. 'continue what we were doing', 'review the project Claude and I built', 'what was I working on here', 'pick up from the last session'. Returns a narrative digest of the relevant prior context, already scoped to the current working directory, ready to act on.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ query: {
67
+ type: "string",
68
+ description: "What to focus the recall on (e.g. 'the auth refactor', 'review the project'). Optional but improves relevance.",
69
+ },
70
+ cwd: {
71
+ type: "string",
72
+ description: "Working directory to scope to. Defaults to where this server was launched (the current project).",
73
+ },
74
+ token_budget: {
75
+ type: "number",
76
+ description: "Max tokens of context to return (default 2000).",
77
+ },
78
+ },
79
+ },
80
+ call: (a) => api("POST", "/memwarden/search", {
81
+ query: str(a["query"], "what was I working on"),
82
+ cwd: str(a["cwd"], serverCwd),
83
+ format: "narrative",
84
+ limit: 20,
85
+ safe_only: true, // Verified Recall: never resume stale memory
86
+ ...(typeof a["token_budget"] === "number"
87
+ ? { token_budget: a["token_budget"] }
88
+ : { token_budget: 2000 }),
89
+ }),
90
+ },
91
+ {
92
+ name: "memory_remember",
93
+ description: "Save a memory so any agent can recall it later. Persisted to the local memwarden store.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ text: { type: "string", description: "The content to remember" },
98
+ sessionId: { type: "string", description: "Optional session id" },
99
+ project: { type: "string", description: "Optional project label" },
100
+ },
101
+ required: ["text"],
102
+ },
103
+ call: (a) => api("POST", "/memwarden/observe", {
104
+ hookType: "post_tool_use",
105
+ sessionId: str(a["sessionId"], "mcp"),
106
+ project: str(a["project"], "mcp"),
107
+ cwd: str(a["project"], "mcp"),
108
+ timestamp: new Date().toISOString(),
109
+ data: {
110
+ tool_name: "memory_remember",
111
+ tool_input: { text: str(a["text"]) },
112
+ tool_output: str(a["text"]),
113
+ },
114
+ }),
115
+ },
116
+ {
117
+ name: "memory_search",
118
+ description: "Search memories by meaning and keywords (TurboQuant vector + BM25 hybrid). Returns ranked matches.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ query: { type: "string", description: "What to look for" },
123
+ limit: { type: "number", description: "Max results (default 10)" },
124
+ },
125
+ required: ["query"],
126
+ },
127
+ call: (a) => api("POST", "/memwarden/search", {
128
+ query: str(a["query"]),
129
+ limit: typeof a["limit"] === "number" ? a["limit"] : 10,
130
+ }),
131
+ },
132
+ {
133
+ name: "memory_context",
134
+ description: "Pack the most relevant prior memory for this project into a context block under a token budget.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ query: { type: "string", description: "Optional focus query" },
139
+ cwd: { type: "string", description: "Working directory to scope to (defaults to this project)" },
140
+ token_budget: { type: "number", description: "Optional token budget" },
141
+ },
142
+ },
143
+ // Routes through the project-scoped narrative search rather than
144
+ // /context (which needs a sessionId+project the MCP layer doesn't
145
+ // have). Returns a packed, budgeted context block.
146
+ call: (a) => api("POST", "/memwarden/search", {
147
+ query: str(a["query"], "relevant context for this project"),
148
+ cwd: str(a["cwd"], serverCwd),
149
+ format: "narrative",
150
+ limit: 20,
151
+ safe_only: true, // Verified Recall: never inject stale memory
152
+ ...(typeof a["token_budget"] === "number"
153
+ ? { token_budget: a["token_budget"] }
154
+ : { token_budget: 2000 }),
155
+ }),
156
+ },
157
+ {
158
+ name: "dejafix_lookup",
159
+ description: "Déjà Fix: before you try to fix an error, check if ANY agent (Claude, Codex, Cursor, …) already solved this exact error in THIS project. Paste the error message, stack trace, or failing-test output. Returns matching fixes that are still valid — each verified against the live repo (a fix whose files changed or vanished is suppressed, never surfaced) and badged 'verified current' or 'sourced, unverified'. Call this whenever you hit an error before debugging from scratch.",
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ error_text: {
164
+ type: "string",
165
+ description: "The error message, stack trace, or failing-test output to look up.",
166
+ },
167
+ cwd: {
168
+ type: "string",
169
+ description: "Working directory to scope to. Defaults to where this server was launched (the current project).",
170
+ },
171
+ },
172
+ required: ["error_text"],
173
+ },
174
+ call: (a) => api("POST", "/memwarden/dejafix/lookup", {
175
+ error_text: str(a["error_text"]),
176
+ cwd: str(a["cwd"], serverCwd),
177
+ }),
178
+ },
179
+ {
180
+ name: "dejafix_record",
181
+ description: "Déjà Fix: record how an error was resolved so any agent that hits it again can recall the fix. Provide the original error text and a short fix narrative; optionally a root cause and the files the fix touched (hashed now so stale fixes are auto-suppressed later). Call this right after you resolve a non-trivial error.",
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ error_text: {
186
+ type: "string",
187
+ description: "The error message/stack trace that was resolved.",
188
+ },
189
+ fix: {
190
+ type: "string",
191
+ description: "Short narrative of the fix that resolved it.",
192
+ },
193
+ root_cause: {
194
+ type: "string",
195
+ description: "Optional one-line root cause.",
196
+ },
197
+ files: {
198
+ type: "array",
199
+ items: { type: "string" },
200
+ description: "Files the fix touched/relied on (relative to cwd). Hashed for drift detection.",
201
+ },
202
+ cwd: {
203
+ type: "string",
204
+ description: "Working directory to scope to. Defaults to where this server was launched.",
205
+ },
206
+ },
207
+ required: ["error_text", "fix"],
208
+ },
209
+ call: (a) => api("POST", "/memwarden/dejafix/record", {
210
+ error_text: str(a["error_text"]),
211
+ fix: str(a["fix"]),
212
+ ...(typeof a["root_cause"] === "string" && a["root_cause"]
213
+ ? { root_cause: a["root_cause"] }
214
+ : {}),
215
+ ...(Array.isArray(a["files"])
216
+ ? {
217
+ files: a["files"].filter((f) => typeof f === "string"),
218
+ }
219
+ : {}),
220
+ cwd: str(a["cwd"], serverCwd),
221
+ }),
222
+ },
223
+ {
224
+ name: "memory_verify",
225
+ description: "Check the oplog hash chain is intact: tamper-EVIDENT integrity. Detects edits and reorders within the memory log (not tamper-proof: there is no signing yet, and truncating the newest entries is not detectable).",
226
+ inputSchema: { type: "object", properties: {} },
227
+ call: () => api("GET", "/memwarden/verify"),
228
+ },
229
+ {
230
+ name: "memory_stats",
231
+ description: "Report memory counts, the active embedding model, and the live TurboQuant compression ratio.",
232
+ inputSchema: { type: "object", properties: {} },
233
+ call: () => api("GET", "/memwarden/stats"),
234
+ },
235
+ ];
236
+ const toolByName = new Map(tools.map((t) => [t.name, t]));
237
+ // MCP prompts. Clients surface these as slash commands; `recall` is the
238
+ // on-demand "pull my memory into this chat" command the whole layer is for.
239
+ const prompts = [
240
+ {
241
+ name: "recall",
242
+ description: "Pull relevant memory from past sessions into THIS chat. Give it what you're working on (e.g. 'the auth refactor'); it searches your memwarden brain — scoped to the current project — and injects the matching context.",
243
+ arguments: [
244
+ {
245
+ name: "query",
246
+ description: "What to recall (e.g. 'auth refactor', 'the proxy work'). Optional — omit to resume the project broadly.",
247
+ required: false,
248
+ },
249
+ ],
250
+ },
251
+ ];
252
+ async function recallText(query) {
253
+ try {
254
+ const result = (await api("POST", "/memwarden/search", {
255
+ query,
256
+ cwd: serverCwd,
257
+ format: "narrative",
258
+ limit: 20,
259
+ token_budget: 2000,
260
+ safe_only: true, // Verified Recall: /recall never injects stale memory
261
+ }));
262
+ return typeof result.text === "string" ? result.text : "";
263
+ }
264
+ catch {
265
+ return "";
266
+ }
267
+ }
268
+ function ok(id, result) {
269
+ return { jsonrpc: "2.0", id, result };
270
+ }
271
+ function fail(id, code, message) {
272
+ return { jsonrpc: "2.0", id, error: { code, message } };
273
+ }
274
+ // Returns the response, or null for notifications (no id / initialized).
275
+ async function dispatch(req) {
276
+ const id = req.id ?? null;
277
+ switch (req.method) {
278
+ case "initialize":
279
+ return ok(id, {
280
+ protocolVersion: PROTOCOL_VERSION,
281
+ capabilities: { tools: {}, prompts: {} },
282
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
283
+ });
284
+ case "notifications/initialized":
285
+ case "initialized":
286
+ return null;
287
+ case "ping":
288
+ return ok(id, {});
289
+ case "tools/list":
290
+ return ok(id, {
291
+ tools: tools.map((t) => ({
292
+ name: t.name,
293
+ description: t.description,
294
+ inputSchema: t.inputSchema,
295
+ })),
296
+ });
297
+ case "prompts/list":
298
+ return ok(id, { prompts });
299
+ case "prompts/get": {
300
+ const params = (req.params ?? {});
301
+ if (params.name !== "recall") {
302
+ return fail(id, -32602, `unknown prompt: ${params.name}`);
303
+ }
304
+ const query = str(params.arguments?.["query"], "what was I working on in this project");
305
+ const text = await recallText(query);
306
+ const body = text
307
+ ? `Relevant memory recalled by memwarden (scoped to this project) for "${query}":\n\n${text}`
308
+ : `No relevant memory found for "${query}".`;
309
+ return ok(id, {
310
+ description: `memwarden recall: ${query}`,
311
+ messages: [{ role: "user", content: { type: "text", text: body } }],
312
+ });
313
+ }
314
+ case "tools/call": {
315
+ const params = (req.params ?? {});
316
+ const tool = params.name ? toolByName.get(params.name) : undefined;
317
+ if (!tool)
318
+ return fail(id, -32602, `unknown tool: ${params.name}`);
319
+ try {
320
+ const result = await tool.call(params.arguments ?? {});
321
+ return ok(id, {
322
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
323
+ });
324
+ }
325
+ catch (err) {
326
+ return ok(id, {
327
+ isError: true,
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: `memory tool error: ${err instanceof Error ? err.message : String(err)}`,
332
+ },
333
+ ],
334
+ });
335
+ }
336
+ }
337
+ default:
338
+ if (id === null)
339
+ return null; // unknown notification
340
+ return fail(id, -32601, `method not found: ${req.method}`);
341
+ }
342
+ }
343
+ return {
344
+ dispatch,
345
+ toolNames: () => tools.map((t) => t.name),
346
+ promptNames: () => prompts.map((p) => p.name),
347
+ };
348
+ }
349
+ /**
350
+ * Wire a server to newline-delimited JSON-RPC over stdin/stdout. Kept thin
351
+ * and separate from dispatch() so the protocol logic stays testable.
352
+ */
353
+ export function runStdio(server) {
354
+ let buffer = "";
355
+ process.stdin.setEncoding("utf8");
356
+ process.stdin.on("data", (chunk) => {
357
+ buffer += chunk;
358
+ let nl;
359
+ while ((nl = buffer.indexOf("\n")) >= 0) {
360
+ const line = buffer.slice(0, nl).trim();
361
+ buffer = buffer.slice(nl + 1);
362
+ if (!line)
363
+ continue;
364
+ let req;
365
+ try {
366
+ req = JSON.parse(line);
367
+ }
368
+ catch {
369
+ continue;
370
+ }
371
+ void server.dispatch(req).then((res) => {
372
+ if (res)
373
+ process.stdout.write(JSON.stringify(res) + "\n");
374
+ });
375
+ }
376
+ });
377
+ }