hippocamp 0.2.5 → 0.2.7

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.
@@ -76,7 +76,6 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
76
76
  const projectRoot = path.join(tempRoot, "search-project");
77
77
 
78
78
  process.env.HIPPOCAMP_GLOBAL_ROOT = lagoonRoot;
79
- await fs.mkdir(projectRoot, { recursive: true });
80
79
  t.after(async () => {
81
80
  if (originalRoot === undefined) {
82
81
  delete process.env.HIPPOCAMP_GLOBAL_ROOT;
@@ -88,7 +87,7 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
88
87
 
89
88
  await memory.appendEvent({
90
89
  content: "Decision:\nUse indexed search only.",
91
- cues: ["strict-index"],
90
+ keywords: ["strict-index"],
92
91
  date: "2026-09-21",
93
92
  projectRoot,
94
93
  scope: "project",
@@ -109,7 +108,7 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
109
108
 
110
109
  await memory.appendEvent({
111
110
  content: `Decision:\n${"x".repeat(1300)}`,
112
- cues: ["oversized-metadata"],
111
+ keywords: ["oversized-metadata"],
113
112
  date: "2026-09-21",
114
113
  projectRoot,
115
114
  scope: "project",
@@ -124,7 +123,9 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
124
123
  scope: "project",
125
124
  });
126
125
 
127
- assert.deepEqual(oversized.results, []);
126
+ // A keyword-only match still returns whole paragraphs; the oversized run is left out, never cropped.
127
+ assert.equal(oversized.results[0]?.heading, "Large metadata-only match");
128
+ assert.equal(oversized.results[0].snippet, "Decision:");
128
129
 
129
130
  const projectMemoryRoot = path.join(lagoonRoot, "projects", "search-project");
130
131
  await fs.writeFile(
@@ -145,7 +146,7 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
145
146
  const eventsRoot = path.join(projectMemoryRoot, "events");
146
147
  await fs.writeFile(
147
148
  path.join(eventsRoot, "2026-09-20.md"),
148
- "# Events: 2026-09-20\n\n## 09:00 — Hidden fallback\n\nCues:\n- quasar-velvet\n\nShould not be scanned.\n",
149
+ "# Events: 2026-09-20\n\n## 09:00 — Hidden fallback\n\nKeywords:\n- quasar-velvet\n\nShould not be scanned.\n",
149
150
  "utf8",
150
151
  );
151
152
 
@@ -174,11 +175,11 @@ test("search ranks indexed event bodies with typo tolerance", async (t) => {
174
175
  await fs.rm(tempRoot, { recursive: true, force: true });
175
176
  });
176
177
 
177
- for (const [title, cues, content] of [
178
+ for (const [title, keywords, content] of [
178
179
  ["Deploy note", ["deploy"], "The zanzibar migration needs a rollback plan."],
179
180
  ["Unrelated note", ["billing"], "Invoices are generated monthly."],
180
181
  ]) {
181
- await memory.appendEvent({ content, cues, projectRoot, scope: "project", sync: false, title });
182
+ await memory.appendEvent({ content, keywords, projectRoot, scope: "project", sync: false, title });
182
183
  }
183
184
 
184
185
  const search = await memory.searchMemory({ projectRoot, query: "zanzibr rollback", scope: "project" });
@@ -188,6 +189,42 @@ test("search ranks indexed event bodies with typo tolerance", async (t) => {
188
189
  assert.equal(search.results[0].match, "body");
189
190
  });
190
191
 
192
+ test("search still matches events written with legacy Cues sections and indexes", async (t) => {
193
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
194
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-legacy-cues-"));
195
+ const projectRoot = path.join(tempRoot, "legacy-project");
196
+ const eventsRoot = path.join(tempRoot, "lagoon", "projects", "legacy-project", "events");
197
+
198
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
199
+ await fs.mkdir(eventsRoot, { recursive: true });
200
+ t.after(async () => {
201
+ if (originalRoot === undefined) {
202
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
203
+ } else {
204
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
205
+ }
206
+ await fs.rm(tempRoot, { recursive: true, force: true });
207
+ });
208
+
209
+ const id = "2026-09-01T09:00:00.000Z";
210
+ await fs.writeFile(
211
+ path.join(eventsRoot, "2026-09-01.md"),
212
+ `# Events — 2026-09-01\n\n## ${id} — Old note\n\nCues:\n- quasar-velvet\n\nOld body.\n`,
213
+ "utf8",
214
+ );
215
+ await fs.writeFile(
216
+ path.join(eventsRoot, "2026-09-01.index.json"),
217
+ JSON.stringify({ version: 1, path: "2026-09-01.md", events: [{ id, heading: "Old note", cues: ["quasar-velvet"] }] }),
218
+ "utf8",
219
+ );
220
+
221
+ const search = await memory.searchMemory({ projectRoot, query: "quasar velvet", scope: "project" });
222
+
223
+ assert.equal(search.results.length, 1);
224
+ assert.deepEqual(search.results[0].keywords, ["quasar-velvet"]);
225
+ assert.equal(search.results[0].match, "keywords");
226
+ });
227
+
191
228
  test("search returns whole sentences from an oversized matching paragraph", async (t) => {
192
229
  const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
193
230
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-long-paragraph-"));
@@ -207,7 +244,7 @@ test("search returns whole sentences from an oversized matching paragraph", asyn
207
244
  const filler = "The weekly sync covered routine updates. ".repeat(40);
208
245
  await memory.appendEvent({
209
246
  content: `${filler}The kestrel database moved to eu-west-3. ${filler}`,
210
- cues: ["infra"],
247
+ keywords: ["infra"],
211
248
  projectRoot,
212
249
  scope: "project",
213
250
  sync: false,
@@ -246,7 +283,7 @@ test("search returns every matching paragraph that fits the excerpt budget", asy
246
283
  ];
247
284
  await memory.appendEvent({
248
285
  content: paragraphs.join("\n\n"),
249
- cues: ["infra"],
286
+ keywords: ["infra"],
250
287
  projectRoot,
251
288
  scope: "project",
252
289
  sync: false,
@@ -282,7 +319,7 @@ test("events appended in the same millisecond keep distinct ids and bodies", asy
282
319
  const timestamp = "2026-10-01T09:00:00.000Z";
283
320
  const first = await memory.appendEvent({
284
321
  content: "The zanzibar migration needs a rollback plan.",
285
- cues: ["deploy"],
322
+ keywords: ["deploy"],
286
323
  projectRoot,
287
324
  scope: "project",
288
325
  sync: false,
@@ -291,7 +328,7 @@ test("events appended in the same millisecond keep distinct ids and bodies", asy
291
328
  });
292
329
  const second = await memory.appendEvent({
293
330
  content: "Invoices are generated monthly.",
294
- cues: ["billing"],
331
+ keywords: ["billing"],
295
332
  projectRoot,
296
333
  scope: "project",
297
334
  sync: false,
@@ -329,7 +366,7 @@ test("parallel appends keep every event", async (t) => {
329
366
  Array.from({ length: 20 }, (_, index) =>
330
367
  memory.appendEvent({
331
368
  content: `Parallel event ${index}.`,
332
- cues: ["parallel"],
369
+ keywords: ["parallel"],
333
370
  date: "2026-10-01",
334
371
  projectRoot,
335
372
  scope: "project",
@@ -366,7 +403,7 @@ test("search favors events from a month named in the query", async (t) => {
366
403
  for (const timestamp of ["2026-05-02T08:00:00.000Z", "2026-08-05T10:00:00.000Z", "2026-09-01T09:00:00.000Z"]) {
367
404
  await memory.appendEvent({
368
405
  content: "Published the Android build to Google Play production.",
369
- cues: ["android-release"],
406
+ keywords: ["android-release"],
370
407
  date: timestamp.slice(0, 10),
371
408
  projectRoot,
372
409
  scope: "project",
@@ -407,7 +444,7 @@ test("search favors events from a relative date named in the query", async (t) =
407
444
  await memory.appendEvent({
408
445
  // The old event matches the words better, so only the date words can rank the others first.
409
446
  content: key === "older" ? "Deploy key rotation: rotated the deploy key again." : "Rotated the key for staging.",
410
- cues: ["deploy-key"],
447
+ keywords: ["deploy-key"],
411
448
  date: timestamp.slice(0, 10),
412
449
  projectRoot,
413
450
  scope: "project",
@@ -448,7 +485,7 @@ test("search returns a snippet for a long event matched only by its date", async
448
485
 
449
486
  await memory.appendEvent({
450
487
  content: Array(30).fill("The weekly sync covered routine updates.").join("\n\n"),
451
- cues: ["sync"],
488
+ keywords: ["sync"],
452
489
  date: "2026-08-05",
453
490
  projectRoot,
454
491
  scope: "project",
@@ -485,7 +522,7 @@ test("search returns nothing when no memory is about the question", async (t) =>
485
522
  ["Deploy rollback", "A failed backend deploy rolls back to the previous revision."],
486
523
  ["Billing note", "Invoices are generated monthly."],
487
524
  ]) {
488
- await memory.appendEvent({ content, cues: ["ops"], projectRoot, scope: "project", sync: false, title });
525
+ await memory.appendEvent({ content, keywords: ["ops"], projectRoot, scope: "project", sync: false, title });
489
526
  }
490
527
 
491
528
  const unrelated = await memory.searchMemory({ projectRoot, query: "which kubernetes namespace does the backend deploy to", scope: "project" });
@@ -498,3 +535,130 @@ test("search returns nothing when no memory is about the question", async (t) =>
498
535
  // No event is from August; the month name must not count as a missing topic word.
499
536
  assert.ok(monthOnly.results.length > 0);
500
537
  });
538
+
539
+ test("search keeps an event matched only by a keyword its long body never repeats", async (t) => {
540
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
541
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-keyword-only-"));
542
+ const projectRoot = path.join(tempRoot, "keyword-only-project");
543
+
544
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
545
+ await fs.mkdir(projectRoot, { recursive: true });
546
+ t.after(async () => {
547
+ if (originalRoot === undefined) {
548
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
549
+ } else {
550
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
551
+ }
552
+ await fs.rm(tempRoot, { recursive: true, force: true });
553
+ });
554
+
555
+ const content = [
556
+ "Opened three hardening PRs after the security audit.",
557
+ "The backend PR rotates the cron secret and checks request signatures on every scheduled job. ".repeat(8),
558
+ "The website PR updates the content studio dependencies and removes an unused preview route. ".repeat(8),
559
+ ].join("\n\n");
560
+
561
+ assert.ok(content.length > 1200);
562
+ await memory.appendEvent({
563
+ content,
564
+ keywords: ["security-audit", "strava-oauth"],
565
+ projectRoot,
566
+ scope: "project",
567
+ sync: false,
568
+ title: "Security audit opened hardening PRs",
569
+ });
570
+
571
+ const search = await memory.searchMemory({ projectRoot, query: "Strava bug", scope: "project" });
572
+
573
+ assert.equal(search.results[0]?.heading, "Security audit opened hardening PRs");
574
+ assert.match(search.results[0].snippet, /^Opened three hardening PRs/);
575
+ });
576
+
577
+ test("search returns nothing when a rare word only resembles memory words", async (t) => {
578
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
579
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-lookalike-"));
580
+ const projectRoot = path.join(tempRoot, "lookalike-project");
581
+
582
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
583
+ await fs.mkdir(projectRoot, { recursive: true });
584
+ t.after(async () => {
585
+ if (originalRoot === undefined) {
586
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
587
+ } else {
588
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
589
+ }
590
+ await fs.rm(tempRoot, { recursive: true, force: true });
591
+ });
592
+
593
+ await memory.appendEvent({
594
+ content: "Each worker instance reads the instruct prompt and its content before a sync.",
595
+ keywords: ["health-sync"],
596
+ projectRoot,
597
+ scope: "project",
598
+ sync: false,
599
+ title: "Apple Health sync banner hardening",
600
+ });
601
+
602
+ // "instinct" is one typo away from "instance" and "instruct" but means something else.
603
+ const lookalike = await memory.searchMemory({ projectRoot, query: "Instinct", scope: "project" });
604
+ const shared = await memory.searchMemory({ projectRoot, query: "context", scope: "project" });
605
+ const plural = await memory.searchMemory({ projectRoot, query: "worker instances", scope: "project" });
606
+
607
+ assert.deepEqual(lookalike.results, []);
608
+ assert.deepEqual(shared.results, []);
609
+ assert.equal(plural.results[0]?.heading, "Apple Health sync banner hardening");
610
+ });
611
+
612
+ test("an empty folder has no project and uses global memory", async (t) => {
613
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
614
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-empty-folder-"));
615
+ const lagoonRoot = path.join(tempRoot, "lagoon");
616
+ const emptyRoot = path.join(tempRoot, "billowy-glove");
617
+ const codeRoot = path.join(tempRoot, "job-search");
618
+
619
+ process.env.HIPPOCAMP_GLOBAL_ROOT = lagoonRoot;
620
+ // Only .git, like a fresh session folder some agents create.
621
+ await fs.mkdir(path.join(emptyRoot, ".git"), { recursive: true });
622
+ await fs.mkdir(codeRoot, { recursive: true });
623
+ await fs.writeFile(path.join(codeRoot, "notes.md"), "Job search notes.\n", "utf8");
624
+ t.after(async () => {
625
+ if (originalRoot === undefined) {
626
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
627
+ } else {
628
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
629
+ }
630
+ await fs.rm(tempRoot, { recursive: true, force: true });
631
+ });
632
+
633
+ const wake = await memory.wakeUp({ projectRoot: emptyRoot });
634
+ const event = await memory.appendEvent({
635
+ content: "Created the applying-to-jobs skill.",
636
+ keywords: ["applying-to-jobs"],
637
+ projectRoot: emptyRoot,
638
+ scope: "project",
639
+ sync: false,
640
+ title: "Job application skill created",
641
+ });
642
+ await memory.appendEvent({
643
+ content: "Created the applying-to-jobs tracker in Notion.",
644
+ keywords: ["applying-to-jobs-tracker"],
645
+ projectRoot: codeRoot,
646
+ scope: "project",
647
+ sync: false,
648
+ title: "Job tracker created",
649
+ });
650
+ const fromEmpty = await memory.searchMemory({ projectRoot: emptyRoot, query: "applying to jobs skill" });
651
+ const projectScope = await memory.searchMemory({ projectRoot: emptyRoot, query: "applying to jobs", scope: "project" });
652
+ const fromProject = await memory.searchMemory({ projectRoot: codeRoot, query: "applying to jobs skill" });
653
+
654
+ assert.equal(wake.projectSlug, null);
655
+ assert.equal(wake.projectMemoryRoot, lagoonRoot);
656
+ assert.equal(path.dirname(path.dirname(event.indexPath)), lagoonRoot);
657
+ // Global memory is searched once, and other projects stay out of it.
658
+ assert.deepEqual(fromEmpty.results.map((result) => result.heading), ["Job application skill created"]);
659
+ assert.deepEqual(projectScope.results.map((result) => result.heading), ["Job application skill created"]);
660
+ assert.equal(fromProject.results[0]?.heading, "Job application skill created");
661
+ assert.equal(memory.getProjectSlug(codeRoot), "job-search");
662
+ // A folder that does not exist keeps its name, as Dream's synthetic roots do.
663
+ assert.equal(memory.getProjectSlug(path.join(tempRoot, "missing")), "missing");
664
+ });
@@ -28,3 +28,15 @@ test("published files contain no local home directory paths", () => {
28
28
 
29
29
  assert.deepEqual(matches, []);
30
30
  });
31
+
32
+ test("the README demo GIF stays out of the npm package", () => {
33
+ // The README loads it from GitHub; .npmignore keeps the 600 kB file out of every install.
34
+ const output = execFileSync("npm", ["pack", "--dry-run", "--json"], {
35
+ cwd: path.join(__dirname, ".."),
36
+ encoding: "utf8",
37
+ shell: process.platform === "win32",
38
+ });
39
+ const [pack] = JSON.parse(output);
40
+
41
+ assert.ok(!pack.files.some((file) => file.path.endsWith(".gif")));
42
+ });
@@ -29,7 +29,6 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
29
29
  });
30
30
 
31
31
  await fs.mkdir(realLagoon);
32
- await fs.mkdir(projectRoot);
33
32
  await fs.symlink(realLagoon, linkedLagoon);
34
33
  git(["init", "-q", "-b", "main"]);
35
34
  git(["commit", "-q", "--allow-empty", "-m", "init"]);
@@ -45,7 +44,7 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
45
44
 
46
45
  const result = await memory.appendEvent({
47
46
  content: "Use advisory locks.",
48
- cues: ["locks"],
47
+ keywords: ["locks"],
49
48
  projectRoot,
50
49
  scope: "project",
51
50
  title: "Queue decision",
@@ -56,12 +55,11 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
56
55
 
57
56
  // A symlink inside the Lagoon pointing at another project must not be synced as this one.
58
57
  const otherProject = path.join(tempRoot, "other-project");
59
- await fs.mkdir(otherProject);
60
58
  await fs.mkdir(path.join(realLagoon, "projects", "other-project"), { recursive: true });
61
59
  await fs.symlink(path.join(realLagoon, "projects", "linked-project", "events"), path.join(realLagoon, "projects", "other-project", "events"));
62
60
 
63
61
  await assert.rejects(
64
- memory.appendEvent({ content: "Cross write.", cues: ["x"], projectRoot: otherProject, scope: "project", title: "Escape" }),
62
+ memory.appendEvent({ content: "Cross write.", keywords: ["x"], projectRoot: otherProject, scope: "project", title: "Escape" }),
65
63
  /escapes the memory root/,
66
64
  );
67
65
  });