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,63 @@
|
|
|
1
|
+
export interface ConnectOptions {
|
|
2
|
+
url?: string;
|
|
3
|
+
secret?: string;
|
|
4
|
+
mcpCommand?: string;
|
|
5
|
+
mcpArgs?: string[];
|
|
6
|
+
}
|
|
7
|
+
/** The MCP server entry pointing at the memwarden MCP stdio adapter. */
|
|
8
|
+
export declare function buildMcpServerEntry(opts?: ConnectOptions): {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
interface HookCommand {
|
|
14
|
+
type: "command";
|
|
15
|
+
command: string;
|
|
16
|
+
}
|
|
17
|
+
interface HookGroup {
|
|
18
|
+
matcher?: string;
|
|
19
|
+
hooks: HookCommand[];
|
|
20
|
+
}
|
|
21
|
+
interface ClaudeSettings {
|
|
22
|
+
hooks?: Record<string, HookGroup[]>;
|
|
23
|
+
[k: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
/** Claude Code settings path for hooks, rooted at `dir`. */
|
|
26
|
+
export declare function claudeSettingsPathFor(dir: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Build the SessionStart (auto-inject) + PostToolUse (auto-capture) hook
|
|
29
|
+
* groups. `hookBase` is the shell command that runs the memwarden CLI, e.g.
|
|
30
|
+
* `"node" "/abs/dist/cli/bin.js"`; the event subcommand is appended.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildClaudeHooks(hookBase: string): Record<string, HookGroup[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Merge memwarden's hooks into an existing Claude settings object without
|
|
35
|
+
* disturbing the user's other hooks. Idempotent: prior memwarden entries
|
|
36
|
+
* for an event are replaced, not duplicated. Pure; does not mutate input.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mergeClaudeHooks(existing: ClaudeSettings | null, hookBase: string): ClaudeSettings;
|
|
39
|
+
/** Write/merge the Claude Code hooks settings file. */
|
|
40
|
+
export declare function writeClaudeHooks(settingsPath: string, hookBase: string): {
|
|
41
|
+
path: string;
|
|
42
|
+
created: boolean;
|
|
43
|
+
};
|
|
44
|
+
type McpConfig = {
|
|
45
|
+
mcpServers?: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Merge the memwarden server into an existing MCP config without clobbering
|
|
49
|
+
* other servers. Returns a new object; does not mutate the input.
|
|
50
|
+
*/
|
|
51
|
+
export declare function mergeMcpConfig(existing: McpConfig | null, entry: ReturnType<typeof buildMcpServerEntry>): McpConfig;
|
|
52
|
+
/**
|
|
53
|
+
* Write/merge the MCP config file at `configPath`. Returns the path and the
|
|
54
|
+
* resulting config. Creates parent directories as needed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function writeMcpConfig(configPath: string, opts?: ConnectOptions): {
|
|
57
|
+
path: string;
|
|
58
|
+
config: McpConfig;
|
|
59
|
+
created: boolean;
|
|
60
|
+
};
|
|
61
|
+
/** Default MCP config path for a given target tool, rooted at `dir`. */
|
|
62
|
+
export declare function mcpConfigPathFor(target: string, dir: string): string;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//
|
|
2
|
+
// `memwarden connect` — wires the local memwarden daemon into MCP clients so
|
|
3
|
+
// every agent shares one brain. The MCP config is the stable, universal
|
|
4
|
+
// unlock (same block works for Claude Code, Cursor, Claude Desktop, Cline,
|
|
5
|
+
// Windsurf), so that is what this writes. Auto-capture hooks are a separate,
|
|
6
|
+
// tool-specific follow-up.
|
|
7
|
+
//
|
|
8
|
+
// The merge logic is pure and testable; the filesystem wrapper is thin.
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
/** The MCP server entry pointing at the memwarden MCP stdio adapter. */
|
|
12
|
+
export function buildMcpServerEntry(opts = {}) {
|
|
13
|
+
const env = {
|
|
14
|
+
MEMWARDEN_URL: opts.url ?? "http://localhost:3111",
|
|
15
|
+
};
|
|
16
|
+
if (opts.secret)
|
|
17
|
+
env["MEMWARDEN_SECRET"] = opts.secret;
|
|
18
|
+
return {
|
|
19
|
+
command: opts.mcpCommand ?? "npx",
|
|
20
|
+
args: opts.mcpArgs ?? ["-y", "@memwarden/mcp"],
|
|
21
|
+
env,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Claude Code settings path for hooks, rooted at `dir`. */
|
|
25
|
+
export function claudeSettingsPathFor(dir) {
|
|
26
|
+
return join(dir, ".claude", "settings.json");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the SessionStart (auto-inject) + PostToolUse (auto-capture) hook
|
|
30
|
+
* groups. `hookBase` is the shell command that runs the memwarden CLI, e.g.
|
|
31
|
+
* `"node" "/abs/dist/cli/bin.js"`; the event subcommand is appended.
|
|
32
|
+
*/
|
|
33
|
+
export function buildClaudeHooks(hookBase) {
|
|
34
|
+
return {
|
|
35
|
+
SessionStart: [
|
|
36
|
+
{ hooks: [{ type: "command", command: `${hookBase} hook session-start` }] },
|
|
37
|
+
],
|
|
38
|
+
PostToolUse: [
|
|
39
|
+
{ matcher: "*", hooks: [{ type: "command", command: `${hookBase} hook capture` }] },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function isMemwardenHookGroup(g) {
|
|
44
|
+
return g.hooks.some((h) => h.command.includes("memwarden") || h.command.includes("hook session-start") || h.command.includes("hook capture"));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Merge memwarden's hooks into an existing Claude settings object without
|
|
48
|
+
* disturbing the user's other hooks. Idempotent: prior memwarden entries
|
|
49
|
+
* for an event are replaced, not duplicated. Pure; does not mutate input.
|
|
50
|
+
*/
|
|
51
|
+
export function mergeClaudeHooks(existing, hookBase) {
|
|
52
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
53
|
+
const ours = buildClaudeHooks(hookBase);
|
|
54
|
+
const hooks = { ...(base.hooks ?? {}) };
|
|
55
|
+
for (const event of Object.keys(ours)) {
|
|
56
|
+
const kept = (hooks[event] ?? []).filter((g) => !isMemwardenHookGroup(g));
|
|
57
|
+
hooks[event] = [...kept, ...(ours[event] ?? [])];
|
|
58
|
+
}
|
|
59
|
+
return { ...base, hooks };
|
|
60
|
+
}
|
|
61
|
+
/** Write/merge the Claude Code hooks settings file. */
|
|
62
|
+
export function writeClaudeHooks(settingsPath, hookBase) {
|
|
63
|
+
let existing = null;
|
|
64
|
+
const created = !existsSync(settingsPath);
|
|
65
|
+
if (!created) {
|
|
66
|
+
try {
|
|
67
|
+
existing = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
existing = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const merged = mergeClaudeHooks(existing, hookBase);
|
|
74
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
75
|
+
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
76
|
+
return { path: settingsPath, created };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Merge the memwarden server into an existing MCP config without clobbering
|
|
80
|
+
* other servers. Returns a new object; does not mutate the input.
|
|
81
|
+
*/
|
|
82
|
+
export function mergeMcpConfig(existing, entry) {
|
|
83
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
mcpServers: { ...(base.mcpServers ?? {}), memwarden: entry },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Write/merge the MCP config file at `configPath`. Returns the path and the
|
|
91
|
+
* resulting config. Creates parent directories as needed.
|
|
92
|
+
*/
|
|
93
|
+
export function writeMcpConfig(configPath, opts = {}) {
|
|
94
|
+
let existing = null;
|
|
95
|
+
const created = !existsSync(configPath);
|
|
96
|
+
if (!created) {
|
|
97
|
+
try {
|
|
98
|
+
existing = JSON.parse(readFileSync(configPath, "utf8"));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
existing = null; // corrupt/non-JSON: start fresh rather than throw
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const merged = mergeMcpConfig(existing, buildMcpServerEntry(opts));
|
|
105
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
106
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
107
|
+
return { path: configPath, config: merged, created };
|
|
108
|
+
}
|
|
109
|
+
/** Default MCP config path for a given target tool, rooted at `dir`. */
|
|
110
|
+
export function mcpConfigPathFor(target, dir) {
|
|
111
|
+
switch (target) {
|
|
112
|
+
case "claude-code":
|
|
113
|
+
case "cursor":
|
|
114
|
+
case "cline":
|
|
115
|
+
case "windsurf":
|
|
116
|
+
// All of these read a project-level .mcp.json.
|
|
117
|
+
return join(dir, ".mcp.json");
|
|
118
|
+
default:
|
|
119
|
+
return join(dir, ".mcp.json");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface HookDeps {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
secret?: string;
|
|
4
|
+
fetchFn?: typeof fetch;
|
|
5
|
+
now?: () => string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* SessionStart: return a Claude-Code-style context injection containing this
|
|
9
|
+
* project's recent memory. Empty string when there is nothing to inject
|
|
10
|
+
* (a brand-new project, or the daemon is down) — a hook that prints nothing
|
|
11
|
+
* is a no-op, never an error.
|
|
12
|
+
*/
|
|
13
|
+
export declare function handleSessionStart(raw: string, deps: HookDeps): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* PostToolUse: forward the tool call to the daemon's observe path, and — the
|
|
16
|
+
* Déjà Fix path — if the tool's output looks like an error, ask the daemon
|
|
17
|
+
* whether any agent already solved it and inject the verified fix. Capture is
|
|
18
|
+
* best-effort and failures are swallowed so a downed daemon never breaks the
|
|
19
|
+
* agent's turn. Returns the Déjà Fix injection (or "" when there's nothing
|
|
20
|
+
* verified to surface).
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleCapture(raw: string, deps: HookDeps): Promise<string>;
|
|
23
|
+
/** Read all of stdin as a string. */
|
|
24
|
+
export declare function readStdin(): Promise<string>;
|
package/dist/cli/hook.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Agent lifecycle hook handlers — the "knows before you ask" path.
|
|
3
|
+
//
|
|
4
|
+
// Coding agents (Claude Code, Codex, …) run hook commands and pass a JSON
|
|
5
|
+
// event on stdin. memwarden wires two:
|
|
6
|
+
//
|
|
7
|
+
// SessionStart -> handleSessionStart: pulls this project's recent memory
|
|
8
|
+
// from the daemon and prints it as injected context, so a freshly
|
|
9
|
+
// opened agent already knows what was done here — even by another tool.
|
|
10
|
+
// PostToolUse -> handleCapture: forwards the tool call/result to the
|
|
11
|
+
// daemon's observe path so memory accrues with zero manual effort.
|
|
12
|
+
//
|
|
13
|
+
// Both are pure over their (rawStdin, deps): the daemon call is an injected
|
|
14
|
+
// fetch and the clock is injected, so they unit-test without a network or a
|
|
15
|
+
// live agent.
|
|
16
|
+
import { isCaptureEnabled, isInjectEnabled, isProjectExcluded, } from "../functions/config.js";
|
|
17
|
+
function headers(deps) {
|
|
18
|
+
const h = { "content-type": "application/json" };
|
|
19
|
+
if (deps.secret)
|
|
20
|
+
h["authorization"] = `Bearer ${deps.secret}`;
|
|
21
|
+
return h;
|
|
22
|
+
}
|
|
23
|
+
function parse(raw) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* SessionStart: return a Claude-Code-style context injection containing this
|
|
33
|
+
* project's recent memory. Empty string when there is nothing to inject
|
|
34
|
+
* (a brand-new project, or the daemon is down) — a hook that prints nothing
|
|
35
|
+
* is a no-op, never an error.
|
|
36
|
+
*/
|
|
37
|
+
export async function handleSessionStart(raw, deps) {
|
|
38
|
+
const evt = parse(raw);
|
|
39
|
+
const cwd = evt.cwd ?? process.cwd();
|
|
40
|
+
// User switches: MEMWARDEN_INJECT=off (clean-slate sessions) and the
|
|
41
|
+
// per-project exclude list both silence auto-injection entirely.
|
|
42
|
+
if (!isInjectEnabled() || isProjectExcluded(cwd))
|
|
43
|
+
return "";
|
|
44
|
+
const doFetch = deps.fetchFn ?? fetch;
|
|
45
|
+
try {
|
|
46
|
+
const res = await doFetch(`${deps.baseUrl}/memwarden/search`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: headers(deps),
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
query: "recent work and decisions in this project",
|
|
51
|
+
cwd,
|
|
52
|
+
format: "narrative",
|
|
53
|
+
limit: 20,
|
|
54
|
+
token_budget: 1500,
|
|
55
|
+
safe_only: true, // Verified Recall: SessionStart never injects stale memory
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok)
|
|
59
|
+
return "";
|
|
60
|
+
// Narrative-format /search returns the packed block under `text`.
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
const text = data.text ?? "";
|
|
63
|
+
if (!text.trim())
|
|
64
|
+
return "";
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
hookSpecificOutput: {
|
|
67
|
+
hookEventName: "SessionStart",
|
|
68
|
+
additionalContext: "Relevant memory from previous sessions in this project " +
|
|
69
|
+
"(captured by memwarden across all your agents):\n\n" +
|
|
70
|
+
text,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* PostToolUse: forward the tool call to the daemon's observe path, and — the
|
|
80
|
+
* Déjà Fix path — if the tool's output looks like an error, ask the daemon
|
|
81
|
+
* whether any agent already solved it and inject the verified fix. Capture is
|
|
82
|
+
* best-effort and failures are swallowed so a downed daemon never breaks the
|
|
83
|
+
* agent's turn. Returns the Déjà Fix injection (or "" when there's nothing
|
|
84
|
+
* verified to surface).
|
|
85
|
+
*/
|
|
86
|
+
export async function handleCapture(raw, deps) {
|
|
87
|
+
const evt = parse(raw);
|
|
88
|
+
const cwd = evt.cwd ?? process.cwd();
|
|
89
|
+
// An excluded project never reaches the brain (no capture) AND the brain
|
|
90
|
+
// never reaches it (no Déjà Fix injection) — the exclude gates every
|
|
91
|
+
// automatic surface, not just some of them.
|
|
92
|
+
if (isProjectExcluded(cwd))
|
|
93
|
+
return "";
|
|
94
|
+
const doFetch = deps.fetchFn ?? fetch;
|
|
95
|
+
const now = deps.now ? deps.now() : new Date().toISOString();
|
|
96
|
+
if (!isCaptureEnabled()) {
|
|
97
|
+
return isInjectEnabled() ? dejaFixInjection(evt, cwd, deps, doFetch) : "";
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await doFetch(`${deps.baseUrl}/memwarden/observe`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: headers(deps),
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
hookType: "post_tool_use",
|
|
105
|
+
sessionId: evt.session_id ?? "hook",
|
|
106
|
+
project: cwd,
|
|
107
|
+
cwd,
|
|
108
|
+
timestamp: now,
|
|
109
|
+
data: {
|
|
110
|
+
tool_name: evt.tool_name ?? "unknown",
|
|
111
|
+
tool_input: evt.tool_input ?? {},
|
|
112
|
+
tool_output: evt.tool_response ?? "",
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// swallow — capture is best-effort
|
|
119
|
+
}
|
|
120
|
+
return isInjectEnabled() ? dejaFixInjection(evt, cwd, deps, doFetch) : "";
|
|
121
|
+
}
|
|
122
|
+
// Cheap client-side gate: only ask the daemon for a fix when the output plausibly
|
|
123
|
+
// contains an error, so a clean tool call doesn't cost a second round-trip.
|
|
124
|
+
const ERROR_HINT_RE = /\b(error|errno|exception|traceback|failed|failure|panic)\b|[✕✗×]/i;
|
|
125
|
+
function outputText(toolResponse) {
|
|
126
|
+
if (typeof toolResponse === "string")
|
|
127
|
+
return toolResponse;
|
|
128
|
+
if (toolResponse == null)
|
|
129
|
+
return "";
|
|
130
|
+
try {
|
|
131
|
+
return JSON.stringify(toolResponse);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* If the tool output looks like an error, look it up in Déjà Fix. Inject ONLY
|
|
139
|
+
* a "verified current" fix (every referenced file still hash-matches) — the
|
|
140
|
+
* conservative, trustworthy default; sourced-but-unverified fixes stay
|
|
141
|
+
* available via /recall and the dejafix tools but are never auto-injected.
|
|
142
|
+
*/
|
|
143
|
+
async function dejaFixInjection(evt, cwd, deps, doFetch) {
|
|
144
|
+
const text = outputText(evt.tool_response);
|
|
145
|
+
if (!text.trim() || !ERROR_HINT_RE.test(text))
|
|
146
|
+
return "";
|
|
147
|
+
try {
|
|
148
|
+
const res = await doFetch(`${deps.baseUrl}/memwarden/dejafix/lookup`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: headers(deps),
|
|
151
|
+
body: JSON.stringify({ error_text: text, cwd }),
|
|
152
|
+
});
|
|
153
|
+
if (!res.ok)
|
|
154
|
+
return "";
|
|
155
|
+
const data = (await res.json());
|
|
156
|
+
const fix = (data.fixes ?? []).find((f) => f.status === "verified" && f.fix);
|
|
157
|
+
if (!fix || !fix.fix)
|
|
158
|
+
return "";
|
|
159
|
+
const who = fix.tool ? `by ${fix.tool}` : "earlier";
|
|
160
|
+
const when = fix.timestamp ? ` on ${fix.timestamp.slice(0, 10)}` : "";
|
|
161
|
+
const cause = fix.rootCause ? `\nRoot cause: ${fix.rootCause}` : "";
|
|
162
|
+
return JSON.stringify({
|
|
163
|
+
hookSpecificOutput: {
|
|
164
|
+
hookEventName: "PostToolUse",
|
|
165
|
+
additionalContext: `Déjà Fix (memwarden): this error was solved ${who}${when} ` +
|
|
166
|
+
`and the fix is verified current against your working tree.${cause}\n` +
|
|
167
|
+
`Fix: ${fix.fix}`,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Read all of stdin as a string. */
|
|
176
|
+
export function readStdin() {
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
let buf = "";
|
|
179
|
+
process.stdin.setEncoding("utf8");
|
|
180
|
+
process.stdin.on("data", (c) => (buf += c));
|
|
181
|
+
process.stdin.on("end", () => resolve(buf));
|
|
182
|
+
// If nothing is piped, don't hang forever.
|
|
183
|
+
if (process.stdin.isTTY)
|
|
184
|
+
resolve("");
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** How the memwarden MCP server is launched (stdio). */
|
|
2
|
+
export interface LaunchInfo {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
env: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/** Whether memory flows automatically, and by what mechanism. */
|
|
8
|
+
export type AutoMode = "hooks" | "agents-md";
|
|
9
|
+
export interface ToolAdapter {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
/** User-scope config file for this tool, rooted at `home`. */
|
|
13
|
+
configPath(home: string): string;
|
|
14
|
+
/** Heuristic: does this tool appear installed for this user? */
|
|
15
|
+
detect(home: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Merge the memwarden MCP server into the tool's existing config text
|
|
18
|
+
* (null when the file does not exist yet). Returns the new file text.
|
|
19
|
+
* Throws if `existing` is present but not parseable — the caller skips
|
|
20
|
+
* rather than clobber a config it cannot understand.
|
|
21
|
+
*/
|
|
22
|
+
merge(existing: string | null, launch: LaunchInfo): string;
|
|
23
|
+
/** How recall/capture happens for this tool, for the summary + wiring. */
|
|
24
|
+
auto: AutoMode;
|
|
25
|
+
}
|
|
26
|
+
export declare const TOOLS: ToolAdapter[];
|
|
27
|
+
export declare function toolById(id: string): ToolAdapter | undefined;
|
|
28
|
+
export declare function memwardenAgentsBlock(): string;
|
|
29
|
+
/** Insert/replace the memwarden block in an AGENTS.md body. Idempotent. */
|
|
30
|
+
export declare function mergeAgentsMd(existing: string | null): string;
|
|
31
|
+
export declare function writeAgentsMd(dir: string): {
|
|
32
|
+
path: string;
|
|
33
|
+
created: boolean;
|
|
34
|
+
};
|
|
35
|
+
export interface WireResult {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
path: string;
|
|
39
|
+
status: "wired" | "skipped";
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Write/merge the memwarden MCP server into one tool's config. Safe by
|
|
44
|
+
* default: if the existing file cannot be parsed, it is left untouched and
|
|
45
|
+
* the result is reported as skipped (never clobbered).
|
|
46
|
+
*/
|
|
47
|
+
export declare function writeTool(adapter: ToolAdapter, home: string, launch: LaunchInfo): WireResult;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Tool adapter registry — the per-tool knowledge that lets `memwarden up`
|
|
3
|
+
// wire one local brain into every AI coding tool. Each adapter knows that
|
|
4
|
+
// tool's user-scope config file and how to merge the memwarden MCP server
|
|
5
|
+
// into it in the tool's exact schema. The merges are pure string->string so
|
|
6
|
+
// they are unit-testable without touching the filesystem; the thin fs
|
|
7
|
+
// wrapper (writeTool) is the only side-effecting part.
|
|
8
|
+
//
|
|
9
|
+
// Config locations verified against each tool's 2026 docs:
|
|
10
|
+
// claude-code ~/.claude.json (mcpServers)
|
|
11
|
+
// cursor ~/.cursor/mcp.json (mcpServers)
|
|
12
|
+
// kiro ~/.kiro/settings/mcp.json (mcpServers)
|
|
13
|
+
// antigravity ~/.gemini/config/mcp_config.json (mcpServers; shared by IDE/CLI)
|
|
14
|
+
// opencode ~/.config/opencode/opencode.json (mcp -> {type,command[],environment})
|
|
15
|
+
// openclaw ~/.openclaw/openclaw.json (mcp.servers)
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
/** Parse existing config, or {} when absent. Throws on present-but-corrupt. */
|
|
19
|
+
function parseOrEmpty(existing) {
|
|
20
|
+
if (existing === null)
|
|
21
|
+
return {};
|
|
22
|
+
const trimmed = existing.trim();
|
|
23
|
+
if (!trimmed)
|
|
24
|
+
return {};
|
|
25
|
+
const parsed = JSON.parse(trimmed);
|
|
26
|
+
if (!parsed || typeof parsed !== "object")
|
|
27
|
+
return {};
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
function asObject(value) {
|
|
31
|
+
return value && typeof value === "object" ? value : {};
|
|
32
|
+
}
|
|
33
|
+
function serialize(obj) {
|
|
34
|
+
return JSON.stringify(obj, null, 2) + "\n";
|
|
35
|
+
}
|
|
36
|
+
// --- per-schema merges ---------------------------------------------
|
|
37
|
+
/** The common `{ mcpServers: { memwarden: {command,args,env} } }` shape. */
|
|
38
|
+
function mergeMcpServers(existing, launch) {
|
|
39
|
+
const base = parseOrEmpty(existing);
|
|
40
|
+
base["mcpServers"] = {
|
|
41
|
+
...asObject(base["mcpServers"]),
|
|
42
|
+
memwarden: { command: launch.command, args: launch.args, env: launch.env },
|
|
43
|
+
};
|
|
44
|
+
return serialize(base);
|
|
45
|
+
}
|
|
46
|
+
/** OpenCode: `mcp` map, `type:"local"`, command is one array, `environment`. */
|
|
47
|
+
function mergeOpencode(existing, launch) {
|
|
48
|
+
const base = parseOrEmpty(existing);
|
|
49
|
+
if (!base["$schema"])
|
|
50
|
+
base["$schema"] = "https://opencode.ai/config.json";
|
|
51
|
+
base["mcp"] = {
|
|
52
|
+
...asObject(base["mcp"]),
|
|
53
|
+
memwarden: {
|
|
54
|
+
type: "local",
|
|
55
|
+
command: [launch.command, ...launch.args],
|
|
56
|
+
enabled: true,
|
|
57
|
+
environment: launch.env,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return serialize(base);
|
|
61
|
+
}
|
|
62
|
+
/** OpenClaw: servers live one level down, under `mcp.servers`. */
|
|
63
|
+
function mergeOpenclaw(existing, launch) {
|
|
64
|
+
const base = parseOrEmpty(existing);
|
|
65
|
+
const mcp = asObject(base["mcp"]);
|
|
66
|
+
mcp["servers"] = {
|
|
67
|
+
...asObject(mcp["servers"]),
|
|
68
|
+
memwarden: { command: launch.command, args: launch.args, env: launch.env },
|
|
69
|
+
};
|
|
70
|
+
base["mcp"] = mcp;
|
|
71
|
+
return serialize(base);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Codex uses TOML (~/.codex/config.toml), not JSON. We can't safely re-parse
|
|
75
|
+
* arbitrary TOML without a parser, so we touch only our own table: strip a
|
|
76
|
+
* prior [mcp_servers.memwarden] table (header through the line before the
|
|
77
|
+
* next table or EOF), then append a fresh one. Every other table is left
|
|
78
|
+
* byte-for-byte intact. Idempotent.
|
|
79
|
+
*/
|
|
80
|
+
function codexBlock(launch) {
|
|
81
|
+
const args = launch.args.map((a) => JSON.stringify(a)).join(", ");
|
|
82
|
+
const env = Object.entries(launch.env)
|
|
83
|
+
.map(([k, v]) => `${k} = ${JSON.stringify(v)}`)
|
|
84
|
+
.join(", ");
|
|
85
|
+
return ("[mcp_servers.memwarden]\n" +
|
|
86
|
+
`command = ${JSON.stringify(launch.command)}\n` +
|
|
87
|
+
`args = [${args}]\n` +
|
|
88
|
+
(env ? `env = { ${env} }\n` : ""));
|
|
89
|
+
}
|
|
90
|
+
function mergeCodexToml(existing, launch) {
|
|
91
|
+
const block = codexBlock(launch);
|
|
92
|
+
if (!existing || !existing.trim())
|
|
93
|
+
return block;
|
|
94
|
+
const stripped = existing
|
|
95
|
+
.replace(/\[mcp_servers\.memwarden\][\s\S]*?(?=\n\[|$)/, "")
|
|
96
|
+
.replace(/\s*$/, "");
|
|
97
|
+
return stripped ? stripped + "\n\n" + block : block;
|
|
98
|
+
}
|
|
99
|
+
// --- the registry --------------------------------------------------
|
|
100
|
+
export const TOOLS = [
|
|
101
|
+
{
|
|
102
|
+
id: "claude-code",
|
|
103
|
+
label: "Claude Code",
|
|
104
|
+
configPath: (home) => join(home, ".claude.json"),
|
|
105
|
+
detect: (home) => existsSync(join(home, ".claude.json")) || existsSync(join(home, ".claude")),
|
|
106
|
+
merge: mergeMcpServers,
|
|
107
|
+
auto: "hooks", // also gets SessionStart/PostToolUse hooks (true auto)
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "codex",
|
|
111
|
+
label: "Codex",
|
|
112
|
+
configPath: (home) => join(home, ".codex", "config.toml"),
|
|
113
|
+
detect: (home) => existsSync(join(home, ".codex")),
|
|
114
|
+
merge: mergeCodexToml,
|
|
115
|
+
auto: "agents-md",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "cursor",
|
|
119
|
+
label: "Cursor",
|
|
120
|
+
configPath: (home) => join(home, ".cursor", "mcp.json"),
|
|
121
|
+
detect: (home) => existsSync(join(home, ".cursor")),
|
|
122
|
+
merge: mergeMcpServers,
|
|
123
|
+
auto: "agents-md",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "kiro",
|
|
127
|
+
label: "Kiro",
|
|
128
|
+
configPath: (home) => join(home, ".kiro", "settings", "mcp.json"),
|
|
129
|
+
detect: (home) => existsSync(join(home, ".kiro")),
|
|
130
|
+
merge: mergeMcpServers,
|
|
131
|
+
auto: "agents-md",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "antigravity",
|
|
135
|
+
label: "Antigravity",
|
|
136
|
+
configPath: (home) => join(home, ".gemini", "config", "mcp_config.json"),
|
|
137
|
+
detect: (home) => existsSync(join(home, ".gemini")),
|
|
138
|
+
merge: mergeMcpServers,
|
|
139
|
+
auto: "agents-md",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "opencode",
|
|
143
|
+
label: "OpenCode",
|
|
144
|
+
configPath: (home) => join(home, ".config", "opencode", "opencode.json"),
|
|
145
|
+
detect: (home) => existsSync(join(home, ".config", "opencode")) ||
|
|
146
|
+
existsSync(join(home, ".opencode")),
|
|
147
|
+
merge: mergeOpencode,
|
|
148
|
+
auto: "agents-md",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "openclaw",
|
|
152
|
+
label: "OpenClaw",
|
|
153
|
+
configPath: (home) => join(home, ".openclaw", "openclaw.json"),
|
|
154
|
+
detect: (home) => existsSync(join(home, ".openclaw")),
|
|
155
|
+
merge: mergeOpenclaw,
|
|
156
|
+
auto: "agents-md",
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
export function toolById(id) {
|
|
160
|
+
return TOOLS.find((t) => t.id === id);
|
|
161
|
+
}
|
|
162
|
+
// --- AGENTS.md: instruction-driven auto-recall ---------------------
|
|
163
|
+
//
|
|
164
|
+
// Tools without a hook system (Cursor, Antigravity, OpenCode, …) can't be
|
|
165
|
+
// made to auto-recall mechanically. The cross-tool lever is AGENTS.md (the
|
|
166
|
+
// Linux Foundation standard read by Cursor, Gemini/Antigravity, OpenCode,
|
|
167
|
+
// Codex): a project instruction telling the agent to use the memory tools at
|
|
168
|
+
// task boundaries. Soft (the agent must follow it) but it is how the
|
|
169
|
+
// ecosystem gets automatic behavior without hooks.
|
|
170
|
+
const AGENTS_START = "<!-- memwarden:start -->";
|
|
171
|
+
const AGENTS_END = "<!-- memwarden:end -->";
|
|
172
|
+
export function memwardenAgentsBlock() {
|
|
173
|
+
return [
|
|
174
|
+
AGENTS_START,
|
|
175
|
+
"## Memory — always use it (memwarden)",
|
|
176
|
+
"",
|
|
177
|
+
"This project has shared, cross-tool memory (memwarden). Treat it as part of every task, automatically, without being asked:",
|
|
178
|
+
"",
|
|
179
|
+
"1. **Before starting any task**, call the `memory_resume` tool (or `/recall`) to load what past sessions — in Claude Code, Codex, Cursor, or any other tool — already learned here.",
|
|
180
|
+
"2. **Whenever you learn something durable** (a decision, a fix, how something works, a preference), call `memory_remember` to save it.",
|
|
181
|
+
"",
|
|
182
|
+
"What you save is available in every tool, so always recall first and save as you go.",
|
|
183
|
+
AGENTS_END,
|
|
184
|
+
"",
|
|
185
|
+
].join("\n");
|
|
186
|
+
}
|
|
187
|
+
/** Insert/replace the memwarden block in an AGENTS.md body. Idempotent. */
|
|
188
|
+
export function mergeAgentsMd(existing) {
|
|
189
|
+
const block = memwardenAgentsBlock();
|
|
190
|
+
if (!existing || !existing.trim())
|
|
191
|
+
return `# AGENTS.md\n\n${block}`;
|
|
192
|
+
const start = existing.indexOf(AGENTS_START);
|
|
193
|
+
const end = existing.indexOf(AGENTS_END);
|
|
194
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
195
|
+
return (existing.slice(0, start) +
|
|
196
|
+
block.trimEnd() +
|
|
197
|
+
existing.slice(end + AGENTS_END.length));
|
|
198
|
+
}
|
|
199
|
+
return existing.replace(/\s*$/, "") + "\n\n" + block;
|
|
200
|
+
}
|
|
201
|
+
export function writeAgentsMd(dir) {
|
|
202
|
+
const path = join(dir, "AGENTS.md");
|
|
203
|
+
const created = !existsSync(path);
|
|
204
|
+
const existing = created ? null : readFileSync(path, "utf8");
|
|
205
|
+
writeFileSync(path, mergeAgentsMd(existing), "utf8");
|
|
206
|
+
return { path, created };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Write/merge the memwarden MCP server into one tool's config. Safe by
|
|
210
|
+
* default: if the existing file cannot be parsed, it is left untouched and
|
|
211
|
+
* the result is reported as skipped (never clobbered).
|
|
212
|
+
*/
|
|
213
|
+
export function writeTool(adapter, home, launch) {
|
|
214
|
+
const path = adapter.configPath(home);
|
|
215
|
+
let existing = null;
|
|
216
|
+
if (existsSync(path)) {
|
|
217
|
+
try {
|
|
218
|
+
existing = readFileSync(path, "utf8");
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return {
|
|
222
|
+
id: adapter.id,
|
|
223
|
+
label: adapter.label,
|
|
224
|
+
path,
|
|
225
|
+
status: "skipped",
|
|
226
|
+
reason: `could not read (${err instanceof Error ? err.message : err})`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let next;
|
|
231
|
+
try {
|
|
232
|
+
next = adapter.merge(existing, launch);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return {
|
|
236
|
+
id: adapter.id,
|
|
237
|
+
label: adapter.label,
|
|
238
|
+
path,
|
|
239
|
+
status: "skipped",
|
|
240
|
+
reason: "existing config is not valid JSON — left untouched",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
244
|
+
writeFileSync(path, next, "utf8");
|
|
245
|
+
return { id: adapter.id, label: adapter.label, path, status: "wired" };
|
|
246
|
+
}
|