hmem-mcp 2.2.0 → 2.4.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.
- package/README.md +83 -50
- package/dist/hmem-config.d.ts +9 -2
- package/dist/hmem-config.js +19 -0
- package/dist/hmem-config.js.map +1 -1
- package/dist/hmem-store.d.ts +88 -8
- package/dist/hmem-store.js +656 -52
- package/dist/hmem-store.js.map +1 -1
- package/dist/mcp-server.js +187 -50
- package/dist/mcp-server.js.map +1 -1
- package/dist/session-cache.d.ts +20 -40
- package/dist/session-cache.js +42 -47
- package/dist/session-cache.js.map +1 -1
- package/package.json +1 -1
- package/skills/hmem-read/SKILL.md +13 -12
package/dist/mcp-server.js
CHANGED
|
@@ -115,6 +115,7 @@ server.tool("search_memory", "Searches the collective memory: agent memories (le
|
|
|
115
115
|
// ---- Humanlike Memory (.hmem) ----
|
|
116
116
|
const prefixList = formatPrefixList(hmemConfig.prefixes);
|
|
117
117
|
const prefixKeys = Object.keys(hmemConfig.prefixes);
|
|
118
|
+
const REMINDER_HINT = "\nACTION: Scan the entries above. Mark stale/noise as irrelevant, important ones as favorite, wrong ones as obsolete. Do it NOW — don't just note it.\n update_memory(id=\"X\", irrelevant=true) — hide noise\n update_memory(id=\"X\", favorite=true) — pin important\n update_memory(id=\"X\", content=\"Wrong — see [✓correctionId]\", obsolete=true) — correct mistakes";
|
|
118
119
|
server.tool("write_memory", "Write a new memory entry to your hierarchical long-term memory (.hmem). " +
|
|
119
120
|
"Use tab indentation to create depth levels:\n" +
|
|
120
121
|
" Level 1: No indentation — the rough summary (always visible at startup)\n" +
|
|
@@ -135,9 +136,13 @@ server.tool("write_memory", "Write a new memory entry to your hierarchical long-
|
|
|
135
136
|
links: z.array(z.string()).optional().describe("Optional: IDs of related memories, e.g. ['P0001', 'L0005']"),
|
|
136
137
|
favorite: z.boolean().optional().describe("Mark this entry as a favorite — shown with [♥] in bulk reads and always inlined with L2 detail. " +
|
|
137
138
|
"Use for reference info you need to see every session, regardless of category."),
|
|
139
|
+
tags: z.array(z.string()).optional().describe("Optional hashtags for cross-cutting search, e.g. ['#hmem', '#curation']. " +
|
|
140
|
+
"Max 10, lowercase, must start with #. Shown after title in reads."),
|
|
141
|
+
pinned: z.boolean().optional().describe("Mark this entry as pinned [P] (super-favorite). Pinned entries show full L2 content in bulk reads. " +
|
|
142
|
+
"Use for reference entries you need to see in full every session."),
|
|
138
143
|
store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' or 'company'"),
|
|
139
144
|
min_role: z.enum(["worker", "al", "pl", "ceo"]).default("worker").describe("Minimum role to see this entry"),
|
|
140
|
-
}, async ({ prefix, content, links, favorite, store: storeName, min_role: minRole }) => {
|
|
145
|
+
}, async ({ prefix, content, links, favorite, tags, pinned, store: storeName, min_role: minRole }) => {
|
|
141
146
|
const templateName = AGENT_ID.replace(/_\d+$/, "");
|
|
142
147
|
const agentRole = (ROLE || "worker");
|
|
143
148
|
const isFirstTime = !AGENT_ID && !fs.existsSync(resolveHmemPath(PROJECT_DIR, ""));
|
|
@@ -166,7 +171,7 @@ server.tool("write_memory", "Write a new memory entry to your hierarchical long-
|
|
|
166
171
|
};
|
|
167
172
|
}
|
|
168
173
|
const effectiveMinRole = storeName === "company" ? minRole : "worker";
|
|
169
|
-
const result = hmemStore.write(prefix, content, links, effectiveMinRole, favorite);
|
|
174
|
+
const result = hmemStore.write(prefix, content, links, effectiveMinRole, favorite, tags, pinned);
|
|
170
175
|
const storeLabel = storeName === "company" ? "company" : (templateName || "memory");
|
|
171
176
|
log(`write_memory [${storeLabel}]: ${result.id} (prefix=${prefix}, min_role=${effectiveMinRole})`);
|
|
172
177
|
const hmemPath = resolveHmemPath(PROJECT_DIR, templateName);
|
|
@@ -214,10 +219,14 @@ server.tool("update_memory", "Update the text of an existing memory entry or sub
|
|
|
214
219
|
"Requires [✓ID] correction reference in content (e.g. 'Wrong — see [✓E0076]')."),
|
|
215
220
|
favorite: z.boolean().optional().describe("Set or clear the [♥] favorite flag. Works on root entries and sub-nodes. " +
|
|
216
221
|
"Root favorites are always shown with L2 detail in bulk reads."),
|
|
217
|
-
irrelevant: z.boolean().optional().describe("Mark
|
|
218
|
-
"No correction entry needed (unlike obsolete). Irrelevant entries are hidden from
|
|
222
|
+
irrelevant: z.boolean().optional().describe("Mark as irrelevant [-]. Works on root entries and sub-nodes. " +
|
|
223
|
+
"No correction entry needed (unlike obsolete). Irrelevant entries/nodes are hidden from output."),
|
|
224
|
+
tags: z.array(z.string()).optional().describe("Set tags on this entry/node. Replaces all existing tags. " +
|
|
225
|
+
"Pass empty array [] to remove all tags. E.g. ['#hmem', '#curation']."),
|
|
226
|
+
pinned: z.boolean().optional().describe("Set or clear the [P] pinned flag (root entries only). " +
|
|
227
|
+
"Pinned entries show full L2 content in bulk reads (super-favorite)."),
|
|
219
228
|
store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' or 'company'"),
|
|
220
|
-
}, async ({ id, content, links, obsolete, favorite, irrelevant, store: storeName }) => {
|
|
229
|
+
}, async ({ id, content, links, obsolete, favorite, irrelevant, tags, pinned, store: storeName }) => {
|
|
221
230
|
const templateName = AGENT_ID.replace(/_\d+$/, "");
|
|
222
231
|
const agentRole = (ROLE || "worker");
|
|
223
232
|
if (storeName === "company") {
|
|
@@ -240,7 +249,7 @@ server.tool("update_memory", "Update the text of an existing memory entry or sub
|
|
|
240
249
|
isError: true,
|
|
241
250
|
};
|
|
242
251
|
}
|
|
243
|
-
const ok = hmemStore.updateNode(id, content, links, obsolete, favorite, undefined, irrelevant);
|
|
252
|
+
const ok = hmemStore.updateNode(id, content, links, obsolete, favorite, undefined, irrelevant, tags, pinned);
|
|
244
253
|
const storeLabel = storeName === "company" ? "company" : (templateName || "memory");
|
|
245
254
|
log(`update_memory [${storeLabel}]: ${id} → ${ok ? "updated" : "not found"}${obsolete ? " (marked obsolete)" : ""}${irrelevant ? " (marked irrelevant)" : ""}${favorite !== undefined ? ` (favorite=${favorite})` : ""}`);
|
|
246
255
|
if (!ok) {
|
|
@@ -262,6 +271,12 @@ server.tool("update_memory", "Update the text of an existing memory entry or sub
|
|
|
262
271
|
parts.push("marked as [♥] favorite");
|
|
263
272
|
if (favorite === false)
|
|
264
273
|
parts.push("favorite flag cleared");
|
|
274
|
+
if (pinned === true)
|
|
275
|
+
parts.push("marked as [P] pinned");
|
|
276
|
+
if (pinned === false)
|
|
277
|
+
parts.push("pinned flag cleared");
|
|
278
|
+
if (tags !== undefined)
|
|
279
|
+
parts.push(tags.length > 0 ? `tags: ${tags.join(" ")}` : "tags cleared");
|
|
265
280
|
return { content: [{ type: "text", text: parts.join(" | ") }] };
|
|
266
281
|
}
|
|
267
282
|
finally {
|
|
@@ -379,7 +394,11 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
379
394
|
"Auto-selected if omitted: first bulk read → discover, subsequent → essentials."),
|
|
380
395
|
store: z.enum(["personal", "company"]).default("personal").describe("Source store: 'personal' or 'company'"),
|
|
381
396
|
curator: z.boolean().optional().describe("Set true to show full metadata (access counts, roles, dates). For curators only."),
|
|
382
|
-
|
|
397
|
+
show_all: z.boolean().optional().describe("Curation mode: show ALL entries of the selected prefix with depth 3 children. " +
|
|
398
|
+
"Bypasses V2 selection and session cache. Use with prefix filter for manageable output."),
|
|
399
|
+
tag: z.string().optional().describe("Filter by hashtag, e.g. '#hmem'. Only entries with this tag are shown in bulk reads. " +
|
|
400
|
+
"Also works with search to find tagged entries."),
|
|
401
|
+
}, async ({ id, depth, prefix, after, before, search, limit: maxResults, time, period, time_around, show_obsolete, show_obsolete_path, titles_only, expand, mode, store: storeName, curator, show_all, tag }) => {
|
|
383
402
|
if (AGENT_ID === "UNKNOWN") {
|
|
384
403
|
return {
|
|
385
404
|
content: [{ type: "text", text: "ERROR: Agent-ID unknown. read_memory is only available for spawned agents." }],
|
|
@@ -397,12 +416,12 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
397
416
|
? "⚠ WARNING: Memory database is corrupted! Reads may be incomplete. A backup (.corrupt) was saved.\n\n"
|
|
398
417
|
: "";
|
|
399
418
|
const effectiveDepth = depth || (id ? 2 : 1);
|
|
400
|
-
// Session cache:
|
|
419
|
+
// Session cache: cached entries shown as titles in subsequent bulk reads
|
|
401
420
|
const isBulkListing = !id && !search && !time_around;
|
|
402
|
-
const useCache = isBulkListing && storeName === "personal";
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
421
|
+
const useCache = isBulkListing && storeName === "personal" && !show_all;
|
|
422
|
+
const cachedIds = useCache ? sessionCache.getCachedIds() : undefined;
|
|
423
|
+
const hiddenIds = useCache ? sessionCache.getHiddenIds() : undefined;
|
|
424
|
+
const slotFraction = useCache ? sessionCache.getSlotFraction() : undefined;
|
|
406
425
|
// Auto-select mode: first bulk read → discover, subsequent → essentials
|
|
407
426
|
const effectiveMode = mode ?? (useCache && sessionCache.readCount > 0 ? "essentials" : "discover");
|
|
408
427
|
const entries = hmemStore.read({
|
|
@@ -414,10 +433,12 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
414
433
|
showObsoletePath: show_obsolete_path,
|
|
415
434
|
titlesOnly: titles_only,
|
|
416
435
|
expand,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
436
|
+
cachedIds,
|
|
437
|
+
hiddenIds,
|
|
438
|
+
slotFraction,
|
|
439
|
+
showAll: show_all,
|
|
420
440
|
mode: isBulkListing ? effectiveMode : undefined,
|
|
441
|
+
tag,
|
|
421
442
|
});
|
|
422
443
|
if (entries.length === 0) {
|
|
423
444
|
const hint = id ? `No memory with ID "${id}".` :
|
|
@@ -442,18 +463,25 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
442
463
|
const storeLabel = storeName === "company" ? "company" : templateName;
|
|
443
464
|
const visibleCount = entries.length;
|
|
444
465
|
// Cache status in header (when active)
|
|
466
|
+
const hiddenCount = hiddenIds?.size ?? 0;
|
|
467
|
+
const cachedCount = cachedIds?.size ?? 0;
|
|
445
468
|
const cacheInfo = useCache && sessionCache.size > 0
|
|
446
|
-
? ` | Cache: ${sessionCache.size} seen`
|
|
469
|
+
? ` | Cache: ${sessionCache.size} seen` + (hiddenCount > 0 ? ` (${hiddenCount} hidden)` : "")
|
|
447
470
|
: "";
|
|
448
471
|
// Mode info in header (only for bulk reads)
|
|
449
472
|
const modeInfo = isBulkListing ? ` | Mode: ${effectiveMode}` : "";
|
|
473
|
+
// Token estimation: output tokens / total tokens
|
|
474
|
+
const outputTokens = Math.round(output.length / 4);
|
|
475
|
+
const totalTokens = Math.round(stats.totalChars / 4);
|
|
476
|
+
const fmtTok = (n) => n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
|
|
477
|
+
const tokenInfo = ` | ${fmtTok(outputTokens)}/${fmtTok(totalTokens)} tokens`;
|
|
450
478
|
const header = `## Memory: ${storeLabel} (${stats.total} total entries)\n` +
|
|
451
|
-
`Query: ${id ? `id=${id}` : ""}${prefix ? `prefix=${prefix}` : ""}${search ? `search="${search}"` : ""}${time_around ? `time_around=${time_around}` : ""}${after ? ` after=${after}` : ""}${before ? ` before=${before}` : ""}${time ? ` time=${time}` : ""} | Depth: ${effectiveDepth} | Results: ${visibleCount}${modeInfo}${cacheInfo}\n`;
|
|
479
|
+
`Query: ${id ? `id=${id}` : ""}${prefix ? `prefix=${prefix}` : ""}${search ? `search="${search}"` : ""}${time_around ? `time_around=${time_around}` : ""}${after ? ` after=${after}` : ""}${before ? ` before=${before}` : ""}${time ? ` time=${time}` : ""} | Depth: ${effectiveDepth} | Results: ${visibleCount}${modeInfo}${cacheInfo}${tokenInfo}\n`;
|
|
452
480
|
log(`read_memory [${storeLabel}]: ${visibleCount} results (depth=${effectiveDepth}, role=${agentRole}${cacheInfo})`);
|
|
453
481
|
return {
|
|
454
482
|
content: [{
|
|
455
483
|
type: "text",
|
|
456
|
-
text: corruptionWarning + header + "\n" + output,
|
|
484
|
+
text: corruptionWarning + header + "\n" + output + REMINDER_HINT,
|
|
457
485
|
}],
|
|
458
486
|
};
|
|
459
487
|
}
|
|
@@ -487,6 +515,82 @@ server.tool("reset_memory_cache", "Clear the session cache so all entries are tr
|
|
|
487
515
|
}],
|
|
488
516
|
};
|
|
489
517
|
});
|
|
518
|
+
// ---- Export Memory ----
|
|
519
|
+
server.tool("export_memory", "Export your memory, excluding secret entries and secret sub-nodes. " +
|
|
520
|
+
"Use for sharing, backup, or publishing a sanitized version of your memory.", {
|
|
521
|
+
store: z.enum(["personal", "company"]).default("personal").describe("Source store: 'personal' (your own memory) or 'company' (shared company store)"),
|
|
522
|
+
format: z.enum(["text", "hmem"]).default("text").describe("Export format: 'text' = Markdown (returned inline), " +
|
|
523
|
+
"'hmem' = SQLite .hmem file (written to disk)"),
|
|
524
|
+
output_path: z.string().optional().describe("Output path for 'hmem' format. Default: export.hmem next to the source file. " +
|
|
525
|
+
"Ignored for 'text' format."),
|
|
526
|
+
}, async ({ store: storeName, format, output_path }) => {
|
|
527
|
+
try {
|
|
528
|
+
const hmemStore = storeName === "company"
|
|
529
|
+
? openCompanyMemory(PROJECT_DIR, hmemConfig)
|
|
530
|
+
: openAgentMemory(PROJECT_DIR, AGENT_ID.replace(/_\d+$/, ""), hmemConfig);
|
|
531
|
+
try {
|
|
532
|
+
if (format === "hmem") {
|
|
533
|
+
const defaultPath = path.join(path.dirname(hmemStore.getDbPath()), "export.hmem");
|
|
534
|
+
const outPath = output_path || defaultPath;
|
|
535
|
+
const result = hmemStore.exportPublicToHmem(outPath);
|
|
536
|
+
return { content: [{ type: "text", text: `Exported to ${outPath}\n${result.entries} entries, ${result.nodes} nodes, ${result.tags} tags` }] };
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const output = hmemStore.exportMarkdown();
|
|
540
|
+
return { content: [{ type: "text", text: output }] };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
hmemStore.close();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: `ERROR: ${e}` }],
|
|
550
|
+
isError: true,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
// ---- Import Memory ----
|
|
555
|
+
server.tool("import_memory", "Import entries from a .hmem file into your memory. " +
|
|
556
|
+
"Deduplicates by L1 content (merges sub-nodes), remaps IDs on conflict.", {
|
|
557
|
+
source_path: z.string().describe("Path to .hmem file to import"),
|
|
558
|
+
store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' (your own memory) or 'company' (shared company store)"),
|
|
559
|
+
dry_run: z.boolean().default(false).describe("Preview only — report what would happen without modifying the database"),
|
|
560
|
+
}, async ({ source_path, store: storeName, dry_run }) => {
|
|
561
|
+
try {
|
|
562
|
+
const hmemStore = storeName === "company"
|
|
563
|
+
? openCompanyMemory(PROJECT_DIR, hmemConfig)
|
|
564
|
+
: openAgentMemory(PROJECT_DIR, AGENT_ID.replace(/_\d+$/, ""), hmemConfig);
|
|
565
|
+
try {
|
|
566
|
+
const result = hmemStore.importFromHmem(source_path, dry_run);
|
|
567
|
+
const mode = dry_run ? "preview" : "imported";
|
|
568
|
+
log(`import_memory: ${mode} from ${source_path} (${result.inserted} new, ${result.merged} merged)`);
|
|
569
|
+
const lines = [];
|
|
570
|
+
lines.push(dry_run
|
|
571
|
+
? `Import preview from ${source_path}:`
|
|
572
|
+
: `Imported from ${source_path}:`);
|
|
573
|
+
lines.push(` ${result.inserted} entries ${dry_run ? "to insert" : "inserted"}`);
|
|
574
|
+
lines.push(` ${result.merged} entries ${dry_run ? "to merge" : "merged"} (L1 match)`);
|
|
575
|
+
lines.push(` ${result.nodesInserted} nodes ${dry_run ? "to insert" : "inserted"}`);
|
|
576
|
+
lines.push(` ${result.nodesSkipped} nodes skipped (duplicate L2)`);
|
|
577
|
+
lines.push(` ${result.tagsImported} tags ${dry_run ? "to import" : "imported"}`);
|
|
578
|
+
if (result.remapped) {
|
|
579
|
+
lines.push(` ID remapping ${dry_run ? "required" : "applied"} (${result.conflicts} conflicts)`);
|
|
580
|
+
}
|
|
581
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
hmemStore.close();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
return {
|
|
589
|
+
content: [{ type: "text", text: `ERROR: ${e}` }],
|
|
590
|
+
isError: true,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
});
|
|
490
594
|
// ---- Curator Tools (ceo role only) ----
|
|
491
595
|
const AUDIT_STATE_FILE = process.env.HMEM_AUDIT_STATE_PATH
|
|
492
596
|
|| path.join(PROJECT_DIR, "audit_state.json");
|
|
@@ -839,6 +943,12 @@ server.tool("mark_audited", "CURATOR ONLY (ceo role). Mark an agent as audited (
|
|
|
839
943
|
* V2 selection applies. Favorites/top-accessed show L2 children titles.
|
|
840
944
|
* Non-expanded entries show (N) child count indicator.
|
|
841
945
|
*/
|
|
946
|
+
/** Format tags as a compact suffix: " #hmem #curation" or "" if no tags. */
|
|
947
|
+
function formatTagSuffix(tags) {
|
|
948
|
+
if (!tags || tags.length === 0)
|
|
949
|
+
return "";
|
|
950
|
+
return " " + tags.join(" ");
|
|
951
|
+
}
|
|
842
952
|
function formatTitlesOnly(entries, config) {
|
|
843
953
|
const CHILD_TITLE_LEN = 50;
|
|
844
954
|
const lines = [];
|
|
@@ -859,9 +969,11 @@ function formatTitlesOnly(entries, config) {
|
|
|
859
969
|
const obs = e.obsolete ? " [!]" : "";
|
|
860
970
|
const irr = e.irrelevant ? " [-]" : "";
|
|
861
971
|
if (e.expanded && e.children && e.children.length > 0) {
|
|
972
|
+
const visibleChildren = e.children.filter(c => !c.irrelevant);
|
|
973
|
+
const hiddenIrr = e.children.length - visibleChildren.length;
|
|
862
974
|
// Expanded entry (favorite/top-accessed): show with L2 children
|
|
863
|
-
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}`);
|
|
864
|
-
for (const child of
|
|
975
|
+
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}${formatTagSuffix(e.tags)}`);
|
|
976
|
+
for (const child of visibleChildren) {
|
|
865
977
|
const short = child.title || (child.content.length > CHILD_TITLE_LEN
|
|
866
978
|
? child.content.substring(0, CHILD_TITLE_LEN)
|
|
867
979
|
: child.content);
|
|
@@ -872,11 +984,14 @@ function formatTitlesOnly(entries, config) {
|
|
|
872
984
|
if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
|
|
873
985
|
lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
|
|
874
986
|
}
|
|
987
|
+
if (hiddenIrr > 0) {
|
|
988
|
+
lines.push(` (+${hiddenIrr} irrelevant hidden)`);
|
|
989
|
+
}
|
|
875
990
|
}
|
|
876
991
|
else {
|
|
877
992
|
// Non-expanded: compact line with child count
|
|
878
993
|
const childHint = (e.hiddenChildrenCount ?? 0) > 0 ? ` (${e.hiddenChildrenCount})` : "";
|
|
879
|
-
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}${childHint}`);
|
|
994
|
+
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}${formatTagSuffix(e.tags)}${childHint}`);
|
|
880
995
|
}
|
|
881
996
|
}
|
|
882
997
|
lines.push("");
|
|
@@ -937,19 +1052,22 @@ function formatFlatOutput(entries, curator, expand = false) {
|
|
|
937
1052
|
return lines.join("\n");
|
|
938
1053
|
}
|
|
939
1054
|
/** Favorite marker for child nodes. */
|
|
940
|
-
function
|
|
941
|
-
|
|
1055
|
+
function nodeMarkers(node) {
|
|
1056
|
+
const fav = node.favorite ? " [♥]" : "";
|
|
1057
|
+
const irr = node.irrelevant ? " [-]" : "";
|
|
1058
|
+
return `${fav}${irr}`;
|
|
942
1059
|
}
|
|
943
1060
|
function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
944
1061
|
const isNode = e.id.includes(".");
|
|
945
1062
|
const hasDetail = !!(e.children?.length || e.linkedEntries?.length);
|
|
1063
|
+
const tagStr = formatTagSuffix(e.tags);
|
|
946
1064
|
// Headline: use title for navigation, show full content below when drilling in
|
|
947
1065
|
if (isNode) {
|
|
948
1066
|
if (curator) {
|
|
949
|
-
lines.push(`[${e.id}] ${e.title}`);
|
|
1067
|
+
lines.push(`[${e.id}] ${e.title}${tagStr}`);
|
|
950
1068
|
}
|
|
951
1069
|
else {
|
|
952
|
-
lines.push(`${e.id} ${e.title}`);
|
|
1070
|
+
lines.push(`${e.id} ${e.title}${tagStr}`);
|
|
953
1071
|
}
|
|
954
1072
|
// Node drilldown: show full content below title
|
|
955
1073
|
if (hasDetail && e.level_1 !== e.title) {
|
|
@@ -959,50 +1077,56 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
959
1077
|
else {
|
|
960
1078
|
if (curator) {
|
|
961
1079
|
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : "";
|
|
1080
|
+
const pinnedTag = e.pinned ? " [P]" : "";
|
|
962
1081
|
const obsoleteTag = e.obsolete ? " [⚠ OBSOLETE]" : "";
|
|
963
1082
|
const irrelevantTag = e.irrelevant ? " [- IRRELEVANT]" : "";
|
|
964
1083
|
const date = e.created_at.substring(0, 10);
|
|
965
1084
|
const accessed = e.access_count > 0 ? ` (${e.access_count}x accessed)` : "";
|
|
966
1085
|
const roleTag = e.min_role !== "worker" ? ` [${e.min_role}+]` : "";
|
|
967
|
-
lines.push(`[${e.id}] ${date}${roleTag}${promotedTag}${obsoleteTag}${irrelevantTag}${accessed}`);
|
|
968
|
-
lines.push(` ${e.title}`);
|
|
1086
|
+
lines.push(`[${e.id}] ${date}${roleTag}${promotedTag}${pinnedTag}${obsoleteTag}${irrelevantTag}${accessed}`);
|
|
1087
|
+
lines.push(` ${e.title}${tagStr}`);
|
|
969
1088
|
}
|
|
970
1089
|
else {
|
|
971
1090
|
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : "";
|
|
1091
|
+
const pinnedTag = e.pinned ? " [P]" : "";
|
|
972
1092
|
const obsoleteTag = e.obsolete ? " [!]" : "";
|
|
973
1093
|
const irrelevantTag = e.irrelevant ? " [-]" : "";
|
|
974
1094
|
const mmdd = e.created_at.substring(5, 10);
|
|
975
|
-
lines.push(`${e.id} ${mmdd}${promotedTag}${obsoleteTag}${irrelevantTag} ${e.title}`);
|
|
1095
|
+
lines.push(`${e.id} ${mmdd}${promotedTag}${pinnedTag}${obsoleteTag}${irrelevantTag} ${e.title}${tagStr}`);
|
|
976
1096
|
}
|
|
977
1097
|
// Show full level_1 content below title when entry is expanded/drilled
|
|
978
1098
|
if (hasDetail && e.level_1 !== e.title) {
|
|
979
1099
|
lines.push(` ${e.level_1}`);
|
|
980
1100
|
}
|
|
981
1101
|
}
|
|
982
|
-
// Children
|
|
1102
|
+
// Children — filter out irrelevant nodes
|
|
983
1103
|
if (e.children && e.children.length > 0) {
|
|
1104
|
+
const visibleChildren = e.children.filter(c => !c.irrelevant);
|
|
1105
|
+
const hiddenIrrelevant = e.children.length - visibleChildren.length;
|
|
984
1106
|
if (expand) {
|
|
985
1107
|
// Expand mode: full content + recursive children
|
|
986
|
-
renderChildrenExpanded(lines,
|
|
1108
|
+
renderChildrenExpanded(lines, visibleChildren, curator);
|
|
987
1109
|
}
|
|
988
1110
|
else if (e.expanded && !expand) {
|
|
989
|
-
renderChildrenFormatted(lines,
|
|
1111
|
+
renderChildrenFormatted(lines, visibleChildren, curator);
|
|
990
1112
|
if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
|
|
991
1113
|
lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
|
|
992
1114
|
}
|
|
993
1115
|
}
|
|
994
1116
|
else if (e.hiddenChildrenCount !== undefined) {
|
|
995
|
-
// Non-expanded bulk read: show only the latest child title
|
|
996
|
-
const child =
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1117
|
+
// Non-expanded bulk read: show only the latest visible child title
|
|
1118
|
+
const child = visibleChildren[0];
|
|
1119
|
+
if (child) {
|
|
1120
|
+
const fav = nodeMarkers(child);
|
|
1121
|
+
const hint = (child.child_count ?? 0) > 0
|
|
1122
|
+
? ` [+${child.child_count} → ${child.id}]`
|
|
1123
|
+
: "";
|
|
1124
|
+
if (curator) {
|
|
1125
|
+
lines.push(` [${child.id}]${fav} ${child.title}${hint}`);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
lines.push(` ${child.id}${fav} ${child.title}${hint}`);
|
|
1129
|
+
}
|
|
1006
1130
|
}
|
|
1007
1131
|
if (e.hiddenChildrenCount > 0) {
|
|
1008
1132
|
lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
|
|
@@ -1010,7 +1134,10 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1010
1134
|
}
|
|
1011
1135
|
else {
|
|
1012
1136
|
// ID-based read: show all direct children as titles
|
|
1013
|
-
renderChildrenFormatted(lines,
|
|
1137
|
+
renderChildrenFormatted(lines, visibleChildren, curator);
|
|
1138
|
+
}
|
|
1139
|
+
if (hiddenIrrelevant > 0) {
|
|
1140
|
+
lines.push(` (+${hiddenIrrelevant} irrelevant hidden)`);
|
|
1014
1141
|
}
|
|
1015
1142
|
}
|
|
1016
1143
|
// Links
|
|
@@ -1044,11 +1171,19 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1044
1171
|
const hint = (lchild.child_count ?? 0) > 0
|
|
1045
1172
|
? ` (${lchild.child_count} ${lchild.child_count === 1 ? "child" : "children"} — use id="${lchild.id}" to expand)`
|
|
1046
1173
|
: "";
|
|
1047
|
-
lines.push(` [${lchild.id}]${
|
|
1174
|
+
lines.push(` [${lchild.id}]${nodeMarkers(lchild)} ${lchild.title}${hint}`);
|
|
1048
1175
|
}
|
|
1049
1176
|
}
|
|
1050
1177
|
}
|
|
1051
1178
|
}
|
|
1179
|
+
// Related entries (shared tags)
|
|
1180
|
+
if (e.relatedEntries && e.relatedEntries.length > 0) {
|
|
1181
|
+
lines.push(` --- Related (shared tags) ---`);
|
|
1182
|
+
for (const rel of e.relatedEntries) {
|
|
1183
|
+
const rmmdd = rel.created_at.substring(5, 10);
|
|
1184
|
+
lines.push(` ${rel.id} ${rmmdd} ${rel.title}${formatTagSuffix(rel.tags)}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1052
1187
|
lines.push("");
|
|
1053
1188
|
}
|
|
1054
1189
|
/**
|
|
@@ -1058,15 +1193,16 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1058
1193
|
function renderChildrenFormatted(lines, children, curator) {
|
|
1059
1194
|
for (const child of children) {
|
|
1060
1195
|
const indent = " ".repeat(child.depth - 1);
|
|
1061
|
-
const fav =
|
|
1196
|
+
const fav = nodeMarkers(child);
|
|
1197
|
+
const ctags = formatTagSuffix(child.tags);
|
|
1062
1198
|
const hint = (child.child_count ?? 0) > 0
|
|
1063
1199
|
? ` [+${child.child_count} → ${child.id}]`
|
|
1064
1200
|
: "";
|
|
1065
1201
|
if (curator) {
|
|
1066
|
-
lines.push(`${indent}[${child.id}]${fav} ${child.title}${hint}`);
|
|
1202
|
+
lines.push(`${indent}[${child.id}]${fav} ${child.title}${ctags}${hint}`);
|
|
1067
1203
|
}
|
|
1068
1204
|
else {
|
|
1069
|
-
lines.push(`${indent}${child.id}${fav} ${child.title}${hint}`);
|
|
1205
|
+
lines.push(`${indent}${child.id}${fav} ${child.title}${ctags}${hint}`);
|
|
1070
1206
|
}
|
|
1071
1207
|
// Don't recurse into grandchildren — titles only, drill for content
|
|
1072
1208
|
}
|
|
@@ -1080,8 +1216,9 @@ function renderChildrenFormatted(lines, children, curator) {
|
|
|
1080
1216
|
function renderChildrenExpanded(lines, children, curator) {
|
|
1081
1217
|
for (const child of children) {
|
|
1082
1218
|
const indent = " ".repeat(child.depth - 1);
|
|
1083
|
-
const fav =
|
|
1084
|
-
const
|
|
1219
|
+
const fav = nodeMarkers(child);
|
|
1220
|
+
const visibleGrandchildren = child.children?.filter(c => !c.irrelevant);
|
|
1221
|
+
const hasLoadedChildren = visibleGrandchildren && visibleGrandchildren.length > 0;
|
|
1085
1222
|
const isBoundary = !hasLoadedChildren && (child.child_count ?? 0) > 0;
|
|
1086
1223
|
if (hasLoadedChildren) {
|
|
1087
1224
|
// Inner node: full content + recurse
|
|
@@ -1091,7 +1228,7 @@ function renderChildrenExpanded(lines, children, curator) {
|
|
|
1091
1228
|
else {
|
|
1092
1229
|
lines.push(`${indent}${child.id}${fav} ${child.content}`);
|
|
1093
1230
|
}
|
|
1094
|
-
renderChildrenExpanded(lines,
|
|
1231
|
+
renderChildrenExpanded(lines, visibleGrandchildren, curator);
|
|
1095
1232
|
}
|
|
1096
1233
|
else if (isBoundary) {
|
|
1097
1234
|
// Boundary: title only + child count hint
|