hmem-mcp 6.0.3 → 6.1.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/dist/hmem-store.d.ts +8 -0
- package/dist/hmem-store.js +107 -13
- package/dist/hmem-store.js.map +1 -1
- package/dist/mcp-server.js +250 -19
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/skills/hmem-config/SKILL.md +1 -1
- package/skills/hmem-curate/SKILL.md +4 -4
- package/skills/hmem-new-project/SKILL.md +2 -2
- package/skills/hmem-read/SKILL.md +1 -1
- package/skills/hmem-self-curate/SKILL.md +2 -2
- package/skills/hmem-setup/SKILL.md +2 -2
- package/skills/hmem-update/SKILL.md +2 -2
- package/skills/hmem-write/SKILL.md +40 -17
package/dist/hmem-store.d.ts
CHANGED
|
@@ -425,6 +425,14 @@ export declare class HmemStore {
|
|
|
425
425
|
*/
|
|
426
426
|
private parseTimeWindow;
|
|
427
427
|
private nextSeq;
|
|
428
|
+
/** Read-only preview of the next root ID that write() would assign for this prefix.
|
|
429
|
+
* Used by mcp-server's id-reservation loop (multi-agent collision prevention). */
|
|
430
|
+
peekNextId(prefix: string): string;
|
|
431
|
+
/** Read-only preview of the top-level child IDs that appendChildren() would create.
|
|
432
|
+
* Used by mcp-server's sub-node reservation loop (multi-agent collision prevention).
|
|
433
|
+
* Returns the IDs of direct children only — nested grandchildren don't need separate
|
|
434
|
+
* reservation because they're parented under nodes this same call would create. */
|
|
435
|
+
peekAppendTopLevelIds(parentId: string, content: string): string[];
|
|
428
436
|
/** Clear all active markers — called at MCP server start so each session starts neutral. */
|
|
429
437
|
clearAllActive(): void;
|
|
430
438
|
/** Auto-resolve linked entries on an entry (extracted for reuse in chain resolution). */
|
package/dist/hmem-store.js
CHANGED
|
@@ -361,6 +361,24 @@ export class HmemStore {
|
|
|
361
361
|
if (prefix === "P") {
|
|
362
362
|
this.db.prepare("UPDATE memories SET active = 1 WHERE id = ?").run(rootId);
|
|
363
363
|
}
|
|
364
|
+
// Auto-scaffold E-entries: create standard L2 structure when no children provided
|
|
365
|
+
if (prefix === "E" && nodes.length === 0) {
|
|
366
|
+
const eSchema = ["Analysis", "Possible fixes", "Fixing attempts", "Solution", "Cause", "Key Learnings"];
|
|
367
|
+
for (let i = 0; i < eSchema.length; i++) {
|
|
368
|
+
const nodeId = `${rootId}.${i + 1}`;
|
|
369
|
+
insertNode.run(nodeId, rootId, rootId, 2, i + 1, eSchema[i], eSchema[i], timestamp, timestamp);
|
|
370
|
+
}
|
|
371
|
+
// Move L1 body into .1 Analysis as content (the short description stays on L1)
|
|
372
|
+
if (level1 !== title) {
|
|
373
|
+
// level1 contains body text — move it to Analysis node
|
|
374
|
+
this.db.prepare("UPDATE memory_nodes SET content = ?, title = ? WHERE id = ?")
|
|
375
|
+
.run(level1, "Analysis", `${rootId}.1`);
|
|
376
|
+
}
|
|
377
|
+
// Auto-add #open tag on root (visible in bulk-read title line)
|
|
378
|
+
if (!validatedTags.includes("#open")) {
|
|
379
|
+
this.addTag(rootId, "#open");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
364
382
|
})();
|
|
365
383
|
return { id: rootId, timestamp };
|
|
366
384
|
}
|
|
@@ -714,7 +732,7 @@ export class HmemStore {
|
|
|
714
732
|
// Step 0.5: Detect active-prefixes — prefixes where at least one entry has active=1.
|
|
715
733
|
// Non-active entries in these prefixes are still shown (as compact titles) but don't get expansion slots.
|
|
716
734
|
// P and I prefixes ALWAYS treated as active-prefix — only expand when explicitly activated.
|
|
717
|
-
const activePrefixes = new Set(["P", "I"]);
|
|
735
|
+
const activePrefixes = new Set(["P", "I", "E"]);
|
|
718
736
|
for (const r of activeRows) {
|
|
719
737
|
if (r.active === 1)
|
|
720
738
|
activePrefixes.add(r.prefix);
|
|
@@ -1608,14 +1626,21 @@ export class HmemStore {
|
|
|
1608
1626
|
if (trimmed.length > nodeLimit * HmemStore.CHAR_LIMIT_TOLERANCE) {
|
|
1609
1627
|
throw new Error(`Content exceeds ${nodeLimit} character limit (${trimmed.length} chars) for L${nodeRow.depth}.`);
|
|
1610
1628
|
}
|
|
1611
|
-
// Parse
|
|
1629
|
+
// Parse body: "> " prefix (legacy) or blank-line separator (git-commit style)
|
|
1612
1630
|
const lines = trimmed.split("\n");
|
|
1613
1631
|
const titleLines = [];
|
|
1614
1632
|
const bodyLines = [];
|
|
1633
|
+
let bodyMode = false;
|
|
1615
1634
|
for (const line of lines) {
|
|
1616
1635
|
if (line.startsWith("> ") || line === ">") {
|
|
1617
1636
|
bodyLines.push(line.replace(/^> ?/, ""));
|
|
1618
1637
|
}
|
|
1638
|
+
else if (line === "" && titleLines.length > 0) {
|
|
1639
|
+
bodyMode = true;
|
|
1640
|
+
}
|
|
1641
|
+
else if (bodyMode) {
|
|
1642
|
+
bodyLines.push(line);
|
|
1643
|
+
}
|
|
1619
1644
|
else {
|
|
1620
1645
|
titleLines.push(line);
|
|
1621
1646
|
}
|
|
@@ -1671,18 +1696,26 @@ export class HmemStore {
|
|
|
1671
1696
|
else {
|
|
1672
1697
|
// Root entry in memories
|
|
1673
1698
|
if (trimmed) {
|
|
1674
|
-
// Split into title lines, body lines (>
|
|
1699
|
+
// Split into title lines, body lines ("> " legacy or blank-line separator), and child lines (indented)
|
|
1675
1700
|
const lines = trimmed.split("\n");
|
|
1676
1701
|
const titleLines = [];
|
|
1677
1702
|
const bodyLines = [];
|
|
1678
1703
|
const childLines = [];
|
|
1704
|
+
let bodyMode = false;
|
|
1679
1705
|
for (const line of lines) {
|
|
1680
1706
|
if (line.startsWith("\t") || (line.length > 0 && line[0] === " " && line.trimStart() !== line)) {
|
|
1681
1707
|
childLines.push(line);
|
|
1708
|
+
bodyMode = false; // indented line exits body mode
|
|
1682
1709
|
}
|
|
1683
1710
|
else if (line.startsWith("> ") || line === ">") {
|
|
1684
1711
|
bodyLines.push(line.replace(/^> ?/, ""));
|
|
1685
1712
|
}
|
|
1713
|
+
else if (line === "" && titleLines.length > 0 && childLines.length === 0) {
|
|
1714
|
+
bodyMode = true;
|
|
1715
|
+
}
|
|
1716
|
+
else if (bodyMode) {
|
|
1717
|
+
bodyLines.push(line);
|
|
1718
|
+
}
|
|
1686
1719
|
else {
|
|
1687
1720
|
titleLines.push(line);
|
|
1688
1721
|
}
|
|
@@ -2305,6 +2338,34 @@ export class HmemStore {
|
|
|
2305
2338
|
const row = this.db.prepare("SELECT MAX(seq) as maxSeq FROM memories WHERE prefix = ?").get(prefix);
|
|
2306
2339
|
return (row?.maxSeq || 0) + 1;
|
|
2307
2340
|
}
|
|
2341
|
+
/** Read-only preview of the next root ID that write() would assign for this prefix.
|
|
2342
|
+
* Used by mcp-server's id-reservation loop (multi-agent collision prevention). */
|
|
2343
|
+
peekNextId(prefix) {
|
|
2344
|
+
prefix = prefix.toUpperCase();
|
|
2345
|
+
const seq = this.nextSeq(prefix);
|
|
2346
|
+
return `${prefix}${String(seq).padStart(4, "0")}`;
|
|
2347
|
+
}
|
|
2348
|
+
/** Read-only preview of the top-level child IDs that appendChildren() would create.
|
|
2349
|
+
* Used by mcp-server's sub-node reservation loop (multi-agent collision prevention).
|
|
2350
|
+
* Returns the IDs of direct children only — nested grandchildren don't need separate
|
|
2351
|
+
* reservation because they're parented under nodes this same call would create. */
|
|
2352
|
+
peekAppendTopLevelIds(parentId, content) {
|
|
2353
|
+
const parentIsRoot = !parentId.includes(".");
|
|
2354
|
+
// Verify parent exists (mirrors appendChildren guard, but throws same shape)
|
|
2355
|
+
if (parentIsRoot) {
|
|
2356
|
+
if (!this.db.prepare("SELECT id FROM memories WHERE id = ?").get(parentId))
|
|
2357
|
+
return [];
|
|
2358
|
+
}
|
|
2359
|
+
else {
|
|
2360
|
+
if (!this.db.prepare("SELECT id FROM memory_nodes WHERE id = ?").get(parentId))
|
|
2361
|
+
return [];
|
|
2362
|
+
}
|
|
2363
|
+
const parentDepth = parentIsRoot ? 1 : (parentId.match(/\./g).length + 1);
|
|
2364
|
+
const maxSeqRow = this.db.prepare("SELECT MAX(seq) as maxSeq FROM memory_nodes WHERE parent_id = ?").get(parentId);
|
|
2365
|
+
const startSeq = (maxSeqRow?.maxSeq ?? 0) + 1;
|
|
2366
|
+
const nodes = this.parseRelativeTree(content, parentId, parentDepth, startSeq);
|
|
2367
|
+
return nodes.filter(n => n.parent_id === parentId).map(n => n.id);
|
|
2368
|
+
}
|
|
2308
2369
|
/** Clear all active markers — called at MCP server start so each session starts neutral. */
|
|
2309
2370
|
clearAllActive() {
|
|
2310
2371
|
this.db.prepare("UPDATE memories SET active = 0 WHERE active = 1").run();
|
|
@@ -3178,8 +3239,12 @@ export class HmemStore {
|
|
|
3178
3239
|
const nodes = [];
|
|
3179
3240
|
const l1Title = [];
|
|
3180
3241
|
const l1Body = [];
|
|
3242
|
+
let l1BodyMode = false; // true after blank line at L1 depth
|
|
3181
3243
|
// Auto-detect space indentation unit: use first indented line (if no tabs present)
|
|
3182
|
-
const rawLines = content.split("\n").map(l => l.trimEnd())
|
|
3244
|
+
const rawLines = content.split("\n").map(l => l.trimEnd());
|
|
3245
|
+
// Keep blank lines for body detection but trim trailing empties
|
|
3246
|
+
while (rawLines.length > 0 && rawLines[rawLines.length - 1] === "")
|
|
3247
|
+
rawLines.pop();
|
|
3183
3248
|
let spaceUnit = 4;
|
|
3184
3249
|
if (!rawLines.some(l => l.startsWith("\t"))) {
|
|
3185
3250
|
for (const l of rawLines) {
|
|
@@ -3190,10 +3255,19 @@ export class HmemStore {
|
|
|
3190
3255
|
}
|
|
3191
3256
|
}
|
|
3192
3257
|
}
|
|
3258
|
+
// Track body mode per depth: after a blank line, subsequent lines at that depth are body
|
|
3259
|
+
const bodyModeAtDepth = new Map();
|
|
3193
3260
|
for (const line of rawLines) {
|
|
3194
3261
|
const trimmedEnd = line;
|
|
3195
|
-
|
|
3262
|
+
// Blank line: activate body mode for L1 and for the last node's depth
|
|
3263
|
+
if (!trimmedEnd) {
|
|
3264
|
+
l1BodyMode = true;
|
|
3265
|
+
// Activate body mode for the last node's depth (L2+)
|
|
3266
|
+
if (nodes.length > 0) {
|
|
3267
|
+
bodyModeAtDepth.set(nodes[nodes.length - 1].depth, true);
|
|
3268
|
+
}
|
|
3196
3269
|
continue;
|
|
3270
|
+
}
|
|
3197
3271
|
// Count leading tabs; fall back to auto-detected space unit
|
|
3198
3272
|
const tabMatch = trimmedEnd.match(/^\t*/);
|
|
3199
3273
|
const leadingTabs = tabMatch ? tabMatch[0].length : 0;
|
|
@@ -3207,9 +3281,11 @@ export class HmemStore {
|
|
|
3207
3281
|
depth = spaceTabs > 0 ? Math.min(spaceTabs, 4) + 1 : 1;
|
|
3208
3282
|
}
|
|
3209
3283
|
const text = trimmedEnd.trim();
|
|
3210
|
-
// Body line detection: "> " prefix
|
|
3211
|
-
const
|
|
3212
|
-
const
|
|
3284
|
+
// Body line detection: "> " prefix (legacy) OR blank-line-activated body mode
|
|
3285
|
+
const isLegacyBody = text.startsWith("> ") || text === ">";
|
|
3286
|
+
const isBlankLineBody = depth === 1 ? l1BodyMode : bodyModeAtDepth.get(depth) === true;
|
|
3287
|
+
const isBodyLine = isLegacyBody || isBlankLineBody;
|
|
3288
|
+
const bodyText = isLegacyBody ? text.replace(/^> ?/, "") : text;
|
|
3213
3289
|
if (depth === 1) {
|
|
3214
3290
|
if (isBodyLine) {
|
|
3215
3291
|
l1Body.push(bodyText);
|
|
@@ -3219,6 +3295,10 @@ export class HmemStore {
|
|
|
3219
3295
|
}
|
|
3220
3296
|
continue;
|
|
3221
3297
|
}
|
|
3298
|
+
// Depth changed → exit body mode for other depths
|
|
3299
|
+
if (!isBodyLine) {
|
|
3300
|
+
bodyModeAtDepth.delete(depth);
|
|
3301
|
+
}
|
|
3222
3302
|
if (isBodyLine) {
|
|
3223
3303
|
// Append body to the last node at this depth
|
|
3224
3304
|
const lastNode = nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
@@ -3233,6 +3313,8 @@ export class HmemStore {
|
|
|
3233
3313
|
}
|
|
3234
3314
|
continue;
|
|
3235
3315
|
}
|
|
3316
|
+
// New node resets body mode for this depth
|
|
3317
|
+
bodyModeAtDepth.delete(depth);
|
|
3236
3318
|
// L2+: determine parent and generate compound ID
|
|
3237
3319
|
const parentId = depth === 2 ? rootId : (lastIdAtDepth.get(depth - 1) ?? rootId);
|
|
3238
3320
|
const seq = (seqAtParent.get(parentId) ?? 0) + 1;
|
|
@@ -3278,7 +3360,9 @@ export class HmemStore {
|
|
|
3278
3360
|
seqAtParent.set(parentId, startSeq - 1);
|
|
3279
3361
|
const lastIdAtRelDepth = new Map();
|
|
3280
3362
|
const nodes = [];
|
|
3281
|
-
const rawLines = content.split("\n").map(l => l.trimEnd())
|
|
3363
|
+
const rawLines = content.split("\n").map(l => l.trimEnd());
|
|
3364
|
+
while (rawLines.length > 0 && rawLines[rawLines.length - 1] === "")
|
|
3365
|
+
rawLines.pop();
|
|
3282
3366
|
// Auto-detect space unit if no tabs used
|
|
3283
3367
|
let spaceUnit = 4;
|
|
3284
3368
|
if (!rawLines.some(l => l.startsWith("\t"))) {
|
|
@@ -3291,10 +3375,16 @@ export class HmemStore {
|
|
|
3291
3375
|
}
|
|
3292
3376
|
}
|
|
3293
3377
|
const maxAbsDepth = this.cfg.maxDepth;
|
|
3378
|
+
const bodyModeAtDepth = new Map();
|
|
3294
3379
|
for (const line of rawLines) {
|
|
3295
3380
|
const text = line.trim();
|
|
3296
|
-
|
|
3381
|
+
// Blank line: activate body mode for the last node's depth
|
|
3382
|
+
if (!text) {
|
|
3383
|
+
if (nodes.length > 0) {
|
|
3384
|
+
bodyModeAtDepth.set(nodes[nodes.length - 1].depth, true);
|
|
3385
|
+
}
|
|
3297
3386
|
continue;
|
|
3387
|
+
}
|
|
3298
3388
|
// Count leading tabs; fall back to space-based detection
|
|
3299
3389
|
const tabMatch = line.match(/^\t*/);
|
|
3300
3390
|
const leadingTabs = tabMatch ? tabMatch[0].length : 0;
|
|
@@ -3309,10 +3399,12 @@ export class HmemStore {
|
|
|
3309
3399
|
const absDepth = parentDepth + 1 + relDepth;
|
|
3310
3400
|
if (absDepth > maxAbsDepth)
|
|
3311
3401
|
continue; // silently skip beyond max depth
|
|
3312
|
-
// Body line detection: "> " prefix
|
|
3313
|
-
const
|
|
3402
|
+
// Body line detection: "> " prefix (legacy) OR blank-line-activated body mode
|
|
3403
|
+
const isLegacyBody = text.startsWith("> ") || text === ">";
|
|
3404
|
+
const isBlankLineBody = bodyModeAtDepth.get(absDepth) === true;
|
|
3405
|
+
const isBodyLine = isLegacyBody || isBlankLineBody;
|
|
3314
3406
|
if (isBodyLine) {
|
|
3315
|
-
const bodyText = text.replace(/^> ?/, "");
|
|
3407
|
+
const bodyText = isLegacyBody ? text.replace(/^> ?/, "") : text;
|
|
3316
3408
|
const lastNode = nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
3317
3409
|
if (lastNode && lastNode.depth === absDepth) {
|
|
3318
3410
|
if (lastNode.content === lastNode.title) {
|
|
@@ -3324,6 +3416,8 @@ export class HmemStore {
|
|
|
3324
3416
|
}
|
|
3325
3417
|
continue;
|
|
3326
3418
|
}
|
|
3419
|
+
// New node resets body mode for this depth
|
|
3420
|
+
bodyModeAtDepth.delete(absDepth);
|
|
3327
3421
|
const myParentId = relDepth === 0
|
|
3328
3422
|
? parentId
|
|
3329
3423
|
: (lastIdAtRelDepth.get(relDepth - 1) ?? parentId);
|