hmem-mcp 5.0.0 → 5.1.21

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.
Files changed (43) hide show
  1. package/README.md +161 -214
  2. package/dist/cli-checkpoint.js +102 -40
  3. package/dist/cli-checkpoint.js.map +1 -1
  4. package/dist/cli-context-inject.d.ts +7 -6
  5. package/dist/cli-context-inject.js +27 -130
  6. package/dist/cli-context-inject.js.map +1 -1
  7. package/dist/cli-env.d.ts +16 -0
  8. package/dist/cli-env.js +40 -0
  9. package/dist/cli-env.js.map +1 -0
  10. package/dist/cli-hook-startup.d.ts +20 -0
  11. package/dist/cli-hook-startup.js +101 -0
  12. package/dist/cli-hook-startup.js.map +1 -0
  13. package/dist/cli-init.js +97 -188
  14. package/dist/cli-init.js.map +1 -1
  15. package/dist/cli-log-exchange.js +63 -3
  16. package/dist/cli-log-exchange.js.map +1 -1
  17. package/dist/cli-statusline.d.ts +14 -0
  18. package/dist/cli-statusline.js +172 -0
  19. package/dist/cli-statusline.js.map +1 -0
  20. package/dist/cli.js +18 -2
  21. package/dist/cli.js.map +1 -1
  22. package/dist/hmem-config.d.ts +10 -0
  23. package/dist/hmem-config.js +63 -13
  24. package/dist/hmem-config.js.map +1 -1
  25. package/dist/hmem-store.d.ts +30 -1
  26. package/dist/hmem-store.js +219 -48
  27. package/dist/hmem-store.js.map +1 -1
  28. package/dist/mcp-server.js +202 -75
  29. package/dist/mcp-server.js.map +1 -1
  30. package/package.json +1 -1
  31. package/scripts/autoresearch-nightly.sh +84 -0
  32. package/scripts/hmem-statusline.sh +4 -0
  33. package/skills/hmem-config/SKILL.md +112 -147
  34. package/skills/hmem-curate/SKILL.md +56 -6
  35. package/skills/hmem-new-project/SKILL.md +164 -0
  36. package/skills/hmem-read/SKILL.md +174 -146
  37. package/skills/hmem-release/SKILL.md +141 -0
  38. package/skills/hmem-self-curate/SKILL.md +49 -7
  39. package/skills/hmem-setup/SKILL.md +169 -87
  40. package/skills/hmem-sync-setup/SKILL.md +16 -3
  41. package/skills/hmem-update/SKILL.md +254 -0
  42. package/skills/hmem-wipe/SKILL.md +47 -21
  43. package/skills/hmem-write/SKILL.md +38 -14
@@ -116,14 +116,14 @@ function syncPull(hmemPath) {
116
116
  "pull", "--config", hmemSyncConfig(hmemPath),
117
117
  "--hmem-path", hmemPath,
118
118
  "--server-url", s.serverUrl, "--token", s.token,
119
- ], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32" });
119
+ ], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true });
120
120
  if (result.error)
121
121
  process.stderr.write(`hmem-sync pull error (${s.name ?? s.serverUrl}): ${result.error.message}\n`);
122
122
  }
123
123
  }
124
124
  else {
125
125
  const result = spawnSync("hmem-sync", ["pull", "--config", hmemSyncConfig(hmemPath), "--hmem-path", hmemPath], {
126
- env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32",
126
+ env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true,
127
127
  });
128
128
  if (result.error)
129
129
  process.stderr.write(`hmem-sync pull error: ${result.error.message}\n`);
@@ -175,12 +175,12 @@ function syncPullThenPush(hmemPath) {
175
175
  "pull", "--config", hmemSyncConfig(hmemPath),
176
176
  "--hmem-path", hmemPath,
177
177
  "--server-url", s.serverUrl, "--token", s.token,
178
- ], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32" });
178
+ ], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true });
179
179
  }
180
180
  }
181
181
  else {
182
182
  spawnSync("hmem-sync", ["pull", "--config", hmemSyncConfig(hmemPath), "--hmem-path", hmemPath], {
183
- env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32",
183
+ env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true,
184
184
  });
185
185
  }
186
186
  lastPullAt = Date.now();
@@ -197,13 +197,13 @@ function syncPush(hmemPath) {
197
197
  "push", "--config", hmemSyncConfig(hmemPath),
198
198
  "--hmem-path", hmemPath,
199
199
  "--server-url", s.serverUrl, "--token", s.token,
200
- ], { env: { ...process.env }, shell: process.platform === "win32", stdio: "ignore", detached: true });
200
+ ], { env: { ...process.env }, shell: process.platform === "win32", stdio: "ignore", detached: true, windowsHide: true });
201
201
  child.unref();
202
202
  }
203
203
  }
204
204
  else {
205
205
  const child = spawn("hmem-sync", ["push", "--config", hmemSyncConfig(hmemPath), "--hmem-path", hmemPath], {
206
- env: { ...process.env }, shell: process.platform === "win32", stdio: "ignore", detached: true,
206
+ env: { ...process.env }, shell: process.platform === "win32", stdio: "ignore", detached: true, windowsHide: true,
207
207
  });
208
208
  child.unref();
209
209
  }
@@ -211,11 +211,60 @@ function syncPush(hmemPath) {
211
211
  // Load hmem config (hmem.config.json in project dir, falls back to defaults)
212
212
  const hmemConfig = loadHmemConfig(PROJECT_DIR);
213
213
  log(`Config: levels=[${hmemConfig.maxCharsPerLevel.join(",")}] depth=${hmemConfig.maxDepth}`);
214
+ // ---- Version upgrade detection ----
215
+ import { createRequire } from "node:module";
216
+ const _require = createRequire(import.meta.url);
217
+ const PKG_VERSION = _require("../package.json").version;
218
+ /** Check if hmem was upgraded since last session. Auto-syncs skills and returns upgrade notice. */
219
+ function checkVersionUpgrade() {
220
+ try {
221
+ const configPath = path.join(PROJECT_DIR, "hmem.config.json");
222
+ if (!fs.existsSync(configPath))
223
+ return "";
224
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
225
+ const lastSeen = raw?.memory?.lastSeenVersion || raw?.lastSeenVersion;
226
+ if (!lastSeen) {
227
+ // First run with version tracking — save current version, sync skills silently
228
+ saveLastSeenVersion(configPath, raw);
229
+ autoSyncSkills();
230
+ return "";
231
+ }
232
+ if (lastSeen !== PKG_VERSION) {
233
+ saveLastSeenVersion(configPath, raw);
234
+ autoSyncSkills();
235
+ return `\n\n⚠ hmem-mcp updated: v${lastSeen} → v${PKG_VERSION}. Skills have been auto-synced. Run /hmem-update for full post-update steps (entry migration, schema enforcement, config check).`;
236
+ }
237
+ }
238
+ catch { }
239
+ return "";
240
+ }
241
+ /** Auto-sync skill files on version upgrade. Runs hmem update-skills in background. */
242
+ function autoSyncSkills() {
243
+ try {
244
+ const child = spawn("hmem", ["update-skills"], {
245
+ detached: true, stdio: "ignore",
246
+ env: { ...process.env },
247
+ });
248
+ child.unref();
249
+ log("Auto-syncing skills after version upgrade");
250
+ }
251
+ catch { }
252
+ }
253
+ function saveLastSeenVersion(configPath, raw) {
254
+ try {
255
+ if (!raw.memory)
256
+ raw.memory = {};
257
+ raw.memory.lastSeenVersion = PKG_VERSION;
258
+ fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf8");
259
+ }
260
+ catch { }
261
+ }
262
+ let versionUpgradeNotice = checkVersionUpgrade();
214
263
  // Session-scoped cache — persists across tool calls within this MCP connection
215
264
  const sessionCache = new SessionCache();
216
265
  const CONTEXT_THRESHOLD_WARNING = "\n\n⚠ CONTEXT THRESHOLD REACHED (~{tokens}k tokens delivered this session).\n" +
217
- "Recommended: flush_context to save current work, then /clear to reset conversation.\n" +
218
- "After /clear, use load_project to restore project context.";
266
+ "Tell the user to run /hmem-wipe it saves key knowledge and prepares for /clear.\n" +
267
+ "Alternative: flush_context manually, then /clear, then load_project to restore context.";
219
268
  const CACHE_RESET_SIGNAL = "/tmp/hmem-cache-reset-signal";
220
269
  /** Track tokens in a tool response and append threshold warning if needed. */
221
270
  function trackTokens(result) {
@@ -232,6 +281,11 @@ function trackTokens(result) {
232
281
  return result;
233
282
  const text = result.content.map(c => c.text).join("");
234
283
  sessionCache.addTokens(text.length);
284
+ // One-time version upgrade notice (shown once per session)
285
+ if (versionUpgradeNotice) {
286
+ result.content[result.content.length - 1].text += versionUpgradeNotice;
287
+ versionUpgradeNotice = ""; // only show once
288
+ }
235
289
  if (sessionCache.checkThreshold(hmemConfig.contextTokenThreshold)) {
236
290
  const tokK = Math.round(sessionCache.totalTokensDelivered / 1000);
237
291
  result.content[result.content.length - 1].text += CONTEXT_THRESHOLD_WARNING.replace("{tokens}", String(tokK));
@@ -259,8 +313,14 @@ function formatRecentOEntries(store, limit, exchangeCount, linkedTo, expandAll)
259
313
  lines.push(` ${o.id} ${o.created_at.substring(0, 10)} ${o.title}`);
260
314
  // Expand exchanges: all entries when expandAll, otherwise only latest
261
315
  if (expandAll || i === 0) {
262
- const exLimit = expandAll && i > 0 ? Math.min(exchangeCount, 5) : exchangeCount;
263
- const exchanges = store.getOEntryExchanges(o.id, exLimit);
316
+ // Check for checkpoint summaries if present, show summary + only exchanges after it
317
+ // Always show: latest summary (if any) + last 5 exchanges verbatim
318
+ const VERBATIM_WINDOW = 5;
319
+ const summaries = store.getCheckpointSummaries(o.id, 1);
320
+ if (summaries.length > 0) {
321
+ lines.push(` [Summary] ${summaries[0].content}`);
322
+ }
323
+ const exchanges = store.getOEntryExchanges(o.id, VERBATIM_WINDOW, true);
264
324
  for (const ex of exchanges) {
265
325
  const userShort = ex.userText.length > 300 ? ex.userText.substring(0, 300) + "..." : ex.userText;
266
326
  const agentShort = ex.agentText.length > 500 ? ex.agentText.substring(0, 500) + "..." : ex.agentText;
@@ -339,26 +399,30 @@ server.tool("write_memory", "Write a new memory entry to your hierarchical long-
339
399
  " Level 3: 2 tabs — even more detail\n" +
340
400
  " Level 4: 3 tabs — fine-grained detail\n" +
341
401
  " Level 5: 4 tabs — raw context/data\n" +
402
+ "Use > lines for body text (shown on drill-down, hidden in listings):\n" +
403
+ " Title line\\n> Body line 1\\n> Body line 2\\n\\tChild title\\n\\t> Child body\n" +
342
404
  "The system auto-assigns an ID and timestamp. " +
343
405
  `Use prefix to categorize: ${prefixList}.\n\n` +
344
406
  "Store types:\n" +
345
407
  " personal (default): Your private memory\n", {
346
408
  prefix: z.string().toUpperCase().describe(`Memory category: ${prefixList}`),
347
- content: z.string().min(3).describe("The memory content. Use tab indentation for depth levels. Example:\n" +
348
- "Built the Council Dashboard for Althing Inc.\n" +
349
- "\tMy role was frontend architecture with React + Vite\n" +
350
- "\t\tShadcnUI for components, SSE for real-time updates\n" +
351
- "\t\t\tAuth was tricky — EventSource can't send custom headers"),
409
+ content: z.string().min(3).describe("The memory content. Use tab indentation for depth levels. Use > for body text (hidden in listings, shown on drill-down).\n" +
410
+ "Example:\n" +
411
+ "Council Dashboard for Althing Inc.\n" +
412
+ "> Built a real-time dashboard with React + Vite. ShadcnUI for components, SSE for live updates.\n" +
413
+ "\tFrontend architecture\n" +
414
+ "\t> React + Vite, ShadcnUI components, SSE for real-time updates\n" +
415
+ "\t\tAuth was tricky — EventSource can't send custom headers"),
352
416
  links: z.array(z.string()).optional().describe("Optional: IDs of related memories, e.g. ['P0001', 'L0005']"),
353
- favorite: z.boolean().optional().describe("Mark this entry as a favorite — shown with [♥] in bulk reads and always inlined with L2 detail. " +
417
+ favorite: z.coerce.boolean().optional().describe("Mark this entry as a favorite — shown with [♥] in bulk reads and always inlined with L2 detail. " +
354
418
  "Use for reference info you need to see every session, regardless of category."),
355
419
  tags: z.array(z.string()).min(1).describe("Required hashtags for cross-cutting search (min 1, recommend 3+). " +
356
420
  "E.g. ['#hmem', '#curation']. Max 10, lowercase, must start with #. Shown after title in reads."),
357
- pinned: z.boolean().optional().describe("Mark this entry as pinned [P] (super-favorite). Pinned entries show full L2 content in bulk reads. " +
421
+ pinned: z.coerce.boolean().optional().describe("Mark this entry as pinned [P] (super-favorite). Pinned entries show full L2 content in bulk reads. " +
358
422
  "Use for reference entries you need to see in full every session."),
359
423
  store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' or 'company'"),
360
424
  min_role: z.enum(["worker", "al", "pl", "ceo"]).default("worker").describe("Minimum role to see this entry"),
361
- force: z.boolean().optional().describe("Force creation of a new root entry even if existing entries share tags. " +
425
+ force: z.coerce.boolean().optional().describe("Force creation of a new root entry even if existing entries share tags. " +
362
426
  "Only use when you intentionally want a separate entry, not a child of an existing one."),
363
427
  }, async ({ prefix, content, links, favorite, tags, pinned, store: storeName, min_role: minRole, force }) => {
364
428
  const templateName = AGENT_ID.replace(/_\d+$/, "");
@@ -455,9 +519,11 @@ server.tool("write_memory", "Write a new memory entry to your hierarchical long-
455
519
  });
456
520
  server.tool("update_memory", "Update the text of an existing memory entry or sub-node (your own personal memory). " +
457
521
  "Only modifies the text at the specified ID — children are preserved unchanged.\n\n" +
522
+ "Supports > body format: 'New title\\n> Body line 1\\n> Body line 2' splits into title (shown in listings) + body (shown on drill-down).\n\n" +
458
523
  "Use cases:\n" +
459
524
  "- Correct outdated wording: update_memory(id='L0003', content='corrected summary')\n" +
460
- "- Fix a sub-node: update_memory(id='L0003.2', content='corrected detail')\n" +
525
+ "- Add title/body split: update_memory(id='L0003', content='Short title\\n> Detailed body text')\n" +
526
+ "- Fix a sub-node: update_memory(id='L0003.2', content='node title\\n> node body')\n" +
461
527
  "- Mark as obsolete: FIRST write the correction, THEN update with [✓ID] reference:\n" +
462
528
  " 1. write_memory(prefix='E', content='Correct fix is...') → E0076\n" +
463
529
  " 2. update_memory(id='E0042', content='Wrong — see [✓E0076]', obsolete=true)\n" +
@@ -469,17 +535,17 @@ server.tool("update_memory", "Update the text of an existing memory entry or sub
469
535
  id: z.string().describe("ID of the entry or node to update, e.g. 'L0003' or 'L0003.2'"),
470
536
  content: z.string().min(1).describe("New text content for this node (plain text, no indentation)"),
471
537
  links: z.array(z.string()).optional().describe("Optional: update linked entry IDs (root entries only). Replaces existing links."),
472
- obsolete: z.boolean().optional().describe("Mark this root entry as no longer valid (root entries only). " +
538
+ obsolete: z.coerce.boolean().optional().describe("Mark this root entry as no longer valid (root entries only). " +
473
539
  "Requires [✓ID] correction reference in content (e.g. 'Wrong — see [✓E0076]')."),
474
- favorite: z.boolean().optional().describe("Set or clear the [♥] favorite flag. Works on root entries and sub-nodes. " +
540
+ favorite: z.coerce.boolean().optional().describe("Set or clear the [♥] favorite flag. Works on root entries and sub-nodes. " +
475
541
  "Root favorites are always shown with L2 detail in bulk reads."),
476
- irrelevant: z.boolean().optional().describe("Mark as irrelevant [-]. Works on root entries and sub-nodes. " +
542
+ irrelevant: z.coerce.boolean().optional().describe("Mark as irrelevant [-]. Works on root entries and sub-nodes. " +
477
543
  "No correction entry needed (unlike obsolete). Irrelevant entries/nodes are hidden from output."),
478
544
  tags: z.array(z.string()).optional().describe("Set tags on this entry/node. Replaces all existing tags. " +
479
545
  "Pass empty array [] to remove all tags. E.g. ['#hmem', '#curation']."),
480
- pinned: z.boolean().optional().describe("Set or clear the [P] pinned flag (root entries only). " +
546
+ pinned: z.coerce.boolean().optional().describe("Set or clear the [P] pinned flag (root entries only). " +
481
547
  "Pinned entries show full L2 content in bulk reads (super-favorite)."),
482
- active: z.boolean().optional().describe("Mark this root entry as actively relevant [*] (root entries only). " +
548
+ active: z.coerce.boolean().optional().describe("Mark this root entry as actively relevant [*] (root entries only). " +
483
549
  "When any entry in a prefix has active=true, only active entries of that prefix are shown with children in bulk reads. " +
484
550
  "Non-active entries in the same prefix are shown as title-only (no children)."),
485
551
  store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' or 'company'"),
@@ -560,10 +626,10 @@ server.tool("update_many", "Batch-update multiple memory entries at once. Applie
560
626
  "Use this instead of calling update_memory multiple times during curation.\n\n" +
561
627
  "Example: update_many(ids=['T0005', 'T0012', 'L0044'], irrelevant=true)", {
562
628
  ids: z.array(z.string()).min(1).describe("List of entry/node IDs to update, e.g. ['T0005', 'T0012', 'L0044']"),
563
- irrelevant: z.boolean().optional().describe("Mark all as irrelevant [-]"),
564
- favorite: z.boolean().optional().describe("Set or clear [♥] favorite on all"),
565
- active: z.boolean().optional().describe("Set or clear [*] active on all"),
566
- pinned: z.boolean().optional().describe("Set or clear [P] pinned on all"),
629
+ irrelevant: z.coerce.boolean().optional().describe("Mark all as irrelevant [-]"),
630
+ favorite: z.coerce.boolean().optional().describe("Set or clear [♥] favorite on all"),
631
+ active: z.coerce.boolean().optional().describe("Set or clear [*] active on all"),
632
+ pinned: z.coerce.boolean().optional().describe("Set or clear [P] pinned on all"),
567
633
  store: z.enum(["personal", "company"]).default("personal"),
568
634
  }, async ({ ids, irrelevant, favorite, active, pinned, store: storeName }) => {
569
635
  const templateName = AGENT_ID.replace(/_\d+$/, "");
@@ -659,10 +725,11 @@ server.tool("append_memory", "Append new child nodes to an existing memory entry
659
725
  "Use this to extend an existing entry with additional detail without overwriting it.\n\n" +
660
726
  "Content uses tab indentation relative to the parent:\n" +
661
727
  " 0 tabs = direct child of id\n" +
662
- " 1 tab = grandchild, etc.\n\n" +
728
+ " 1 tab = grandchild, etc.\n" +
729
+ "Use > for body text: 'Node title\\n> Body shown on drill-down\\n\\tChild node'\n\n" +
663
730
  "Examples:\n" +
664
- " append_memory(id='L0003', content='New finding\\n\\tSub-detail') " +
665
- "→ adds L2 node + L3 child\n" +
731
+ " append_memory(id='L0003', content='New finding\\n> Detailed explanation\\n\\tSub-detail') " +
732
+ "→ adds L2 node (with title + body) + L3 child\n" +
666
733
  " append_memory(id='L0003.2', content='Extra note') " +
667
734
  "→ adds L3 node under the L2 node L0003.2", {
668
735
  id: z.string().describe("Root entry ID or parent node ID to append children to, e.g. 'L0003' or 'L0003.2'"),
@@ -749,11 +816,11 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
749
816
  time: z.string().optional().describe("Time filter 'HH:MM' — filter entries by time of day"),
750
817
  period: z.string().optional().describe("Time window: '+4h' (after), '-2h' (before), '4h' (±4h symmetric), 'both' (±2h default)"),
751
818
  time_around: z.string().optional().describe("Reference entry ID — find entries created around the same time"),
752
- show_obsolete: z.boolean().optional().describe("Include all obsolete entries (default: only top 3 most-accessed)"),
753
- show_obsolete_path: z.boolean().optional().describe("When reading an obsolete entry by ID, show the full correction chain instead of just the final valid entry."),
754
- titles_only: z.boolean().optional().describe("Compact title listing — shows all entries as ID + date + title, without V2 selection or children. " +
819
+ show_obsolete: z.coerce.boolean().optional().describe("Include all obsolete entries (default: only top 3 most-accessed)"),
820
+ show_obsolete_path: z.coerce.boolean().optional().describe("When reading an obsolete entry by ID, show the full correction chain instead of just the final valid entry."),
821
+ titles_only: z.coerce.boolean().optional().describe("Compact title listing — shows all entries as ID + date + title, without V2 selection or children. " +
755
822
  "Like a table of contents. Combine with prefix to filter by category."),
756
- expand: z.boolean().optional().describe("Expand full tree with complete node content (ID queries only). " +
823
+ expand: z.coerce.boolean().optional().describe("Expand full tree with complete node content (ID queries only). " +
757
824
  "Use to deep-dive into a project after a long break. " +
758
825
  "depth controls how deep (default: 5 = full tree). " +
759
826
  "Example: read_memory({ id: 'P0001', expand: true, depth: 3 })"),
@@ -762,8 +829,8 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
762
829
  "use after context compression to recover key knowledge. " +
763
830
  "Auto-selected if omitted: first bulk read → discover, subsequent → essentials."),
764
831
  store: z.enum(["personal", "company"]).default("personal").describe("Source store: 'personal' or 'company'"),
765
- curator: z.boolean().optional().describe("Set true to show full metadata (access counts, roles, dates). For curators only."),
766
- show_all: z.boolean().optional().describe("Curation mode: show ALL entries of the selected prefix with depth 3 children. " +
832
+ curator: z.coerce.boolean().optional().describe("Set true to show full metadata (access counts, roles, dates). For curators only."),
833
+ show_all: z.coerce.boolean().optional().describe("Curation mode: show ALL entries of the selected prefix with depth 3 children. " +
767
834
  "Bypasses V2 selection and session cache. Use with prefix filter for manageable output."),
768
835
  tag: z.string().optional().describe("Filter by hashtag, e.g. '#hmem'. Only entries with this tag are shown in bulk reads. " +
769
836
  "Also works with search to find tagged entries."),
@@ -863,7 +930,8 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
863
930
  }
864
931
  const effectiveDepth = depth || (id ? 2 : 1);
865
932
  // Session cache: cached entries shown as titles in subsequent bulk reads
866
- const isBulkListing = !id && !search && !time_around;
933
+ // Explicit filters (after, before, prefix, stale_days, tag) bypass V2 selection + cache
934
+ const isBulkListing = !id && !search && !time_around && !after && !before && !prefix && !stale_days && !tag;
867
935
  const useCache = isBulkListing && storeName === "personal" && !show_all;
868
936
  const cachedIds = useCache ? sessionCache.getCachedIds() : undefined;
869
937
  const hiddenIds = useCache ? sessionCache.getHiddenIds() : undefined;
@@ -886,6 +954,7 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
886
954
  mode: isBulkListing ? effectiveMode : undefined,
887
955
  tag,
888
956
  staleDays: stale_days,
957
+ directResults: !isBulkListing && !id && !search && !time_around,
889
958
  });
890
959
  if (entries.length === 0) {
891
960
  const hmemPath = storeName === "company"
@@ -1123,7 +1192,7 @@ server.tool("import_memory", "Import entries from a .hmem file into your memory.
1123
1192
  "Deduplicates by L1 content (merges sub-nodes), remaps IDs on conflict.", {
1124
1193
  source_path: z.string().describe("Path to .hmem file to import"),
1125
1194
  store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' (your own memory) or 'company' (shared company store)"),
1126
- dry_run: z.boolean().default(false).describe("Preview only — report what would happen without modifying the database"),
1195
+ dry_run: z.coerce.boolean().default(false).describe("Preview only — report what would happen without modifying the database"),
1127
1196
  }, async ({ source_path, store: storeName, dry_run }) => {
1128
1197
  try {
1129
1198
  const hmemStore = storeName === "company"
@@ -1311,30 +1380,51 @@ server.tool("load_project", "Load a project and activate it. Returns L2 content
1311
1380
  if (e.level_1 && e.level_1 !== e.title)
1312
1381
  lines.push(` ${e.level_1}`);
1313
1382
  if (e.children) {
1383
+ const { withBody, withChildren } = hmemConfig.loadProjectExpand;
1314
1384
  for (const child of e.children.filter(c => !c.irrelevant)) {
1315
1385
  const cId = child.id.replace(e.id, "");
1316
- const isOverview = child.seq === 1; // .1 = Overview node
1386
+ const expandBody = withBody.includes(child.seq);
1387
+ const expandChildTitles = withChildren.includes(child.seq);
1317
1388
  lines.push(` ${cId} ${child.title || child.content.substring(0, 60)}`);
1318
1389
  if (child.children && child.children.length > 0) {
1319
1390
  for (const gc of child.children.filter((g) => !g.irrelevant)) {
1320
1391
  const gcId = gc.id.replace(e.id, "");
1321
- if (isOverview) {
1322
- // Overview: show full L3 content
1323
- lines.push(` ${gcId} ${gc.content}`);
1392
+ if (expandBody) {
1393
+ // Show L3 title + body content
1394
+ lines.push(` ${gcId} ${gc.title || gc.content.substring(0, 80)}`);
1395
+ if (gc.content && gc.content !== gc.title) {
1396
+ // Show body lines indented
1397
+ for (const bodyLine of gc.content.split("\n")) {
1398
+ lines.push(` ${bodyLine}`);
1399
+ }
1400
+ }
1401
+ }
1402
+ else if (expandChildTitles) {
1403
+ // Show all L3 children as titles
1404
+ const gcTitle = gc.title || (gc.content.length > 80 ? gc.content.substring(0, 80) : gc.content);
1405
+ lines.push(` ${gcId} ${gcTitle}`);
1324
1406
  }
1325
1407
  else {
1326
- // Other L2 nodes: compact titles only
1408
+ // Default: compact titles only
1327
1409
  const gcTitle = gc.title || (gc.content.length > 80 ? gc.content.substring(0, 80) : gc.content);
1328
1410
  lines.push(` ${gcId} ${gcTitle}`);
1329
1411
  }
1330
- // L4 hint
1331
- if (gc.child_count && gc.child_count > 0) {
1332
- lines.push(` [+${gc.child_count} ${gc.id}]`);
1412
+ // L4 children titles (already loaded via depth=4)
1413
+ if (gc.children && gc.children.length > 0) {
1414
+ const visibleL4 = gc.children.filter((l4) => !l4.irrelevant);
1415
+ for (const l4 of visibleL4) {
1416
+ const l4Id = l4.id.replace(e.id, "");
1417
+ const l4Title = l4.title || (l4.content?.length > 60 ? l4.content.substring(0, 60) + "…" : l4.content || "");
1418
+ lines.push(` ${l4Id} ${l4Title}`);
1419
+ }
1420
+ }
1421
+ else if (gc.child_count && gc.child_count > 0) {
1422
+ lines.push(` [+${gc.child_count}]`);
1333
1423
  }
1334
1424
  }
1335
1425
  }
1336
1426
  else if (child.child_count && child.child_count > 0) {
1337
- lines.push(` [+${child.child_count} → ${child.id}]`);
1427
+ lines.push(` [+${child.child_count}]`);
1338
1428
  }
1339
1429
  }
1340
1430
  }
@@ -1369,14 +1459,31 @@ server.tool("load_project", "Load a project and activate it. Returns L2 content
1369
1459
  lines.push(` ${r.id} ${r.title}`);
1370
1460
  }
1371
1461
  }
1372
- // Inject recent O-entries linked to this project (full exchanges for all)
1462
+ // Inject the most recent O-entry linked to this project with last N exchanges
1463
+ // Purpose: seamless continuation of the previous session's conversation
1373
1464
  if (hmemConfig.recentOEntries > 0) {
1374
- const { text, ids } = formatRecentOEntries(hmemStore, hmemConfig.recentOEntries, 10, id, true);
1465
+ const { text, ids } = formatRecentOEntries(hmemStore, 1, hmemConfig.recentOEntries, id, true);
1375
1466
  if (text) {
1376
1467
  lines.push(" " + text.replace(/\n/g, "\n "));
1377
1468
  sessionCache.registerDelivered(ids);
1378
1469
  }
1379
1470
  }
1471
+ // Inject universal conventions (C-entries tagged #universal)
1472
+ try {
1473
+ const conventions = hmemStore.read({
1474
+ prefix: "C", depth: 2, agentRole: (ROLE || "worker"),
1475
+ }).filter(c => !c.obsolete && !c.irrelevant && c.tags?.includes("#universal"));
1476
+ if (conventions.length > 0) {
1477
+ lines.push(" Conventions (#universal):");
1478
+ for (const c of conventions) {
1479
+ lines.push(` ${c.id} ${c.title}`);
1480
+ if (c.level_1 && c.level_1 !== c.title)
1481
+ lines.push(` ${c.level_1}`);
1482
+ }
1483
+ }
1484
+ }
1485
+ catch { /* conventions are optional */ }
1486
+ const irrelevantTip = `Tip: update_memory(id, { irrelevant: true }) to hide noisy entries from future loads.`;
1380
1487
  const output = lines.join("\n");
1381
1488
  const outputTokens = Math.round(output.length / 4);
1382
1489
  const totalStats = hmemStore.stats();
@@ -1390,7 +1497,7 @@ server.tool("load_project", "Load a project and activate it. Returns L2 content
1390
1497
  return trackTokens({
1391
1498
  content: [{
1392
1499
  type: "text",
1393
- text: `✓ Project ${id} activated.${tokenInfo}\n\n${output}`,
1500
+ text: `✓ Project ${id} activated.${tokenInfo}\n${irrelevantTip}\n\n${output}\n\n${irrelevantTip}`,
1394
1501
  }],
1395
1502
  });
1396
1503
  }
@@ -1595,7 +1702,7 @@ server.tool("get_audit_queue", "CURATOR ONLY (ceo role). Returns agents whose .h
1595
1702
  "Each agent should be audited in a separate spawn to keep context bounded.", {}, async () => {
1596
1703
  if (!isCurator()) {
1597
1704
  return {
1598
- content: [{ type: "text", text: "ERROR: get_audit_queue is only available to the ceo/curator role." }],
1705
+ content: [{ type: "text", text: "ERROR: get_audit_queue is only available to the ceo/curator role. Set HMEM_AGENT_ROLE=ceo in your MCP server config to use curation tools." }],
1599
1706
  isError: true,
1600
1707
  };
1601
1708
  }
@@ -1681,8 +1788,11 @@ server.tool("read_agent_memory", "CURATOR ONLY (ceo role). Read the full memory
1681
1788
  const favTag = e.favorite ? " [♥]" : "";
1682
1789
  lines.push(`[${e.id}] ${date}${role}${favTag}${obsoleteTag}${irrelevantTag}${access}`);
1683
1790
  lines.push(` ${e.title}`);
1684
- if (e.level_1 !== e.title)
1685
- lines.push(` ${e.level_1}`);
1791
+ if (e.level_1 && e.level_1 !== e.title) {
1792
+ for (const bodyLine of e.level_1.split("\n")) {
1793
+ lines.push(` ${bodyLine}`);
1794
+ }
1795
+ }
1686
1796
  if (e.children && e.children.length > 0) {
1687
1797
  for (const child of e.children) {
1688
1798
  const indent = " ".repeat(child.depth - 1);
@@ -1716,10 +1826,10 @@ server.tool("fix_agent_memory", "CURATOR ONLY (ceo role). Correct a specific ent
1716
1826
  content: z.string().optional().describe("New text content. For root entries: replaces the L1 summary. " +
1717
1827
  "For node IDs: replaces that node's content."),
1718
1828
  min_role: z.enum(["worker", "al", "pl", "ceo"]).optional().describe("Update access clearance (root entries only)."),
1719
- obsolete: z.boolean().optional().describe("Mark or unmark as obsolete (root entries only). " +
1829
+ obsolete: z.coerce.boolean().optional().describe("Mark or unmark as obsolete (root entries only). " +
1720
1830
  "Obsolete entries stay in memory but are shown with [⚠ OBSOLETE]."),
1721
- favorite: z.boolean().optional().describe("Set or clear the [♥] favorite flag (root entries only)."),
1722
- irrelevant: z.boolean().optional().describe("Mark or unmark as irrelevant (root entries only). Irrelevant entries are hidden from bulk reads. No correction entry needed."),
1831
+ favorite: z.coerce.boolean().optional().describe("Set or clear the [♥] favorite flag (root entries only)."),
1832
+ irrelevant: z.coerce.boolean().optional().describe("Mark or unmark as irrelevant (root entries only). Irrelevant entries are hidden from bulk reads. No correction entry needed."),
1723
1833
  }, async ({ agent_name, entry_id, content, min_role, obsolete, favorite, irrelevant }) => {
1724
1834
  if (!isCurator()) {
1725
1835
  return {
@@ -1961,7 +2071,7 @@ function formatTitlesOnly(entries, config, curator = false) {
1961
2071
  lines.push(` ${compactChildId}${cfav} ${short}${grandchildren}`);
1962
2072
  }
1963
2073
  if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
1964
- lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
2074
+ lines.push(` [+${e.hiddenChildrenCount} more]`);
1965
2075
  }
1966
2076
  if (hiddenIrr > 0) {
1967
2077
  lines.push(` (+${hiddenIrr} irrelevant hidden)`);
@@ -2068,9 +2178,11 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
2068
2178
  else {
2069
2179
  lines.push(`${e.id} ${e.title}${tagStr}`);
2070
2180
  }
2071
- // Node drilldown: show full content below title
2072
- if (hasDetail && e.level_1 !== e.title) {
2073
- lines.push(` ${e.level_1}`);
2181
+ // Node drilldown: show body below title
2182
+ if (e.level_1 && e.level_1 !== e.title) {
2183
+ for (const bodyLine of e.level_1.split("\n")) {
2184
+ lines.push(` ${bodyLine}`);
2185
+ }
2074
2186
  }
2075
2187
  }
2076
2188
  else {
@@ -2096,9 +2208,11 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
2096
2208
  const syncTag = syncThreshold && e.updated_at && e.updated_at <= syncThreshold ? " ✓" : "";
2097
2209
  lines.push(`${e.id}${promotedTag}${activeTag}${pinnedTag}${obsoleteTag}${irrelevantTag}${syncTag} ${e.title}${tagStr}`);
2098
2210
  }
2099
- // Show full level_1 content below title when entry is expanded/drilled
2100
- if (hasDetail && e.level_1 !== e.title) {
2101
- lines.push(` ${e.level_1}`);
2211
+ // Show body below title when entry is drilled into
2212
+ if (e.level_1 && e.level_1 !== e.title) {
2213
+ for (const bodyLine of e.level_1.split("\n")) {
2214
+ lines.push(` ${bodyLine}`);
2215
+ }
2102
2216
  }
2103
2217
  }
2104
2218
  // Children — filter out irrelevant nodes
@@ -2114,7 +2228,7 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
2114
2228
  else if (e.expanded && !expand) {
2115
2229
  renderChildrenFormatted(lines, visibleChildren, curator, rootId);
2116
2230
  if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
2117
- lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
2231
+ lines.push(` [+${e.hiddenChildrenCount} more]`);
2118
2232
  }
2119
2233
  }
2120
2234
  else if (e.hiddenChildrenCount !== undefined) {
@@ -2124,7 +2238,7 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
2124
2238
  const fav = nodeMarkers(child);
2125
2239
  const compactChildId = child.id.replace(rootId, "");
2126
2240
  const hint = (child.child_count ?? 0) > 0
2127
- ? ` [+${child.child_count} → ${child.id}]`
2241
+ ? ` [+${child.child_count}]`
2128
2242
  : "";
2129
2243
  if (curator) {
2130
2244
  lines.push(` [${child.id}]${fav} ${child.title}${hint}`);
@@ -2134,7 +2248,7 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
2134
2248
  }
2135
2249
  }
2136
2250
  if (e.hiddenChildrenCount > 0) {
2137
- lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
2251
+ lines.push(` [+${e.hiddenChildrenCount} more]`);
2138
2252
  }
2139
2253
  }
2140
2254
  else {
@@ -2202,7 +2316,7 @@ function renderChildrenFormatted(lines, children, curator, rootId) {
2202
2316
  const ctags = formatTagSuffix(child.tags, curator);
2203
2317
  const compactId = rootId ? child.id.replace(rootId, "") : child.id;
2204
2318
  const hint = (child.child_count ?? 0) > 0
2205
- ? ` [+${child.child_count} → ${child.id}]`
2319
+ ? ` [+${child.child_count}]`
2206
2320
  : "";
2207
2321
  if (curator) {
2208
2322
  lines.push(`${indent}[${child.id}]${fav} ${child.title}${ctags}${hint}`);
@@ -2221,24 +2335,31 @@ function renderChildrenFormatted(lines, children, curator, rootId) {
2221
2335
  function renderChildrenExpanded(lines, children, curator, rootId) {
2222
2336
  for (const child of children) {
2223
2337
  const indent = " ".repeat(child.depth - 1);
2338
+ const bodyIndent = indent + " ";
2224
2339
  const fav = nodeMarkers(child);
2225
2340
  const compactId = rootId ? child.id.replace(rootId, "") : child.id;
2226
2341
  const visibleGrandchildren = child.children?.filter(c => !c.irrelevant);
2227
2342
  const hasLoadedChildren = visibleGrandchildren && visibleGrandchildren.length > 0;
2228
2343
  const isBoundary = !hasLoadedChildren && (child.child_count ?? 0) > 0;
2344
+ const hasBody = child.content && child.content !== child.title;
2229
2345
  if (hasLoadedChildren) {
2230
- // Inner node: full content + recurse
2346
+ // Inner node: title + body + recurse
2231
2347
  if (curator) {
2232
- lines.push(`${indent}[${child.id}]${fav} ${child.content}`);
2348
+ lines.push(`${indent}[${child.id}]${fav} ${child.title}`);
2233
2349
  }
2234
2350
  else {
2235
- lines.push(`${indent}${compactId}${fav} ${child.content}`);
2351
+ lines.push(`${indent}${compactId}${fav} ${child.title}`);
2352
+ }
2353
+ if (hasBody) {
2354
+ for (const bodyLine of child.content.split("\n")) {
2355
+ lines.push(`${bodyIndent}${bodyLine}`);
2356
+ }
2236
2357
  }
2237
2358
  renderChildrenExpanded(lines, visibleGrandchildren, curator, rootId);
2238
2359
  }
2239
2360
  else if (isBoundary) {
2240
2361
  // Boundary: title only + child count hint
2241
- const hint = ` [+${child.child_count} → ${child.id}]`;
2362
+ const hint = ` [+${child.child_count}]`;
2242
2363
  if (curator) {
2243
2364
  lines.push(`${indent}[${child.id}]${fav} ${child.title}${hint}`);
2244
2365
  }
@@ -2247,12 +2368,17 @@ function renderChildrenExpanded(lines, children, curator, rootId) {
2247
2368
  }
2248
2369
  }
2249
2370
  else {
2250
- // Leaf node (no children at all): full content
2371
+ // Leaf node: title + body
2251
2372
  if (curator) {
2252
- lines.push(`${indent}[${child.id}]${fav} ${child.content}`);
2373
+ lines.push(`${indent}[${child.id}]${fav} ${child.title}`);
2253
2374
  }
2254
2375
  else {
2255
- lines.push(`${indent}${compactId}${fav} ${child.content}`);
2376
+ lines.push(`${indent}${compactId}${fav} ${child.title}`);
2377
+ }
2378
+ if (hasBody) {
2379
+ for (const bodyLine of child.content.split("\n")) {
2380
+ lines.push(`${bodyIndent}${bodyLine}`);
2381
+ }
2256
2382
  }
2257
2383
  }
2258
2384
  }
@@ -2277,6 +2403,7 @@ function checkForUpdates() {
2277
2403
  stdio: ["ignore", "pipe", "ignore"],
2278
2404
  detached: true,
2279
2405
  shell: process.platform === "win32",
2406
+ windowsHide: true,
2280
2407
  });
2281
2408
  child.unref();
2282
2409
  let out = "";