@mysten-incubation/memwal-mcp 0.0.4 → 0.0.5-dev.0
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/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +12 -0
- package/plugin/.codex-mcp.json +8 -0
- package/plugin/.codex-plugin/plugin.json +29 -0
- package/plugin/.cursor-mcp.json +8 -0
- package/plugin/.cursor-plugin/plugin.json +12 -0
- package/plugin/hooks/codex-hooks.json +41 -0
- package/plugin/hooks/cursor-hooks.json +24 -0
- package/plugin/hooks/hooks.json +41 -0
- package/plugin/hooks.json +41 -0
- package/plugin/plugin.json +12 -0
- package/plugin/scripts/install_codex_hooks.mjs +163 -0
- package/plugin/scripts/lib/hook-io.mjs +81 -0
- package/plugin/scripts/lib/signals.mjs +38 -0
- package/plugin/scripts/on_post_tool.mjs +45 -0
- package/plugin/scripts/on_session_start.mjs +18 -0
- package/plugin/scripts/on_user_prompt.mjs +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mysten-incubation/memwal-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5-dev.0",
|
|
4
4
|
"description": "Walrus Memory MCP client — single-binary stdio MCP server that bridges Cursor / Claude Desktop / Antigravity / Claude Code to the Walrus Memory relayer. Handles browser-based wallet login on first run.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
|
+
"plugin",
|
|
39
40
|
"README.md"
|
|
40
41
|
],
|
|
41
42
|
"keywords": [
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memwal",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Automatic Walrus Memory for Claude Code — proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Mysten Labs"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://memory.walrus.xyz",
|
|
9
|
+
"repository": "https://github.com/MystenLabs/MemWal",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"keywords": ["memory", "mcp", "walrus", "sui", "semantic-search"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memwal",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Persistent Walrus Memory for Codex. Remembers decisions, preferences, and project context across sessions.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Mysten Labs",
|
|
7
|
+
"url": "https://memory.walrus.xyz"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://memory.walrus.xyz",
|
|
10
|
+
"repository": "https://github.com/MystenLabs/MemWal",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"keywords": ["memory", "personalization", "mcp", "walrus", "semantic-search"],
|
|
13
|
+
"mcpServers": "./.codex-mcp.json",
|
|
14
|
+
"hooks": "./hooks/codex-hooks.json",
|
|
15
|
+
"interface": {
|
|
16
|
+
"displayName": "MemWal",
|
|
17
|
+
"shortDescription": "Portable, encrypted memory layer for AI coding workflows",
|
|
18
|
+
"longDescription": "MemWal adds long-term, user-owned memory to Codex. Store decisions, preferences, and session context; memories are encrypted with SEAL and stored on Walrus, and retrieved via semantic search so Codex always has the right context.",
|
|
19
|
+
"developerName": "Mysten Labs",
|
|
20
|
+
"category": "Productivity",
|
|
21
|
+
"capabilities": ["Read", "Write"],
|
|
22
|
+
"websiteURL": "https://memory.walrus.xyz",
|
|
23
|
+
"defaultPrompt": [
|
|
24
|
+
"Search my memories for recent project decisions",
|
|
25
|
+
"Remember that I prefer pnpm and TypeScript strict mode",
|
|
26
|
+
"What do you know about my coding preferences?"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memwal",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Automatic Walrus Memory for Cursor — proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.",
|
|
5
|
+
"author": { "name": "Mysten Labs" },
|
|
6
|
+
"homepage": "https://memory.walrus.xyz",
|
|
7
|
+
"repository": "https://github.com/MystenLabs/MemWal",
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"keywords": ["memory", "mcp", "walrus", "semantic-search"],
|
|
10
|
+
"hooks": "./hooks/cursor-hooks.json",
|
|
11
|
+
"mcpServers": ".cursor-mcp.json"
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|compact",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/on_session_start.mjs\"",
|
|
10
|
+
"statusMessage": "Loading Walrus Memory context...",
|
|
11
|
+
"timeout": 10
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"UserPromptSubmit": [
|
|
17
|
+
{
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/on_user_prompt.mjs\"",
|
|
22
|
+
"statusMessage": "Checking memory relevance...",
|
|
23
|
+
"timeout": 12
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"PostToolUse": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/on_post_tool.mjs\"",
|
|
35
|
+
"timeout": 12
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"command": "node \"${CURSOR_PLUGIN_ROOT}/scripts/on_session_start.mjs\"",
|
|
7
|
+
"matcher": "startup|resume|compact"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"beforeSubmitPrompt": [
|
|
11
|
+
{
|
|
12
|
+
"command": "node \"${CURSOR_PLUGIN_ROOT}/scripts/on_user_prompt.mjs\"",
|
|
13
|
+
"timeout": 8
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"postToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"command": "node \"${CURSOR_PLUGIN_ROOT}/scripts/on_post_tool.mjs\"",
|
|
19
|
+
"matcher": "Bash",
|
|
20
|
+
"timeout": 5
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|compact",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/on_session_start.mjs\"",
|
|
10
|
+
"statusMessage": "Loading Walrus Memory context...",
|
|
11
|
+
"timeout": 8
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"UserPromptSubmit": [
|
|
17
|
+
{
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/on_user_prompt.mjs\"",
|
|
22
|
+
"statusMessage": "Checking memory relevance...",
|
|
23
|
+
"timeout": 8
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"PostToolUse": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/on_post_tool.mjs\"",
|
|
35
|
+
"timeout": 5
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"name": "memwal-session-start",
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node \"${extensionPath}/scripts/on_session_start.mjs\" 2>/dev/null || true"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"UserPromptSubmit": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"name": "memwal-user-prompt",
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"${extensionPath}/scripts/on_user_prompt.mjs\" 2>/dev/null || true",
|
|
22
|
+
"timeout": 8
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"PostToolUse": [
|
|
28
|
+
{
|
|
29
|
+
"matcher": "Bash",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"name": "memwal-post-tool",
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node \"${extensionPath}/scripts/on_post_tool.mjs\" 2>/dev/null || true",
|
|
35
|
+
"timeout": 5
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "memwal",
|
|
3
|
+
"name": "memwal",
|
|
4
|
+
"version": "0.0.5",
|
|
5
|
+
"description": "Automatic Walrus Memory for Antigravity — proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.",
|
|
6
|
+
"author": { "name": "Mysten Labs" },
|
|
7
|
+
"homepage": "https://memory.walrus.xyz",
|
|
8
|
+
"repository": "https://github.com/MystenLabs/MemWal",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"keywords": ["memory", "mcp", "walrus", "sui", "semantic-search"],
|
|
11
|
+
"contextFileName": "AGENTS.md"
|
|
12
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install MemWal lifecycle hooks into ~/.codex/hooks.json and register the
|
|
4
|
+
* MemWal MCP server in ~/.codex/config.toml.
|
|
5
|
+
*
|
|
6
|
+
* Codex discovers hooks only at ~/.codex/hooks.json (or <repo>/.codex/hooks.json)
|
|
7
|
+
* and has no plugin host that auto-wires hooks. This installer reads the
|
|
8
|
+
* template at hooks/codex-hooks.json, rewrites the ${PLUGIN_ROOT} placeholder
|
|
9
|
+
* to this plugin's absolute path, and merges the entries into ~/.codex/hooks.json.
|
|
10
|
+
*
|
|
11
|
+
* Re-running is idempotent: entries this installer owns (identified by our
|
|
12
|
+
* hook script filenames) are removed before fresh entries are added.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node install_codex_hooks.mjs # install or update
|
|
16
|
+
* node install_codex_hooks.mjs --uninstall # remove MemWal hook entries
|
|
17
|
+
*
|
|
18
|
+
* After installing, enable the Codex hooks feature flag in ~/.codex/config.toml:
|
|
19
|
+
*
|
|
20
|
+
* [features]
|
|
21
|
+
* codex_hooks = true
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { join, dirname } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const PLUGIN_ROOT = dirname(SCRIPT_DIR);
|
|
30
|
+
|
|
31
|
+
const CODEX_DIR = join(homedir(), ".codex");
|
|
32
|
+
const HOOKS_FILE = join(CODEX_DIR, "hooks.json");
|
|
33
|
+
const CONFIG_FILE = join(CODEX_DIR, "config.toml");
|
|
34
|
+
const TEMPLATE_FILE = join(PLUGIN_ROOT, "hooks", "codex-hooks.json");
|
|
35
|
+
|
|
36
|
+
// Entries are "ours" when a hook command references one of our scripts.
|
|
37
|
+
const OWNER_MARKERS = [
|
|
38
|
+
"on_session_start.mjs",
|
|
39
|
+
"on_user_prompt.mjs",
|
|
40
|
+
"on_post_tool.mjs",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function loadTemplate() {
|
|
44
|
+
const raw = readFileSync(TEMPLATE_FILE, "utf8").replaceAll(
|
|
45
|
+
"${PLUGIN_ROOT}",
|
|
46
|
+
PLUGIN_ROOT
|
|
47
|
+
);
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadExisting() {
|
|
52
|
+
if (!existsSync(HOOKS_FILE)) return { hooks: {} };
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(HOOKS_FILE, "utf8"));
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(`error: failed to read ${HOOKS_FILE}: ${e.message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isOwned(entry) {
|
|
62
|
+
for (const hook of entry.hooks || []) {
|
|
63
|
+
const cmd = hook.command || "";
|
|
64
|
+
if (OWNER_MARKERS.some((m) => cmd.includes(m))) return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stripOwned(config) {
|
|
70
|
+
const hooks = config.hooks || {};
|
|
71
|
+
for (const event of Object.keys(hooks)) {
|
|
72
|
+
hooks[event] = (hooks[event] || []).filter((e) => !isOwned(e));
|
|
73
|
+
if (hooks[event].length === 0) delete hooks[event];
|
|
74
|
+
}
|
|
75
|
+
config.hooks = hooks;
|
|
76
|
+
return config;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mergeTemplate(config, template) {
|
|
80
|
+
config.hooks = config.hooks || {};
|
|
81
|
+
for (const [event, entries] of Object.entries(template.hooks || {})) {
|
|
82
|
+
config.hooks[event] = (config.hooks[event] || []).concat(entries);
|
|
83
|
+
}
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writeHooks(config) {
|
|
88
|
+
mkdirSync(CODEX_DIR, { recursive: true });
|
|
89
|
+
writeFileSync(HOOKS_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Append [mcp_servers.memwal] to config.toml if it isn't registered yet. */
|
|
93
|
+
function ensureMcpRegistered() {
|
|
94
|
+
mkdirSync(CODEX_DIR, { recursive: true });
|
|
95
|
+
let content = existsSync(CONFIG_FILE) ? readFileSync(CONFIG_FILE, "utf8") : "";
|
|
96
|
+
if (content.includes("[mcp_servers.memwal]")) return false;
|
|
97
|
+
const block =
|
|
98
|
+
"\n[mcp_servers.memwal]\n" +
|
|
99
|
+
'command = "npx"\n' +
|
|
100
|
+
'args = ["-y", "@mysten-incubation/memwal-mcp"]\n';
|
|
101
|
+
writeFileSync(CONFIG_FILE, (content.trimEnd() + "\n" + block).trimStart());
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function featureFlagEnabled() {
|
|
106
|
+
if (!existsSync(CONFIG_FILE)) return false;
|
|
107
|
+
return readFileSync(CONFIG_FILE, "utf8")
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map((l) => l.split("#", 1)[0].replace(/\s/g, ""))
|
|
110
|
+
.includes("codex_hooks=true");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printFeatureFlagHint() {
|
|
114
|
+
console.log("");
|
|
115
|
+
console.log("Codex hooks feature flag is not enabled.");
|
|
116
|
+
console.log(`Add this to ${CONFIG_FILE}:`);
|
|
117
|
+
console.log("");
|
|
118
|
+
console.log(" [features]");
|
|
119
|
+
console.log(" codex_hooks = true");
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log("Then restart Codex.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function main() {
|
|
125
|
+
const uninstall = process.argv.includes("--uninstall");
|
|
126
|
+
let config = loadExisting();
|
|
127
|
+
|
|
128
|
+
if (uninstall) {
|
|
129
|
+
config = stripOwned(config);
|
|
130
|
+
writeHooks(config);
|
|
131
|
+
console.log(`Removed MemWal hooks from ${HOOKS_FILE}`);
|
|
132
|
+
console.log(
|
|
133
|
+
"(The [mcp_servers.memwal] entry in config.toml was left in place — remove it manually if you no longer want the tools.)"
|
|
134
|
+
);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!existsSync(TEMPLATE_FILE)) {
|
|
139
|
+
console.error(`error: template not found at ${TEMPLATE_FILE}`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const template = loadTemplate();
|
|
144
|
+
config = stripOwned(config);
|
|
145
|
+
config = mergeTemplate(config, template);
|
|
146
|
+
writeHooks(config);
|
|
147
|
+
|
|
148
|
+
const mcpAdded = ensureMcpRegistered();
|
|
149
|
+
|
|
150
|
+
console.log(`Installed MemWal hooks into ${HOOKS_FILE}`);
|
|
151
|
+
console.log(`Plugin path: ${PLUGIN_ROOT}`);
|
|
152
|
+
console.log("Events: SessionStart, UserPromptSubmit, PostToolUse");
|
|
153
|
+
console.log(
|
|
154
|
+
mcpAdded
|
|
155
|
+
? `Registered [mcp_servers.memwal] in ${CONFIG_FILE}`
|
|
156
|
+
: `[mcp_servers.memwal] already present in ${CONFIG_FILE}`
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!featureFlagEnabled()) printFeatureFlagHint();
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
process.exit(main());
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared I/O + throttle helpers for MemWal Claude Code / Codex hooks.
|
|
3
|
+
*
|
|
4
|
+
* Pure Node, no dependencies, no network. Every hook reads one JSON object
|
|
5
|
+
* from stdin, optionally emits a `hookSpecificOutput` directive on stdout,
|
|
6
|
+
* and always exits 0 — a hook must never block the session.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
/** Read + parse the hook input JSON from stdin. Returns {} on any error. */
|
|
13
|
+
export function readStdin() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(0, "utf8");
|
|
16
|
+
return raw ? JSON.parse(raw) : {};
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Emit a hook directive. No-op when `additionalContext` is empty. */
|
|
23
|
+
export function emitContext(hookEventName, additionalContext) {
|
|
24
|
+
if (!additionalContext) return;
|
|
25
|
+
process.stdout.write(
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
hookSpecificOutput: { hookEventName, additionalContext },
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STATE_DIR = join(process.env.TMPDIR || tmpdir(), "memwal-hooks");
|
|
33
|
+
|
|
34
|
+
function ensureDir() {
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
37
|
+
} catch {
|
|
38
|
+
/* best effort */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safe(s) {
|
|
43
|
+
return String(s || "default")
|
|
44
|
+
.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
45
|
+
.slice(0, 64);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true the FIRST time it is called for a (name, session) pair, then
|
|
50
|
+
* false thereafter — used to inject a rubric or banner only once per session.
|
|
51
|
+
*/
|
|
52
|
+
export function firstTime(name, sessionId) {
|
|
53
|
+
ensureDir();
|
|
54
|
+
const f = join(STATE_DIR, `${safe(name)}_${safe(sessionId)}`);
|
|
55
|
+
if (existsSync(f)) return false;
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(f, "1");
|
|
58
|
+
} catch {
|
|
59
|
+
/* best effort */
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Increment and return a per-(name, session) counter. */
|
|
65
|
+
export function bumpCounter(name, sessionId) {
|
|
66
|
+
ensureDir();
|
|
67
|
+
const f = join(STATE_DIR, `count_${safe(name)}_${safe(sessionId)}`);
|
|
68
|
+
let n = 0;
|
|
69
|
+
try {
|
|
70
|
+
n = parseInt(readFileSync(f, "utf8"), 10) || 0;
|
|
71
|
+
} catch {
|
|
72
|
+
/* missing -> 0 */
|
|
73
|
+
}
|
|
74
|
+
n += 1;
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(f, String(n));
|
|
77
|
+
} catch {
|
|
78
|
+
/* best effort */
|
|
79
|
+
}
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic signal detection over user input / tool output. No network, no
|
|
3
|
+
* LLM — cheap regexes only. The agent (guided by the now-agentic tool
|
|
4
|
+
* descriptions) makes the final call; these just decide when to *remind* it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// User is referencing earlier context / asking to resume.
|
|
8
|
+
const RECALL_INTENT =
|
|
9
|
+
/\b(remember when|last time|previously|earlier|where (did|were) we|pick up where|catch me up|as i (said|mentioned|told you)|like i said|we (already|previously) (did|decided|discussed|set up)|my (usual|preferred|preference|setup|config|stack))\b/i;
|
|
10
|
+
|
|
11
|
+
// User is stating a durable fact / preference / correction worth saving.
|
|
12
|
+
const REMEMBER_INTENT =
|
|
13
|
+
/\b(remember (this|that)|don'?t forget|note that|for future reference|keep in mind|from now on|going forward|i (prefer|like|love|hate|always|never|usually|use|work with|am|m)\b|my name is|call me|i'?m (using|on)|we (use|prefer|standardi[sz]e on))\b/i;
|
|
14
|
+
|
|
15
|
+
// Strong error markers in command output.
|
|
16
|
+
// No trailing \b: markers ending in `:` (panic:, fatal:) or `]` (error[E0432])
|
|
17
|
+
// are followed by a non-word char, so a closing \b would never match them.
|
|
18
|
+
const ERROR_STRONG =
|
|
19
|
+
/\b(traceback \(most recent|panic:|fatal:|segmentation fault|unhandled (exception|rejection)|error\[[A-Z]?\d+\]|cannot find module|command not found|permission denied|no such file)/i;
|
|
20
|
+
const ERROR_PAIR = /(^|\s)(error:|exception:|failed:|fail:|err!)/gi;
|
|
21
|
+
|
|
22
|
+
/** True when the user's message references past context worth recalling. */
|
|
23
|
+
export function detectRecall(text) {
|
|
24
|
+
return !!text && RECALL_INTENT.test(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** True when the user's message states a durable fact worth remembering. */
|
|
28
|
+
export function detectRemember(text) {
|
|
29
|
+
return !!text && REMEMBER_INTENT.test(text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** True when command output looks like an error/traceback. */
|
|
33
|
+
export function detectError(text) {
|
|
34
|
+
if (!text) return false;
|
|
35
|
+
if (ERROR_STRONG.test(text)) return true;
|
|
36
|
+
const m = text.match(ERROR_PAIR);
|
|
37
|
+
return !!m && m.length >= 2;
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse hook (Bash) — when a command's output looks like an error,
|
|
3
|
+
* remind the agent it can recall prior fixes and save the resolution.
|
|
4
|
+
*
|
|
5
|
+
* Heuristic only: no fetch, no network. Always exits 0.
|
|
6
|
+
*/
|
|
7
|
+
import { readStdin, emitContext } from "./lib/hook-io.mjs";
|
|
8
|
+
import { detectError } from "./lib/signals.mjs";
|
|
9
|
+
|
|
10
|
+
const input = readStdin();
|
|
11
|
+
const output = extractToolOutput(input);
|
|
12
|
+
|
|
13
|
+
// Ignore short / empty output — not enough to reason about.
|
|
14
|
+
if (!output || output.length < 50) process.exit(0);
|
|
15
|
+
|
|
16
|
+
if (detectError(output)) {
|
|
17
|
+
emitContext(
|
|
18
|
+
"PostToolUse",
|
|
19
|
+
"That command produced an error. Consider calling memwal_recall to check for a prior fix to a similar error; once you resolve it, save the fix with memwal_remember so it's available next time."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
process.exit(0);
|
|
24
|
+
|
|
25
|
+
/** Best-effort extraction of a tool's textual output across hook-input shapes. */
|
|
26
|
+
function extractToolOutput(input) {
|
|
27
|
+
const r =
|
|
28
|
+
input.tool_response ??
|
|
29
|
+
input.tool_result ??
|
|
30
|
+
input.response ??
|
|
31
|
+
input.output ??
|
|
32
|
+
"";
|
|
33
|
+
if (typeof r === "string") return r;
|
|
34
|
+
if (r && typeof r === "object") {
|
|
35
|
+
if (typeof r.stdout === "string" || typeof r.stderr === "string") {
|
|
36
|
+
return `${r.stdout || ""}\n${r.stderr || ""}`;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(r);
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStart hook — announce that Walrus Memory is active and remind the
|
|
3
|
+
* agent how/when to use it. No fetch, no network.
|
|
4
|
+
*/
|
|
5
|
+
import { readStdin, emitContext } from "./lib/hook-io.mjs";
|
|
6
|
+
|
|
7
|
+
readStdin(); // drain stdin; we don't need any field today
|
|
8
|
+
const ns = process.env.MEMWAL_NAMESPACE || "default";
|
|
9
|
+
|
|
10
|
+
const context = [
|
|
11
|
+
`Walrus Memory is this user's memory system, exposed via the memwal_* tools (namespace: ${ns}).`,
|
|
12
|
+
"Use it as the PRIMARY place to store and recall durable facts — prefer the memwal_* tools over any built-in or local memory feature, so the user's memory stays portable and persistent on Walrus.",
|
|
13
|
+
"When you learn a durable fact (preference, decision, constraint, correction, identity), save it with memwal_remember (or memwal_remember_bulk for several at once); before tasks that reference past work or the user's preferences, recall with memwal_recall.",
|
|
14
|
+
"If memwal_recall unexpectedly returns nothing for a namespace you've used before, run memwal_restore to rebuild the index from Walrus.",
|
|
15
|
+
].join(" ");
|
|
16
|
+
|
|
17
|
+
emitContext("SessionStart", context);
|
|
18
|
+
process.exit(0);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserPromptSubmit hook — decide, from the user's input, whether the agent
|
|
3
|
+
* should recall context or save a fact, and inject a short directive saying so.
|
|
4
|
+
*
|
|
5
|
+
* Heuristic only: no fetch, no network. The agent does the actual tool call.
|
|
6
|
+
*/
|
|
7
|
+
import { readStdin, emitContext, firstTime } from "./lib/hook-io.mjs";
|
|
8
|
+
import { detectRecall, detectRemember } from "./lib/signals.mjs";
|
|
9
|
+
|
|
10
|
+
const input = readStdin();
|
|
11
|
+
const prompt = (input.prompt || "").toString();
|
|
12
|
+
const sessionId = input.session_id || "default";
|
|
13
|
+
|
|
14
|
+
// Skip very short prompts (acks, single words) — nothing useful to act on.
|
|
15
|
+
if (prompt.trim().length < 20) process.exit(0);
|
|
16
|
+
|
|
17
|
+
const parts = [];
|
|
18
|
+
|
|
19
|
+
if (detectRecall(prompt)) {
|
|
20
|
+
parts.push(
|
|
21
|
+
"The user is referencing earlier work or context. Before answering, call memwal_recall (Walrus Memory) with a focused query — use the memwal_* tools, not any built-in memory, so recall stays portable."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (detectRemember(prompt)) {
|
|
26
|
+
parts.push(
|
|
27
|
+
"The user just stated a durable fact or preference. Save it with memwal_remember (or memwal_remember_bulk for several distinct facts) — prefer the memwal_* tools over any built-in memory so the fact persists on Walrus across sessions and agents."
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// When no strong signal fired, inject the general rubric once per session so
|
|
32
|
+
// the agent knows the tools exist, when to reach for them, and to prefer them
|
|
33
|
+
// over any built-in memory feature.
|
|
34
|
+
if (parts.length === 0 && firstTime("rubric", sessionId)) {
|
|
35
|
+
parts.push(
|
|
36
|
+
"Walrus Memory (the memwal_* tools) is this user's primary memory system — prefer it over any built-in memory. Call memwal_recall proactively when the user references past work, decisions, or preferences; save durable facts with memwal_remember (or memwal_remember_bulk for several) as you learn them — you don't need to be asked."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
emitContext("UserPromptSubmit", parts.join(" "));
|
|
41
|
+
process.exit(0);
|