hippocamp 0.2.10 → 0.2.11

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.
@@ -78,7 +78,7 @@ Event writes update a sibling `events/YYYY-MM-DD.index.json` file. The Markdown
78
78
 
79
79
  - Keep memory concise.
80
80
  - Prefer curated summaries over raw event history.
81
- - Give events a short `Keywords:` section; keywords weigh the most in search ranking. Older events with a `Cues:` section still search the same way.
81
+ - Give events a short `Keywords:` section; keywords weigh the most in search ranking. `append_event` writes each keyword as a `[[keyword]]` wiki link, so a graph view such as Obsidian's groups events by topic. Older events with plain keywords or a `Cues:` section still search the same way.
82
82
  - Do not duplicate GitHub-owned facts such as commits, PRs, issues, reviews, or CI results.
83
83
  - Store artifact references plus the missing rationale, preference, assumption, or follow-up context.
84
84
  - Use project scope for project-specific state.
@@ -93,9 +93,9 @@ Agent: claude
93
93
  Session: mcp-a1b2c3d4
94
94
 
95
95
  Keywords:
96
- - local-first
97
- - token-free
98
- - install-story
96
+ - [[local-first]]
97
+ - [[token-free]]
98
+ - [[install-story]]
99
99
 
100
100
  References:
101
101
  - commit: abc1234
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippocamp",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "private": false,
5
5
  "description": "Local Git-backed memory for AI agents.",
6
6
  "license": "MIT",
@@ -79,7 +79,20 @@ function getGlobalRoot() {
79
79
  }
80
80
 
81
81
  function getProjectRoot(projectRoot) {
82
- return path.resolve(expandHomePath(projectRoot || process.env.HIPPOCAMP_PROJECT_ROOT || process.cwd()));
82
+ return path.resolve(expandHomePath(projectRoot || process.env.HIPPOCAMP_PROJECT_ROOT || getWorkingDirectory()));
83
+ }
84
+
85
+ // A long-running MCP server keeps its cwd after the worktree it started in is deleted.
86
+ function getWorkingDirectory() {
87
+ try {
88
+ return process.cwd();
89
+ } catch (error) {
90
+ if (error.code !== "ENOENT") {
91
+ throw error;
92
+ }
93
+
94
+ throw new Error("The working directory no longer exists, likely a deleted worktree. Pass projectRoot.");
95
+ }
83
96
  }
84
97
 
85
98
  function findGitRootSync(startPath) {
@@ -140,7 +153,9 @@ function isProjectRepository(projectRoot) {
140
153
  }
141
154
 
142
155
  function slugifyProjectName(value) {
156
+ // macOS can report the same accented folder name in NFC or NFD form.
143
157
  const slug = value
158
+ .normalize("NFC")
144
159
  .toLowerCase()
145
160
  .replace(/[^a-z0-9._-]+/g, "-")
146
161
  .replace(/^-+|-+$/g, "");
@@ -297,7 +312,8 @@ function formatKeywordsSection(keywords) {
297
312
  return "";
298
313
  }
299
314
 
300
- return ["Keywords:", ...normalizedKeywords.map((keyword) => `- ${keyword}`)].join("\n");
315
+ // Wiki links let Obsidian-style graph views group events by keyword.
316
+ return ["Keywords:", ...normalizedKeywords.map((keyword) => `- [[${keyword}]]`)].join("\n");
301
317
  }
302
318
 
303
319
  function normalizeAgentName(value) {
@@ -662,3 +662,57 @@ test("an empty folder has no project and uses global memory", async (t) => {
662
662
  // A folder that does not exist keeps its name, as Dream's synthetic roots do.
663
663
  assert.equal(memory.getProjectSlug(path.join(tempRoot, "missing")), "missing");
664
664
  });
665
+
666
+ test("Project slug is the same for NFC and NFD folder names", () => {
667
+ const name = "Génération IA";
668
+ assert.equal(memory.getProjectSlug(`/tmp/${name.normalize("NFC")}`), "g-n-ration-ia");
669
+ assert.equal(memory.getProjectSlug(`/tmp/${name.normalize("NFD")}`), "g-n-ration-ia");
670
+ });
671
+
672
+ test("A deleted working directory asks for projectRoot", async (t) => {
673
+ const originalCwd = process.cwd();
674
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-deleted-cwd-"));
675
+ t.after(() => process.chdir(originalCwd));
676
+ process.chdir(tempRoot);
677
+ await fs.rm(tempRoot, { recursive: true });
678
+
679
+ assert.throws(() => memory.getProjectRoot(), /Pass projectRoot/);
680
+ });
681
+
682
+ test("event keywords are written as wiki links and still indexed as plain keywords", async (t) => {
683
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
684
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-wiki-links-"));
685
+ const projectRoot = path.join(tempRoot, "wiki-links-project");
686
+
687
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
688
+ await fs.mkdir(projectRoot, { recursive: true });
689
+ t.after(async () => {
690
+ if (originalRoot === undefined) {
691
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
692
+ } else {
693
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
694
+ }
695
+ await fs.rm(tempRoot, { recursive: true, force: true });
696
+ });
697
+
698
+ const event = await memory.appendEvent({
699
+ content: "Graph views group events by keyword.",
700
+ keywords: ["Knowledge Graph", "obsidian"],
701
+ projectRoot,
702
+ scope: "project",
703
+ sync: false,
704
+ timestamp: "2026-10-16T09:00:00.000Z",
705
+ title: "Graph note",
706
+ });
707
+
708
+ const content = await fs.readFile(path.join(event.root, event.path), "utf8");
709
+ const index = JSON.parse(await fs.readFile(event.indexPath, "utf8"));
710
+
711
+ assert.match(content, /Keywords:\n- \[\[knowledge-graph\]\]\n- \[\[obsidian\]\]/);
712
+ assert.match(JSON.stringify(index), /"knowledge-graph"/);
713
+ assert.doesNotMatch(JSON.stringify(index), /\[\[/);
714
+
715
+ const search = await memory.searchMemory({ projectRoot, query: "obsidian", scope: "project" });
716
+
717
+ assert.deepEqual(search.results[0].keywords, ["knowledge-graph", "obsidian"]);
718
+ });