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.
package/docs/how-it-works.md
CHANGED
|
@@ -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
|
@@ -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 ||
|
|
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
|
-
|
|
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
|
+
});
|