hmem-mcp 2.3.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 +34 -2
- 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 +87 -8
- package/dist/hmem-store.js +651 -50
- package/dist/hmem-store.js.map +1 -1
- package/dist/mcp-server.js +181 -49
- 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,8 +463,10 @@ 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}` : "";
|
|
@@ -458,7 +481,7 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
458
481
|
return {
|
|
459
482
|
content: [{
|
|
460
483
|
type: "text",
|
|
461
|
-
text: corruptionWarning + header + "\n" + output,
|
|
484
|
+
text: corruptionWarning + header + "\n" + output + REMINDER_HINT,
|
|
462
485
|
}],
|
|
463
486
|
};
|
|
464
487
|
}
|
|
@@ -492,6 +515,82 @@ server.tool("reset_memory_cache", "Clear the session cache so all entries are tr
|
|
|
492
515
|
}],
|
|
493
516
|
};
|
|
494
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
|
+
});
|
|
495
594
|
// ---- Curator Tools (ceo role only) ----
|
|
496
595
|
const AUDIT_STATE_FILE = process.env.HMEM_AUDIT_STATE_PATH
|
|
497
596
|
|| path.join(PROJECT_DIR, "audit_state.json");
|
|
@@ -844,6 +943,12 @@ server.tool("mark_audited", "CURATOR ONLY (ceo role). Mark an agent as audited (
|
|
|
844
943
|
* V2 selection applies. Favorites/top-accessed show L2 children titles.
|
|
845
944
|
* Non-expanded entries show (N) child count indicator.
|
|
846
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
|
+
}
|
|
847
952
|
function formatTitlesOnly(entries, config) {
|
|
848
953
|
const CHILD_TITLE_LEN = 50;
|
|
849
954
|
const lines = [];
|
|
@@ -864,9 +969,11 @@ function formatTitlesOnly(entries, config) {
|
|
|
864
969
|
const obs = e.obsolete ? " [!]" : "";
|
|
865
970
|
const irr = e.irrelevant ? " [-]" : "";
|
|
866
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;
|
|
867
974
|
// Expanded entry (favorite/top-accessed): show with L2 children
|
|
868
|
-
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}`);
|
|
869
|
-
for (const child of
|
|
975
|
+
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}${formatTagSuffix(e.tags)}`);
|
|
976
|
+
for (const child of visibleChildren) {
|
|
870
977
|
const short = child.title || (child.content.length > CHILD_TITLE_LEN
|
|
871
978
|
? child.content.substring(0, CHILD_TITLE_LEN)
|
|
872
979
|
: child.content);
|
|
@@ -877,11 +984,14 @@ function formatTitlesOnly(entries, config) {
|
|
|
877
984
|
if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
|
|
878
985
|
lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
|
|
879
986
|
}
|
|
987
|
+
if (hiddenIrr > 0) {
|
|
988
|
+
lines.push(` (+${hiddenIrr} irrelevant hidden)`);
|
|
989
|
+
}
|
|
880
990
|
}
|
|
881
991
|
else {
|
|
882
992
|
// Non-expanded: compact line with child count
|
|
883
993
|
const childHint = (e.hiddenChildrenCount ?? 0) > 0 ? ` (${e.hiddenChildrenCount})` : "";
|
|
884
|
-
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}${childHint}`);
|
|
994
|
+
lines.push(`${e.id} ${mmdd}${fav}${obs} ${e.title}${formatTagSuffix(e.tags)}${childHint}`);
|
|
885
995
|
}
|
|
886
996
|
}
|
|
887
997
|
lines.push("");
|
|
@@ -942,19 +1052,22 @@ function formatFlatOutput(entries, curator, expand = false) {
|
|
|
942
1052
|
return lines.join("\n");
|
|
943
1053
|
}
|
|
944
1054
|
/** Favorite marker for child nodes. */
|
|
945
|
-
function
|
|
946
|
-
|
|
1055
|
+
function nodeMarkers(node) {
|
|
1056
|
+
const fav = node.favorite ? " [♥]" : "";
|
|
1057
|
+
const irr = node.irrelevant ? " [-]" : "";
|
|
1058
|
+
return `${fav}${irr}`;
|
|
947
1059
|
}
|
|
948
1060
|
function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
949
1061
|
const isNode = e.id.includes(".");
|
|
950
1062
|
const hasDetail = !!(e.children?.length || e.linkedEntries?.length);
|
|
1063
|
+
const tagStr = formatTagSuffix(e.tags);
|
|
951
1064
|
// Headline: use title for navigation, show full content below when drilling in
|
|
952
1065
|
if (isNode) {
|
|
953
1066
|
if (curator) {
|
|
954
|
-
lines.push(`[${e.id}] ${e.title}`);
|
|
1067
|
+
lines.push(`[${e.id}] ${e.title}${tagStr}`);
|
|
955
1068
|
}
|
|
956
1069
|
else {
|
|
957
|
-
lines.push(`${e.id} ${e.title}`);
|
|
1070
|
+
lines.push(`${e.id} ${e.title}${tagStr}`);
|
|
958
1071
|
}
|
|
959
1072
|
// Node drilldown: show full content below title
|
|
960
1073
|
if (hasDetail && e.level_1 !== e.title) {
|
|
@@ -964,50 +1077,56 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
964
1077
|
else {
|
|
965
1078
|
if (curator) {
|
|
966
1079
|
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : "";
|
|
1080
|
+
const pinnedTag = e.pinned ? " [P]" : "";
|
|
967
1081
|
const obsoleteTag = e.obsolete ? " [⚠ OBSOLETE]" : "";
|
|
968
1082
|
const irrelevantTag = e.irrelevant ? " [- IRRELEVANT]" : "";
|
|
969
1083
|
const date = e.created_at.substring(0, 10);
|
|
970
1084
|
const accessed = e.access_count > 0 ? ` (${e.access_count}x accessed)` : "";
|
|
971
1085
|
const roleTag = e.min_role !== "worker" ? ` [${e.min_role}+]` : "";
|
|
972
|
-
lines.push(`[${e.id}] ${date}${roleTag}${promotedTag}${obsoleteTag}${irrelevantTag}${accessed}`);
|
|
973
|
-
lines.push(` ${e.title}`);
|
|
1086
|
+
lines.push(`[${e.id}] ${date}${roleTag}${promotedTag}${pinnedTag}${obsoleteTag}${irrelevantTag}${accessed}`);
|
|
1087
|
+
lines.push(` ${e.title}${tagStr}`);
|
|
974
1088
|
}
|
|
975
1089
|
else {
|
|
976
1090
|
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : "";
|
|
1091
|
+
const pinnedTag = e.pinned ? " [P]" : "";
|
|
977
1092
|
const obsoleteTag = e.obsolete ? " [!]" : "";
|
|
978
1093
|
const irrelevantTag = e.irrelevant ? " [-]" : "";
|
|
979
1094
|
const mmdd = e.created_at.substring(5, 10);
|
|
980
|
-
lines.push(`${e.id} ${mmdd}${promotedTag}${obsoleteTag}${irrelevantTag} ${e.title}`);
|
|
1095
|
+
lines.push(`${e.id} ${mmdd}${promotedTag}${pinnedTag}${obsoleteTag}${irrelevantTag} ${e.title}${tagStr}`);
|
|
981
1096
|
}
|
|
982
1097
|
// Show full level_1 content below title when entry is expanded/drilled
|
|
983
1098
|
if (hasDetail && e.level_1 !== e.title) {
|
|
984
1099
|
lines.push(` ${e.level_1}`);
|
|
985
1100
|
}
|
|
986
1101
|
}
|
|
987
|
-
// Children
|
|
1102
|
+
// Children — filter out irrelevant nodes
|
|
988
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;
|
|
989
1106
|
if (expand) {
|
|
990
1107
|
// Expand mode: full content + recursive children
|
|
991
|
-
renderChildrenExpanded(lines,
|
|
1108
|
+
renderChildrenExpanded(lines, visibleChildren, curator);
|
|
992
1109
|
}
|
|
993
1110
|
else if (e.expanded && !expand) {
|
|
994
|
-
renderChildrenFormatted(lines,
|
|
1111
|
+
renderChildrenFormatted(lines, visibleChildren, curator);
|
|
995
1112
|
if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
|
|
996
1113
|
lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
|
|
997
1114
|
}
|
|
998
1115
|
}
|
|
999
1116
|
else if (e.hiddenChildrenCount !== undefined) {
|
|
1000
|
-
// Non-expanded bulk read: show only the latest child title
|
|
1001
|
-
const child =
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
}
|
|
1011
1130
|
}
|
|
1012
1131
|
if (e.hiddenChildrenCount > 0) {
|
|
1013
1132
|
lines.push(` [+${e.hiddenChildrenCount} more → ${e.id}]`);
|
|
@@ -1015,7 +1134,10 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1015
1134
|
}
|
|
1016
1135
|
else {
|
|
1017
1136
|
// ID-based read: show all direct children as titles
|
|
1018
|
-
renderChildrenFormatted(lines,
|
|
1137
|
+
renderChildrenFormatted(lines, visibleChildren, curator);
|
|
1138
|
+
}
|
|
1139
|
+
if (hiddenIrrelevant > 0) {
|
|
1140
|
+
lines.push(` (+${hiddenIrrelevant} irrelevant hidden)`);
|
|
1019
1141
|
}
|
|
1020
1142
|
}
|
|
1021
1143
|
// Links
|
|
@@ -1049,11 +1171,19 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1049
1171
|
const hint = (lchild.child_count ?? 0) > 0
|
|
1050
1172
|
? ` (${lchild.child_count} ${lchild.child_count === 1 ? "child" : "children"} — use id="${lchild.id}" to expand)`
|
|
1051
1173
|
: "";
|
|
1052
|
-
lines.push(` [${lchild.id}]${
|
|
1174
|
+
lines.push(` [${lchild.id}]${nodeMarkers(lchild)} ${lchild.title}${hint}`);
|
|
1053
1175
|
}
|
|
1054
1176
|
}
|
|
1055
1177
|
}
|
|
1056
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
|
+
}
|
|
1057
1187
|
lines.push("");
|
|
1058
1188
|
}
|
|
1059
1189
|
/**
|
|
@@ -1063,15 +1193,16 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1063
1193
|
function renderChildrenFormatted(lines, children, curator) {
|
|
1064
1194
|
for (const child of children) {
|
|
1065
1195
|
const indent = " ".repeat(child.depth - 1);
|
|
1066
|
-
const fav =
|
|
1196
|
+
const fav = nodeMarkers(child);
|
|
1197
|
+
const ctags = formatTagSuffix(child.tags);
|
|
1067
1198
|
const hint = (child.child_count ?? 0) > 0
|
|
1068
1199
|
? ` [+${child.child_count} → ${child.id}]`
|
|
1069
1200
|
: "";
|
|
1070
1201
|
if (curator) {
|
|
1071
|
-
lines.push(`${indent}[${child.id}]${fav} ${child.title}${hint}`);
|
|
1202
|
+
lines.push(`${indent}[${child.id}]${fav} ${child.title}${ctags}${hint}`);
|
|
1072
1203
|
}
|
|
1073
1204
|
else {
|
|
1074
|
-
lines.push(`${indent}${child.id}${fav} ${child.title}${hint}`);
|
|
1205
|
+
lines.push(`${indent}${child.id}${fav} ${child.title}${ctags}${hint}`);
|
|
1075
1206
|
}
|
|
1076
1207
|
// Don't recurse into grandchildren — titles only, drill for content
|
|
1077
1208
|
}
|
|
@@ -1085,8 +1216,9 @@ function renderChildrenFormatted(lines, children, curator) {
|
|
|
1085
1216
|
function renderChildrenExpanded(lines, children, curator) {
|
|
1086
1217
|
for (const child of children) {
|
|
1087
1218
|
const indent = " ".repeat(child.depth - 1);
|
|
1088
|
-
const fav =
|
|
1089
|
-
const
|
|
1219
|
+
const fav = nodeMarkers(child);
|
|
1220
|
+
const visibleGrandchildren = child.children?.filter(c => !c.irrelevant);
|
|
1221
|
+
const hasLoadedChildren = visibleGrandchildren && visibleGrandchildren.length > 0;
|
|
1090
1222
|
const isBoundary = !hasLoadedChildren && (child.child_count ?? 0) > 0;
|
|
1091
1223
|
if (hasLoadedChildren) {
|
|
1092
1224
|
// Inner node: full content + recurse
|
|
@@ -1096,7 +1228,7 @@ function renderChildrenExpanded(lines, children, curator) {
|
|
|
1096
1228
|
else {
|
|
1097
1229
|
lines.push(`${indent}${child.id}${fav} ${child.content}`);
|
|
1098
1230
|
}
|
|
1099
|
-
renderChildrenExpanded(lines,
|
|
1231
|
+
renderChildrenExpanded(lines, visibleGrandchildren, curator);
|
|
1100
1232
|
}
|
|
1101
1233
|
else if (isBoundary) {
|
|
1102
1234
|
// Boundary: title only + child count hint
|