hippocamp 0.1.0 → 0.2.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.
@@ -8,8 +8,10 @@ const COMMANDS = {
8
8
  mcp: "hippocamp-mcp.cjs",
9
9
  "install-codex": "install-codex.cjs",
10
10
  "install-claude": "install-claude.cjs",
11
+ "install-grok": "install-grok.cjs",
11
12
  "upgrade-codex": "install-codex.cjs",
12
13
  "upgrade-claude": "install-claude.cjs",
14
+ "upgrade-grok": "install-grok.cjs",
13
15
  };
14
16
 
15
17
  function printHelp() {
@@ -20,16 +22,20 @@ Usage:
20
22
  hippocamp mcp
21
23
  hippocamp install-codex
22
24
  hippocamp install-claude
25
+ hippocamp install-grok
23
26
  hippocamp upgrade-codex
24
27
  hippocamp upgrade-claude
28
+ hippocamp upgrade-grok
25
29
 
26
30
  Commands:
27
31
  dream Run offline Dream memory compaction
28
32
  mcp Run the local MCP server
29
33
  install-codex Install the Hippocamp skill and MCP server into Codex
30
34
  install-claude Install the Hippocamp skill and MCP server into Claude Code
35
+ install-grok Install the Hippocamp skill and MCP server into Grok Build
31
36
  upgrade-codex Upgrade the Codex Hippocamp install
32
37
  upgrade-claude Upgrade the Claude Code Hippocamp install
38
+ upgrade-grok Upgrade the Grok Build Hippocamp install
33
39
  `);
34
40
  }
35
41
 
@@ -105,11 +105,14 @@ function buildClaudeManagedBlock() {
105
105
  "- For every new top-level coding task, call the Hippocamp `wake_up` MCP tool before inspecting repository files, planning, or editing.",
106
106
  "- Read the returned global and project memory before using other memory tools.",
107
107
  "- Do this even when the user does not explicitly mention memory.",
108
- "- Only call `search_memory` if the wake-up files are insufficient.",
108
+ "- Use `search_memory` as the normal path for task-specific recall after wake-up.",
109
+ "- If `search_memory` returns no result, continue without memory instead of reading whole event logs as a fallback.",
109
110
  "- If `wake_up` is unavailable or fails, say so explicitly and continue without memory instead of silently skipping it.",
110
111
  "- After meaningful code or content changes, checkpoint project memory before the final response.",
111
112
  "- When project state changes, use `append_event` for milestones and update curated files such as `current_state.md` and `open_threads.md` before finishing the task.",
112
- "- When appending events, include concise `Cues:` values or pass the `cues` argument so fuzzy recall can find the event later.",
113
+ "- Do not re-read `current_state.md` or `open_threads.md` after `wake_up`; they are already loaded.",
114
+ "- Update curated files only when project state actually changed. For small tasks, `append_event` is enough.",
115
+ "- When appending events, pass a `title` and 1-8 concise `cues` so fuzzy recall can find the event later. Never write under `events/` with `write_memory_file`.",
113
116
  "- Do not duplicate commits, PRs, issues, reviews, or CI results in memory; store references plus the missing rationale, preference, assumption, or follow-up context.",
114
117
  "- If a project-memory write does not sync automatically, call `sync_memory` before finishing the task.",
115
118
  "- If the user asks to upgrade Hippocamp for Claude Code, run `npx hippocamp@latest upgrade-claude`; from a source checkout, run `git pull --ff-only`, `npm install`, then `npm run upgrade:claude`.",
@@ -213,6 +216,8 @@ async function ensureServerInstalled({ repoRoot, globalRoot, serverName }) {
213
216
  serverName,
214
217
  "--env",
215
218
  `HIPPOCAMP_GLOBAL_ROOT=${globalRoot}`,
219
+ "--env",
220
+ "HIPPOCAMP_AGENT=claude",
216
221
  "--",
217
222
  process.execPath,
218
223
  scriptPath,
@@ -113,11 +113,14 @@ function buildCodexManagedBlock() {
113
113
  "- For every new top-level coding task, call the Hippocamp `wake_up` MCP tool before inspecting repository files, planning, or editing.",
114
114
  "- Read the returned global and project memory before using other memory tools.",
115
115
  "- Do this even when the user does not explicitly mention memory.",
116
- "- Only call `search_memory` if the wake-up files are insufficient.",
116
+ "- Use `search_memory` as the normal path for task-specific recall after wake-up.",
117
+ "- If `search_memory` returns no result, continue without memory instead of reading whole event logs as a fallback.",
117
118
  "- If `wake_up` is unavailable or fails, say so explicitly and continue without memory instead of silently skipping it.",
118
119
  "- After meaningful code or content changes, checkpoint project memory before the final response.",
119
120
  "- When project state changes, use `append_event` for milestones and update curated files such as `current_state.md` and `open_threads.md` before finishing the task.",
120
- "- When appending events, include concise `Cues:` values or pass the `cues` argument so fuzzy recall can find the event later.",
121
+ "- Do not re-read `current_state.md` or `open_threads.md` after `wake_up`; they are already loaded.",
122
+ "- Update curated files only when project state actually changed. For small tasks, `append_event` is enough.",
123
+ "- When appending events, pass a `title` and 1-8 concise `cues` so fuzzy recall can find the event later. Never write under `events/` with `write_memory_file`.",
121
124
  "- Do not duplicate commits, PRs, issues, reviews, or CI results in memory; store references plus the missing rationale, preference, assumption, or follow-up context.",
122
125
  "- If a project-memory write does not sync automatically, call `sync_memory` before finishing the task.",
123
126
  "- If the user asks to upgrade Hippocamp for Codex, run `npx hippocamp@latest upgrade-codex`; from a source checkout, run `git pull --ff-only`, `npm install`, then `npm run upgrade:codex`.",
@@ -178,6 +181,8 @@ async function ensureServerInstalled({ repoRoot, globalRoot, serverName }) {
178
181
  serverName,
179
182
  "--env",
180
183
  `HIPPOCAMP_GLOBAL_ROOT=${globalRoot}`,
184
+ "--env",
185
+ "HIPPOCAMP_AGENT=codex",
181
186
  "--",
182
187
  process.execPath,
183
188
  scriptPath,
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs/promises");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { execFile } = require("node:child_process");
7
+ const { promisify } = require("node:util");
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const GROK_MANAGED_START = "<!-- hippocamp:managed:start -->";
11
+ const GROK_MANAGED_END = "<!-- hippocamp:managed:end -->";
12
+ const HIPPOCAMP_SKILL_NAME = "hippocamp-memory";
13
+
14
+ function expandHomePath(value) {
15
+ if (!value) {
16
+ return value;
17
+ }
18
+
19
+ if (value === "~") {
20
+ return os.homedir();
21
+ }
22
+
23
+ if (value.startsWith("~/")) {
24
+ return path.join(os.homedir(), value.slice(2));
25
+ }
26
+
27
+ return value;
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const options = {
32
+ grokHome: expandHomePath(process.env.GROK_HOME || "~/.grok"),
33
+ globalRoot: expandHomePath(process.env.HIPPOCAMP_GLOBAL_ROOT || "~/.lagoon"),
34
+ serverName: "hippocamp",
35
+ };
36
+
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const token = argv[index];
39
+
40
+ if (token === "--grok-home") {
41
+ options.grokHome = path.resolve(expandHomePath(argv[index + 1]));
42
+ index += 1;
43
+ continue;
44
+ }
45
+
46
+ if (token === "--global-root") {
47
+ options.globalRoot = path.resolve(expandHomePath(argv[index + 1]));
48
+ index += 1;
49
+ continue;
50
+ }
51
+
52
+ if (token === "--server-name") {
53
+ options.serverName = argv[index + 1];
54
+ index += 1;
55
+ continue;
56
+ }
57
+
58
+ if (token === "--help") {
59
+ printHelp();
60
+ process.exit(0);
61
+ }
62
+
63
+ throw new Error(`Unknown argument: ${token}`);
64
+ }
65
+
66
+ return options;
67
+ }
68
+
69
+ function printHelp() {
70
+ console.log(`Install Hippocamp into Grok Build.
71
+
72
+ Usage:
73
+ node scripts/install-grok.cjs [--grok-home PATH] [--global-root PATH] [--server-name NAME]
74
+
75
+ Defaults:
76
+ --grok-home ~/.grok
77
+ --global-root ~/.lagoon
78
+ --server-name hippocamp
79
+ `);
80
+ }
81
+
82
+ async function runGrok(args) {
83
+ try {
84
+ const result = await execFileAsync("grok", args);
85
+ return {
86
+ ok: true,
87
+ stdout: result.stdout.trim(),
88
+ stderr: result.stderr.trim(),
89
+ };
90
+ } catch (error) {
91
+ return {
92
+ ok: false,
93
+ stdout: error.stdout?.trim() || "",
94
+ stderr: error.stderr?.trim() || error.message,
95
+ };
96
+ }
97
+ }
98
+
99
+ async function ensureSkillInstalled({ repoRoot, grokHome }) {
100
+ const sourceDir = path.join(repoRoot, "skills", HIPPOCAMP_SKILL_NAME);
101
+ const targetDir = path.join(grokHome, "skills", HIPPOCAMP_SKILL_NAME);
102
+
103
+ await fs.mkdir(targetDir, { recursive: true });
104
+ await fs.copyFile(path.join(sourceDir, "SKILL.md"), path.join(targetDir, "SKILL.md"));
105
+
106
+ return targetDir;
107
+ }
108
+
109
+ function buildGrokManagedBlock() {
110
+ return [
111
+ GROK_MANAGED_START,
112
+ "# Hippocamp",
113
+ "",
114
+ "- For every new top-level coding task, call the Hippocamp `wake_up` MCP tool before inspecting repository files, planning, or editing.",
115
+ "- Read the returned global and project memory before using other memory tools.",
116
+ "- Do this even when the user does not explicitly mention memory.",
117
+ "- Use `search_memory` as the normal path for task-specific recall after wake-up.",
118
+ "- If `search_memory` returns no result, continue without memory instead of reading whole event logs as a fallback.",
119
+ "- If `wake_up` is unavailable or fails, say so explicitly and continue without memory instead of silently skipping it.",
120
+ "- After meaningful code or content changes, checkpoint project memory before the final response.",
121
+ "- When project state changes, use `append_event` for milestones and update curated files such as `current_state.md` and `open_threads.md` before finishing the task.",
122
+ "- Do not re-read `current_state.md` or `open_threads.md` after `wake_up`; they are already loaded.",
123
+ "- Update curated files only when project state actually changed. For small tasks, `append_event` is enough.",
124
+ "- When appending events, pass a `title` and 1-8 concise `cues` so fuzzy recall can find the event later. Never write under `events/` with `write_memory_file`.",
125
+ "- Do not duplicate commits, PRs, issues, reviews, or CI results in memory; store references plus the missing rationale, preference, assumption, or follow-up context.",
126
+ "- If a project-memory write does not sync automatically, call `sync_memory` before finishing the task.",
127
+ "- If the user asks to upgrade Hippocamp for Grok Build, run `npx hippocamp@latest upgrade-grok`; from a source checkout, run `git pull --ff-only`, `npm install`, then `npm run upgrade:grok`.",
128
+ GROK_MANAGED_END,
129
+ "",
130
+ ].join("\n");
131
+ }
132
+
133
+ function upsertManagedBlock(existingContent, block) {
134
+ if (!existingContent) {
135
+ return block;
136
+ }
137
+
138
+ const startIndex = existingContent.indexOf(GROK_MANAGED_START);
139
+ const endIndex = existingContent.indexOf(GROK_MANAGED_END);
140
+
141
+ if (startIndex >= 0 && endIndex >= 0 && endIndex >= startIndex) {
142
+ const before = existingContent.slice(0, startIndex).trimEnd();
143
+ const after = existingContent.slice(endIndex + GROK_MANAGED_END.length).trimStart();
144
+ return [before, block.trimEnd(), after].filter(Boolean).join("\n\n").concat("\n");
145
+ }
146
+
147
+ return `${existingContent.trimEnd()}\n\n${block}`;
148
+ }
149
+
150
+ async function ensureGrokInstructionsInstalled({ grokHome }) {
151
+ const rulesDir = path.join(grokHome, "rules");
152
+ const rulesPath = path.join(rulesDir, "hippocamp.md");
153
+ const existingContent = await fs.readFile(rulesPath, "utf8").catch((error) => {
154
+ if (error && typeof error === "object" && error.code === "ENOENT") {
155
+ return "";
156
+ }
157
+
158
+ throw error;
159
+ });
160
+ const nextContent = upsertManagedBlock(existingContent, buildGrokManagedBlock());
161
+
162
+ await fs.mkdir(rulesDir, { recursive: true });
163
+ await fs.writeFile(rulesPath, nextContent, "utf8");
164
+
165
+ return rulesPath;
166
+ }
167
+
168
+ async function ensureServerInstalled({ repoRoot, globalRoot, serverName }) {
169
+ const existing = await runGrok(["mcp", "list"]);
170
+
171
+ if (!existing.ok && /not found|command not found|ENOENT/i.test(existing.stderr)) {
172
+ throw new Error(
173
+ existing.stderr ||
174
+ "grok CLI not found. Install Grok Build and ensure `grok` is on PATH, then retry.",
175
+ );
176
+ }
177
+
178
+ const listed = `${existing.stdout}\n${existing.stderr}`;
179
+ const hasServer =
180
+ existing.ok &&
181
+ listed
182
+ .split(/\r?\n/)
183
+ .some((line) => line.trim().startsWith(`${serverName}:`) || line.trim() === serverName);
184
+
185
+ if (hasServer) {
186
+ const removed = await runGrok(["mcp", "remove", serverName]);
187
+
188
+ if (!removed.ok && !/no mcp server named/i.test(removed.stderr || "")) {
189
+ throw new Error(removed.stderr || `Failed to remove existing MCP server: ${serverName}`);
190
+ }
191
+ }
192
+
193
+ const scriptPath = path.join(repoRoot, "scripts", "hippocamp-mcp.cjs");
194
+ const added = await runGrok([
195
+ "mcp",
196
+ "add",
197
+ serverName,
198
+ "-e",
199
+ `HIPPOCAMP_GLOBAL_ROOT=${globalRoot}`,
200
+ "-e",
201
+ "HIPPOCAMP_AGENT=grok",
202
+ "--",
203
+ process.execPath,
204
+ scriptPath,
205
+ ]);
206
+
207
+ if (!added.ok) {
208
+ throw new Error(added.stderr || `Failed to add MCP server: ${serverName}`);
209
+ }
210
+
211
+ return {
212
+ name: serverName,
213
+ command: process.execPath,
214
+ scriptPath,
215
+ globalRoot,
216
+ agent: "grok",
217
+ summary: added.stdout || added.stderr,
218
+ };
219
+ }
220
+
221
+ async function main() {
222
+ const options = parseArgs(process.argv.slice(2));
223
+ const repoRoot = path.resolve(__dirname, "..");
224
+
225
+ await fs.mkdir(options.grokHome, { recursive: true });
226
+
227
+ const skillPath = await ensureSkillInstalled({
228
+ repoRoot,
229
+ grokHome: options.grokHome,
230
+ });
231
+ const grokInstructionsPath = await ensureGrokInstructionsInstalled({
232
+ grokHome: options.grokHome,
233
+ });
234
+
235
+ const server = await ensureServerInstalled({
236
+ repoRoot,
237
+ globalRoot: options.globalRoot,
238
+ serverName: options.serverName,
239
+ });
240
+
241
+ console.log(
242
+ JSON.stringify(
243
+ {
244
+ ok: true,
245
+ grokHome: options.grokHome,
246
+ skillPath,
247
+ grokInstructionsPath,
248
+ server,
249
+ note: "Restart Grok Build (or start a new session) to load the installed skill, MCP server, and home rules.",
250
+ },
251
+ null,
252
+ 2,
253
+ ),
254
+ );
255
+ }
256
+
257
+ main().catch((error) => {
258
+ console.error("Failed to install Hippocamp into Grok Build:", error.message);
259
+ process.exit(1);
260
+ });
@@ -7,10 +7,11 @@ description: Use Hippocamp memory to load global identity and workflow context f
7
7
 
8
8
  Use this skill when the user wants persistent memory across agent sessions.
9
9
 
10
- This same skill can be installed into Codex or Claude Code.
10
+ This same skill can be installed into Codex, Claude Code, or Grok Build.
11
11
  For Codex, use `npm run install:codex`; that installer copies this skill into `~/.codex/skills/hippocamp-memory/`, registers the Hippocamp MCP server, and refreshes `~/.codex/AGENTS.md`.
12
12
  For Claude Code, use `npm run install:claude`; that installer copies this skill into `~/.claude/skills/hippocamp-memory/`, registers the Hippocamp MCP server, and refreshes `~/.claude/CLAUDE.md`.
13
- Upgrade uses the same path: run `npm run upgrade:codex` or `npm run upgrade:claude` from a source checkout, or `npx hippocamp@latest upgrade-codex` / `npx hippocamp@latest upgrade-claude` after publishing.
13
+ For Grok Build, use `npm run install:grok`; that installer copies this skill into `~/.grok/skills/hippocamp-memory/`, registers the Hippocamp MCP server with `HIPPOCAMP_AGENT=grok`, and refreshes `~/.grok/rules/hippocamp.md`.
14
+ Upgrade uses the same path: run `npm run upgrade:codex`, `npm run upgrade:claude`, or `npm run upgrade:grok` from a source checkout, or `npx hippocamp@latest upgrade-codex` / `upgrade-claude` / `upgrade-grok` after publishing.
14
15
 
15
16
  ## Scope
16
17
 
@@ -23,12 +24,13 @@ Upgrade uses the same path: run `npm run upgrade:codex` or `npm run upgrade:clau
23
24
 
24
25
  1. At the start of every top-level coding task, call `wake_up` before inspecting files, planning, or editing.
25
26
  2. Read the wake-up output before searching.
26
- 3. Only call `search_memory` if the wake-up files are insufficient.
27
- 4. During work, use `append_event` only for meaningful milestones.
28
- 5. When appending events, include concise `Cues:` values or pass the `cues` argument so fuzzy recall can find the event later.
29
- 6. `write_memory_file` and `append_event` sync by default.
30
- 7. If a sync is skipped or fails, call `sync_memory` explicitly.
31
- 8. At the end of the task, use `write_memory_file` to update curated files such as:
27
+ 3. Use `search_memory` as the normal path for task-specific recall after wake-up.
28
+ 4. If search returns no result, continue without memory. Do not read whole event logs as a fallback.
29
+ 5. During work, use `append_event` only for meaningful milestones.
30
+ 6. When appending events, pass a `title` and 1-8 concise `cues` so fuzzy recall can find the event later. Never write under `events/` with `write_memory_file`.
31
+ 7. `write_memory_file` and `append_event` sync by default.
32
+ 8. If a sync is skipped or fails, call `sync_memory` explicitly.
33
+ 9. At the end of the task, if project state actually changed, use `write_memory_file` to update curated files such as:
32
34
  - `current_state.md`
33
35
  - `open_threads.md`
34
36
  - `identity.md`
@@ -39,8 +41,13 @@ Upgrade uses the same path: run `npm run upgrade:codex` or `npm run upgrade:clau
39
41
 
40
42
  - Do not full-scan memory on every thread.
41
43
  - Prefer curated summaries over raw event history.
42
- - Use event cues as short recall handles; `search_memory` fuzzy-ranks cues and headings before reading matching event bodies.
44
+ - Do not re-read `current_state.md` or `open_threads.md` after `wake_up`; they are already loaded.
45
+ - For small tasks, `append_event` is enough; skip curated file rewrites.
46
+ - Use event cues as short recall handles; `search_memory` ranks indexed events by cues, title, and body with typo tolerance, and cues weigh the most.
47
+ - Search returns bounded coherent evidence and does not scan unindexed event logs as a fallback.
43
48
  - Keep curated files short and legible.
49
+ - Rewrite `current_state.md` as a short snapshot of what is true now. Do not prepend dated entries to it; milestone detail belongs in `append_event` only.
50
+ - Remove resolved items from `open_threads.md` instead of keeping them.
44
51
  - Do not duplicate GitHub-owned facts such as commits, PRs, issues, reviews, or CI results. Store artifact references plus the missing rationale, preference, assumption, or follow-up context.
45
52
  - Use `project` scope for project-specific state.
46
53
  - Use `global` scope only for durable personal context that should follow the user across projects.
@@ -0,0 +1,80 @@
1
+ const assert = require("node:assert/strict");
2
+ const { execFileSync } = require("node:child_process");
3
+ const fs = require("node:fs/promises");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const test = require("node:test");
7
+
8
+ const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
9
+ const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
10
+
11
+ function resultText(result) {
12
+ return result.content.map((item) => item.text).join("\n");
13
+ }
14
+
15
+ test("MCP server rejects calls that would succeed but misbehave", async (t) => {
16
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-mcp-params-"));
17
+ const lagoonRoot = path.join(tempRoot, "lagoon");
18
+ const projectRoot = path.join(tempRoot, "params-project");
19
+
20
+ await fs.mkdir(projectRoot, { recursive: true });
21
+ execFileSync("git", ["init", "-q", projectRoot]);
22
+
23
+ const client = new Client({ name: "hippocamp-test", version: "0.0.0" });
24
+ await client.connect(
25
+ new StdioClientTransport({
26
+ command: process.execPath,
27
+ args: [path.join(__dirname, "..", "scripts", "hippocamp-mcp.cjs")],
28
+ cwd: projectRoot,
29
+ env: { ...process.env, HIPPOCAMP_GLOBAL_ROOT: lagoonRoot, HIPPOCAMP_PROJECT_ROOT: projectRoot },
30
+ }),
31
+ );
32
+ t.after(async () => {
33
+ await client.close();
34
+ await fs.rm(tempRoot, { recursive: true, force: true });
35
+ });
36
+
37
+ const eventWrite = await client.callTool({
38
+ name: "write_memory_file",
39
+ arguments: { scope: "project", path: "events/2026-09-29.md", content: "## Bypass\n\nNo index.", sync: false },
40
+ });
41
+ assert.equal(eventWrite.isError, true);
42
+ assert.match(resultText(eventWrite), /append-only/);
43
+
44
+ const noCues = await client.callTool({
45
+ name: "append_event",
46
+ arguments: { title: "Missing cues", content: "Unsearchable.", sync: false },
47
+ });
48
+ assert.equal(noCues.isError, true);
49
+
50
+ const tooManyCues = await client.callTool({
51
+ name: "append_event",
52
+ arguments: {
53
+ title: "Too many cues",
54
+ cues: Array.from({ length: 9 }, (_, index) => `cue-${index}`),
55
+ content: "Too broad.",
56
+ sync: false,
57
+ },
58
+ });
59
+ assert.equal(tooManyCues.isError, true);
60
+
61
+ const noTitle = await client.callTool({
62
+ name: "append_event",
63
+ arguments: { title: " ", cues: ["untitled"], content: "No heading.", sync: false },
64
+ });
65
+ assert.equal(noTitle.isError, true);
66
+
67
+ const wrongProject = await client.callTool({
68
+ name: "wake_up",
69
+ arguments: { projectRoot: path.join(tempRoot, "missing-project") },
70
+ });
71
+ assert.equal(wrongProject.isError, true);
72
+ assert.match(resultText(wrongProject), /Git repository/);
73
+
74
+ const valid = await client.callTool({
75
+ name: "append_event",
76
+ arguments: { title: "Valid event", cues: ["valid-cue"], content: "Indexed.", projectRoot, sync: false },
77
+ });
78
+ assert.notEqual(valid.isError, true);
79
+ assert.deepEqual(JSON.parse(resultText(valid)).cues, ["valid-cue"]);
80
+ });