hmem-mcp 4.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.
- package/README.md +161 -205
- package/dist/cli-checkpoint.d.ts +16 -0
- package/dist/cli-checkpoint.js +233 -0
- package/dist/cli-checkpoint.js.map +1 -0
- package/dist/cli-context-inject.d.ts +19 -0
- package/dist/cli-context-inject.js +77 -0
- package/dist/cli-context-inject.js.map +1 -0
- package/dist/cli-env.d.ts +16 -0
- package/dist/cli-env.js +40 -0
- package/dist/cli-env.js.map +1 -0
- package/dist/cli-hook-startup.d.ts +20 -0
- package/dist/cli-hook-startup.js +101 -0
- package/dist/cli-hook-startup.js.map +1 -0
- package/dist/cli-init.js +148 -10
- package/dist/cli-init.js.map +1 -1
- package/dist/cli-log-exchange.js +87 -23
- package/dist/cli-log-exchange.js.map +1 -1
- package/dist/cli-statusline.d.ts +14 -0
- package/dist/cli-statusline.js +172 -0
- package/dist/cli-statusline.js.map +1 -0
- package/dist/cli.js +30 -2
- package/dist/cli.js.map +1 -1
- package/dist/hmem-config.d.ts +31 -0
- package/dist/hmem-config.js +76 -12
- package/dist/hmem-config.js.map +1 -1
- package/dist/hmem-store.d.ts +62 -1
- package/dist/hmem-store.js +364 -46
- package/dist/hmem-store.js.map +1 -1
- package/dist/mcp-server.js +405 -99
- package/dist/mcp-server.js.map +1 -1
- package/dist/session-cache.d.ts +11 -0
- package/dist/session-cache.js +25 -0
- package/dist/session-cache.js.map +1 -1
- package/package.json +1 -1
- package/scripts/autoresearch-nightly.sh +84 -0
- package/scripts/hmem-statusline.sh +4 -0
- package/skills/hmem-config/SKILL.md +112 -147
- package/skills/hmem-curate/SKILL.md +56 -6
- package/skills/hmem-new-project/SKILL.md +164 -0
- package/skills/hmem-read/SKILL.md +174 -146
- package/skills/hmem-release/SKILL.md +141 -0
- package/skills/hmem-self-curate/SKILL.md +49 -7
- package/skills/hmem-setup/SKILL.md +169 -87
- package/skills/hmem-sync-setup/SKILL.md +16 -3
- package/skills/hmem-update/SKILL.md +254 -0
- package/skills/hmem-wipe/SKILL.md +75 -0
- package/skills/hmem-write/SKILL.md +113 -61
package/dist/mcp-server.js
CHANGED
|
@@ -114,15 +114,16 @@ function syncPull(hmemPath) {
|
|
|
114
114
|
continue;
|
|
115
115
|
const result = spawnSync("hmem-sync", [
|
|
116
116
|
"pull", "--config", hmemSyncConfig(hmemPath),
|
|
117
|
+
"--hmem-path", hmemPath,
|
|
117
118
|
"--server-url", s.serverUrl, "--token", s.token,
|
|
118
|
-
], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32" });
|
|
119
|
+
], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true });
|
|
119
120
|
if (result.error)
|
|
120
121
|
process.stderr.write(`hmem-sync pull error (${s.name ?? s.serverUrl}): ${result.error.message}\n`);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
else {
|
|
124
|
-
const result = spawnSync("hmem-sync", ["pull", "--config", hmemSyncConfig(hmemPath)], {
|
|
125
|
-
env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32",
|
|
125
|
+
const result = spawnSync("hmem-sync", ["pull", "--config", hmemSyncConfig(hmemPath), "--hmem-path", hmemPath], {
|
|
126
|
+
env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true,
|
|
126
127
|
});
|
|
127
128
|
if (result.error)
|
|
128
129
|
process.stderr.write(`hmem-sync pull error: ${result.error.message}\n`);
|
|
@@ -172,13 +173,14 @@ function syncPullThenPush(hmemPath) {
|
|
|
172
173
|
continue;
|
|
173
174
|
spawnSync("hmem-sync", [
|
|
174
175
|
"pull", "--config", hmemSyncConfig(hmemPath),
|
|
176
|
+
"--hmem-path", hmemPath,
|
|
175
177
|
"--server-url", s.serverUrl, "--token", s.token,
|
|
176
|
-
], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32" });
|
|
178
|
+
], { env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true });
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
else {
|
|
180
|
-
spawnSync("hmem-sync", ["pull", "--config", hmemSyncConfig(hmemPath)], {
|
|
181
|
-
env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32",
|
|
182
|
+
spawnSync("hmem-sync", ["pull", "--config", hmemSyncConfig(hmemPath), "--hmem-path", hmemPath], {
|
|
183
|
+
env: { ...process.env }, encoding: "utf8", shell: process.platform === "win32", windowsHide: true,
|
|
182
184
|
});
|
|
183
185
|
}
|
|
184
186
|
lastPullAt = Date.now();
|
|
@@ -193,14 +195,15 @@ function syncPush(hmemPath) {
|
|
|
193
195
|
continue;
|
|
194
196
|
const child = spawn("hmem-sync", [
|
|
195
197
|
"push", "--config", hmemSyncConfig(hmemPath),
|
|
198
|
+
"--hmem-path", hmemPath,
|
|
196
199
|
"--server-url", s.serverUrl, "--token", s.token,
|
|
197
|
-
], { 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 });
|
|
198
201
|
child.unref();
|
|
199
202
|
}
|
|
200
203
|
}
|
|
201
204
|
else {
|
|
202
|
-
const child = spawn("hmem-sync", ["push", "--config", hmemSyncConfig(hmemPath)], {
|
|
203
|
-
env: { ...process.env }, shell: process.platform === "win32", stdio: "ignore", detached: true,
|
|
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, windowsHide: true,
|
|
204
207
|
});
|
|
205
208
|
child.unref();
|
|
206
209
|
}
|
|
@@ -208,8 +211,127 @@ function syncPush(hmemPath) {
|
|
|
208
211
|
// Load hmem config (hmem.config.json in project dir, falls back to defaults)
|
|
209
212
|
const hmemConfig = loadHmemConfig(PROJECT_DIR);
|
|
210
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();
|
|
211
263
|
// Session-scoped cache — persists across tool calls within this MCP connection
|
|
212
264
|
const sessionCache = new SessionCache();
|
|
265
|
+
const CONTEXT_THRESHOLD_WARNING = "\n\n⚠ CONTEXT THRESHOLD REACHED (~{tokens}k tokens delivered this session).\n" +
|
|
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.";
|
|
268
|
+
const CACHE_RESET_SIGNAL = "/tmp/hmem-cache-reset-signal";
|
|
269
|
+
/** Track tokens in a tool response and append threshold warning if needed. */
|
|
270
|
+
function trackTokens(result) {
|
|
271
|
+
// Check for /clear signal from hook
|
|
272
|
+
if (fs.existsSync(CACHE_RESET_SIGNAL)) {
|
|
273
|
+
try {
|
|
274
|
+
fs.unlinkSync(CACHE_RESET_SIGNAL);
|
|
275
|
+
}
|
|
276
|
+
catch { }
|
|
277
|
+
sessionCache.reset();
|
|
278
|
+
log("Session cache reset via /clear signal");
|
|
279
|
+
}
|
|
280
|
+
if (result.isError)
|
|
281
|
+
return result;
|
|
282
|
+
const text = result.content.map(c => c.text).join("");
|
|
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
|
+
}
|
|
289
|
+
if (sessionCache.checkThreshold(hmemConfig.contextTokenThreshold)) {
|
|
290
|
+
const tokK = Math.round(sessionCache.totalTokensDelivered / 1000);
|
|
291
|
+
result.content[result.content.length - 1].text += CONTEXT_THRESHOLD_WARNING.replace("{tokens}", String(tokK));
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Format recent O-entries block: latest O-entry with full exchanges, rest as titles.
|
|
297
|
+
* @param store - HmemStore instance
|
|
298
|
+
* @param limit - total O-entries to show
|
|
299
|
+
* @param exchangeCount - number of exchanges to show from the latest O-entry
|
|
300
|
+
* @param linkedTo - optional project ID filter
|
|
301
|
+
* @returns formatted string + list of O-entry IDs for cache registration
|
|
302
|
+
*/
|
|
303
|
+
function formatRecentOEntries(store, limit, exchangeCount, linkedTo, expandAll) {
|
|
304
|
+
if (limit <= 0)
|
|
305
|
+
return { text: "", ids: [] };
|
|
306
|
+
const recentO = store.getRecentOEntries(limit, linkedTo);
|
|
307
|
+
if (recentO.length === 0)
|
|
308
|
+
return { text: "", ids: [] };
|
|
309
|
+
const lines = ["Recent sessions:"];
|
|
310
|
+
const ids = recentO.map(o => o.id);
|
|
311
|
+
for (let i = 0; i < recentO.length; i++) {
|
|
312
|
+
const o = recentO[i];
|
|
313
|
+
lines.push(` ${o.id} ${o.created_at.substring(0, 10)} ${o.title}`);
|
|
314
|
+
// Expand exchanges: all entries when expandAll, otherwise only latest
|
|
315
|
+
if (expandAll || i === 0) {
|
|
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);
|
|
324
|
+
for (const ex of exchanges) {
|
|
325
|
+
const userShort = ex.userText.length > 300 ? ex.userText.substring(0, 300) + "..." : ex.userText;
|
|
326
|
+
const agentShort = ex.agentText.length > 500 ? ex.agentText.substring(0, 500) + "..." : ex.agentText;
|
|
327
|
+
lines.push(` USER: ${userShort}`);
|
|
328
|
+
if (agentShort)
|
|
329
|
+
lines.push(` AGENT: ${agentShort}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { text: lines.join("\n"), ids };
|
|
334
|
+
}
|
|
213
335
|
// ---- Server ----
|
|
214
336
|
const server = new McpServer({
|
|
215
337
|
name: "hmem",
|
|
@@ -277,26 +399,30 @@ server.tool("write_memory", "Write a new memory entry to your hierarchical long-
|
|
|
277
399
|
" Level 3: 2 tabs — even more detail\n" +
|
|
278
400
|
" Level 4: 3 tabs — fine-grained detail\n" +
|
|
279
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" +
|
|
280
404
|
"The system auto-assigns an ID and timestamp. " +
|
|
281
405
|
`Use prefix to categorize: ${prefixList}.\n\n` +
|
|
282
406
|
"Store types:\n" +
|
|
283
407
|
" personal (default): Your private memory\n", {
|
|
284
408
|
prefix: z.string().toUpperCase().describe(`Memory category: ${prefixList}`),
|
|
285
|
-
content: z.string().min(3).describe("The memory content. Use tab indentation for depth levels.
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"\
|
|
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"),
|
|
290
416
|
links: z.array(z.string()).optional().describe("Optional: IDs of related memories, e.g. ['P0001', 'L0005']"),
|
|
291
|
-
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. " +
|
|
292
418
|
"Use for reference info you need to see every session, regardless of category."),
|
|
293
419
|
tags: z.array(z.string()).min(1).describe("Required hashtags for cross-cutting search (min 1, recommend 3+). " +
|
|
294
420
|
"E.g. ['#hmem', '#curation']. Max 10, lowercase, must start with #. Shown after title in reads."),
|
|
295
|
-
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. " +
|
|
296
422
|
"Use for reference entries you need to see in full every session."),
|
|
297
423
|
store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' or 'company'"),
|
|
298
424
|
min_role: z.enum(["worker", "al", "pl", "ceo"]).default("worker").describe("Minimum role to see this entry"),
|
|
299
|
-
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. " +
|
|
300
426
|
"Only use when you intentionally want a separate entry, not a child of an existing one."),
|
|
301
427
|
}, async ({ prefix, content, links, favorite, tags, pinned, store: storeName, min_role: minRole, force }) => {
|
|
302
428
|
const templateName = AGENT_ID.replace(/_\d+$/, "");
|
|
@@ -313,7 +439,7 @@ server.tool("write_memory", "Write a new memory entry to your hierarchical long-
|
|
|
313
439
|
if (prefix.toUpperCase() === "P") {
|
|
314
440
|
const VALID_L2_CATEGORIES = [
|
|
315
441
|
"overview", "codebase", "usage", "context", "deployment",
|
|
316
|
-
"
|
|
442
|
+
"bugs", "protocol", "open tasks", "ideas",
|
|
317
443
|
];
|
|
318
444
|
const lines = content.split("\n");
|
|
319
445
|
const l2Lines = lines.filter(l => /^\t[^\t]/.test(l)).map(l => l.replace(/^\t/, "").toLowerCase().trim());
|
|
@@ -393,9 +519,11 @@ server.tool("write_memory", "Write a new memory entry to your hierarchical long-
|
|
|
393
519
|
});
|
|
394
520
|
server.tool("update_memory", "Update the text of an existing memory entry or sub-node (your own personal memory). " +
|
|
395
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" +
|
|
396
523
|
"Use cases:\n" +
|
|
397
524
|
"- Correct outdated wording: update_memory(id='L0003', content='corrected summary')\n" +
|
|
398
|
-
"-
|
|
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" +
|
|
399
527
|
"- Mark as obsolete: FIRST write the correction, THEN update with [✓ID] reference:\n" +
|
|
400
528
|
" 1. write_memory(prefix='E', content='Correct fix is...') → E0076\n" +
|
|
401
529
|
" 2. update_memory(id='E0042', content='Wrong — see [✓E0076]', obsolete=true)\n" +
|
|
@@ -407,17 +535,17 @@ server.tool("update_memory", "Update the text of an existing memory entry or sub
|
|
|
407
535
|
id: z.string().describe("ID of the entry or node to update, e.g. 'L0003' or 'L0003.2'"),
|
|
408
536
|
content: z.string().min(1).describe("New text content for this node (plain text, no indentation)"),
|
|
409
537
|
links: z.array(z.string()).optional().describe("Optional: update linked entry IDs (root entries only). Replaces existing links."),
|
|
410
|
-
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). " +
|
|
411
539
|
"Requires [✓ID] correction reference in content (e.g. 'Wrong — see [✓E0076]')."),
|
|
412
|
-
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. " +
|
|
413
541
|
"Root favorites are always shown with L2 detail in bulk reads."),
|
|
414
|
-
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. " +
|
|
415
543
|
"No correction entry needed (unlike obsolete). Irrelevant entries/nodes are hidden from output."),
|
|
416
544
|
tags: z.array(z.string()).optional().describe("Set tags on this entry/node. Replaces all existing tags. " +
|
|
417
545
|
"Pass empty array [] to remove all tags. E.g. ['#hmem', '#curation']."),
|
|
418
|
-
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). " +
|
|
419
547
|
"Pinned entries show full L2 content in bulk reads (super-favorite)."),
|
|
420
|
-
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). " +
|
|
421
549
|
"When any entry in a prefix has active=true, only active entries of that prefix are shown with children in bulk reads. " +
|
|
422
550
|
"Non-active entries in the same prefix are shown as title-only (no children)."),
|
|
423
551
|
store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' or 'company'"),
|
|
@@ -498,10 +626,10 @@ server.tool("update_many", "Batch-update multiple memory entries at once. Applie
|
|
|
498
626
|
"Use this instead of calling update_memory multiple times during curation.\n\n" +
|
|
499
627
|
"Example: update_many(ids=['T0005', 'T0012', 'L0044'], irrelevant=true)", {
|
|
500
628
|
ids: z.array(z.string()).min(1).describe("List of entry/node IDs to update, e.g. ['T0005', 'T0012', 'L0044']"),
|
|
501
|
-
irrelevant: z.boolean().optional().describe("Mark all as irrelevant [-]"),
|
|
502
|
-
favorite: z.boolean().optional().describe("Set or clear [♥] favorite on all"),
|
|
503
|
-
active: z.boolean().optional().describe("Set or clear [*] active on all"),
|
|
504
|
-
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"),
|
|
505
633
|
store: z.enum(["personal", "company"]).default("personal"),
|
|
506
634
|
}, async ({ ids, irrelevant, favorite, active, pinned, store: storeName }) => {
|
|
507
635
|
const templateName = AGENT_ID.replace(/_\d+$/, "");
|
|
@@ -572,14 +700,14 @@ server.tool("flush_context", "Store a conversation chunk as linear context histo
|
|
|
572
700
|
const levels = [l1, l2, l3, l4, l5].filter(Boolean).length;
|
|
573
701
|
log(`flush_context: ${result.id} (${levels} levels, ${tags.join(" ")})`);
|
|
574
702
|
syncPush(hmemPath);
|
|
575
|
-
return {
|
|
703
|
+
return trackTokens({
|
|
576
704
|
content: [{
|
|
577
705
|
type: "text",
|
|
578
706
|
text: `Context saved: ${result.id} (${levels} levels)\n` +
|
|
579
707
|
`Title: ${l1}\nTags: ${tags.join(" ")}` +
|
|
580
708
|
(links?.length ? `\nLinks: ${links.join(", ")}` : ""),
|
|
581
709
|
}],
|
|
582
|
-
};
|
|
710
|
+
});
|
|
583
711
|
}
|
|
584
712
|
finally {
|
|
585
713
|
hmemStore.close();
|
|
@@ -597,10 +725,11 @@ server.tool("append_memory", "Append new child nodes to an existing memory entry
|
|
|
597
725
|
"Use this to extend an existing entry with additional detail without overwriting it.\n\n" +
|
|
598
726
|
"Content uses tab indentation relative to the parent:\n" +
|
|
599
727
|
" 0 tabs = direct child of id\n" +
|
|
600
|
-
" 1 tab = grandchild, etc.\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" +
|
|
601
730
|
"Examples:\n" +
|
|
602
|
-
" append_memory(id='L0003', content='New finding\\n\\tSub-detail') " +
|
|
603
|
-
"→ 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" +
|
|
604
733
|
" append_memory(id='L0003.2', content='Extra note') " +
|
|
605
734
|
"→ adds L3 node under the L2 node L0003.2", {
|
|
606
735
|
id: z.string().describe("Root entry ID or parent node ID to append children to, e.g. 'L0003' or 'L0003.2'"),
|
|
@@ -687,11 +816,11 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
687
816
|
time: z.string().optional().describe("Time filter 'HH:MM' — filter entries by time of day"),
|
|
688
817
|
period: z.string().optional().describe("Time window: '+4h' (after), '-2h' (before), '4h' (±4h symmetric), 'both' (±2h default)"),
|
|
689
818
|
time_around: z.string().optional().describe("Reference entry ID — find entries created around the same time"),
|
|
690
|
-
show_obsolete: z.boolean().optional().describe("Include all obsolete entries (default: only top 3 most-accessed)"),
|
|
691
|
-
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."),
|
|
692
|
-
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. " +
|
|
693
822
|
"Like a table of contents. Combine with prefix to filter by category."),
|
|
694
|
-
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). " +
|
|
695
824
|
"Use to deep-dive into a project after a long break. " +
|
|
696
825
|
"depth controls how deep (default: 5 = full tree). " +
|
|
697
826
|
"Example: read_memory({ id: 'P0001', expand: true, depth: 3 })"),
|
|
@@ -700,8 +829,8 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
700
829
|
"use after context compression to recover key knowledge. " +
|
|
701
830
|
"Auto-selected if omitted: first bulk read → discover, subsequent → essentials."),
|
|
702
831
|
store: z.enum(["personal", "company"]).default("personal").describe("Source store: 'personal' or 'company'"),
|
|
703
|
-
curator: z.boolean().optional().describe("Set true to show full metadata (access counts, roles, dates). For curators only."),
|
|
704
|
-
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. " +
|
|
705
834
|
"Bypasses V2 selection and session cache. Use with prefix filter for manageable output."),
|
|
706
835
|
tag: z.string().optional().describe("Filter by hashtag, e.g. '#hmem'. Only entries with this tag are shown in bulk reads. " +
|
|
707
836
|
"Also works with search to find tagged entries."),
|
|
@@ -795,13 +924,14 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
795
924
|
const outputTokens = Math.round(output.length / 4);
|
|
796
925
|
const finalOutput = output.replace(/^(## Context for .+\n)(Source:.+)\n/, `$1$2 | ~${fmtTok(outputTokens)} tokens\n`);
|
|
797
926
|
log(`read_memory [${storeLabel}]: context_for=${context_for}, ${totalRelated} related (${linked.length} linked, ${dedupedTagRelated.length} tag-related), ~${fmtTok(outputTokens)} tokens`);
|
|
798
|
-
return {
|
|
927
|
+
return trackTokens({
|
|
799
928
|
content: [{ type: "text", text: corruptionWarning + finalOutput }],
|
|
800
|
-
};
|
|
929
|
+
});
|
|
801
930
|
}
|
|
802
931
|
const effectiveDepth = depth || (id ? 2 : 1);
|
|
803
932
|
// Session cache: cached entries shown as titles in subsequent bulk reads
|
|
804
|
-
|
|
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;
|
|
805
935
|
const useCache = isBulkListing && storeName === "personal" && !show_all;
|
|
806
936
|
const cachedIds = useCache ? sessionCache.getCachedIds() : undefined;
|
|
807
937
|
const hiddenIds = useCache ? sessionCache.getHiddenIds() : undefined;
|
|
@@ -824,6 +954,7 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
824
954
|
mode: isBulkListing ? effectiveMode : undefined,
|
|
825
955
|
tag,
|
|
826
956
|
staleDays: stale_days,
|
|
957
|
+
directResults: !isBulkListing && !id && !search && !time_around,
|
|
827
958
|
});
|
|
828
959
|
if (entries.length === 0) {
|
|
829
960
|
const hmemPath = storeName === "company"
|
|
@@ -852,7 +983,7 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
852
983
|
// Update session cache after bulk read
|
|
853
984
|
if (useCache) {
|
|
854
985
|
const allIds = entries.filter(e => !e.obsolete).map(e => e.id);
|
|
855
|
-
const promotedIds = new Set(entries.filter(e => e.promoted === "favorite" || e.promoted === "access" || e.promoted === "subnode").map(e => e.id));
|
|
986
|
+
const promotedIds = new Set(entries.filter(e => e.promoted === "favorite" || e.promoted === "access" || e.promoted === "subnode" || e.promoted === "task").map(e => e.id));
|
|
856
987
|
sessionCache.registerDelivered(allIds, promotedIds);
|
|
857
988
|
}
|
|
858
989
|
// Format output
|
|
@@ -932,7 +1063,16 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
932
1063
|
const projectList = projects.length > 0
|
|
933
1064
|
? projects.map(e => ` ${e.id} ${e.title}`).join("\n")
|
|
934
1065
|
: " (no projects yet — create one with write_memory(prefix=\"P\", content=\"Name | Status | Stack | Description\", tags=[...]))";
|
|
935
|
-
|
|
1066
|
+
// Inject recent O-entries even without active project (global, no project filter)
|
|
1067
|
+
let recentOHint = "";
|
|
1068
|
+
if (hmemConfig.recentOEntries > 0) {
|
|
1069
|
+
const { text, ids } = formatRecentOEntries(hmemStore, hmemConfig.recentOEntries, 10);
|
|
1070
|
+
if (text) {
|
|
1071
|
+
recentOHint = `\n${text}\n`;
|
|
1072
|
+
sessionCache.registerDelivered(ids);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return trackTokens({
|
|
936
1076
|
content: [{
|
|
937
1077
|
type: "text",
|
|
938
1078
|
text: `⚠ ACTION REQUIRED: No project is active.\n\n` +
|
|
@@ -942,9 +1082,21 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
942
1082
|
` write_memory(prefix="P", content="Name | Status | Stack | Description", tags=["#project"])\n\n` +
|
|
943
1083
|
`Available projects:\n${projectList}\n\n` +
|
|
944
1084
|
`Session logs (O-entries) will be linked to the active project.\n` +
|
|
945
|
-
`Memory data is withheld until a project is activated
|
|
1085
|
+
`Memory data is withheld until a project is activated.` + recentOHint,
|
|
946
1086
|
}],
|
|
947
|
-
};
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// Inject recent O-entries (session logs) on bulk reads when none are cached
|
|
1091
|
+
let recentOSection = "";
|
|
1092
|
+
if (isBulkListing && storeName === "personal" && hmemConfig.recentOEntries > 0) {
|
|
1093
|
+
const cachedOIds = [...(cachedIds || []), ...(hiddenIds || [])].filter(id => id.startsWith("O"));
|
|
1094
|
+
if (cachedOIds.length === 0) {
|
|
1095
|
+
const { text, ids } = formatRecentOEntries(hmemStore, hmemConfig.recentOEntries, 10);
|
|
1096
|
+
if (text) {
|
|
1097
|
+
recentOSection = `\n${text}\n`;
|
|
1098
|
+
sessionCache.registerDelivered(ids);
|
|
1099
|
+
}
|
|
948
1100
|
}
|
|
949
1101
|
}
|
|
950
1102
|
// Check for P-entries that need migration to standard schema
|
|
@@ -962,12 +1114,12 @@ server.tool("read_memory", "Read from your hierarchical long-term memory (.hmem)
|
|
|
962
1114
|
const header = `## Memory: ${storeLabel} (${stats.total} total entries)\n` +
|
|
963
1115
|
`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}${staleHint}\n`;
|
|
964
1116
|
log(`read_memory [${storeLabel}]: ${visibleCount} results (depth=${effectiveDepth}, role=${agentRole}${cacheInfo})`);
|
|
965
|
-
return {
|
|
1117
|
+
return trackTokens({
|
|
966
1118
|
content: [{
|
|
967
1119
|
type: "text",
|
|
968
|
-
text: corruptionWarning + projectWarning + migrationHint + newSinceSection + header + "\n" + output + (isBulkListing && (sessionCache.readCount <= 1 || sessionCache.size === 0) ? REMINDER_HINT : ""),
|
|
1120
|
+
text: corruptionWarning + projectWarning + migrationHint + newSinceSection + header + "\n" + output + recentOSection + (isBulkListing && (sessionCache.readCount <= 1 || sessionCache.size === 0) ? REMINDER_HINT : ""),
|
|
969
1121
|
}],
|
|
970
|
-
};
|
|
1122
|
+
});
|
|
971
1123
|
}
|
|
972
1124
|
finally {
|
|
973
1125
|
hmemStore.close();
|
|
@@ -1040,7 +1192,7 @@ server.tool("import_memory", "Import entries from a .hmem file into your memory.
|
|
|
1040
1192
|
"Deduplicates by L1 content (merges sub-nodes), remaps IDs on conflict.", {
|
|
1041
1193
|
source_path: z.string().describe("Path to .hmem file to import"),
|
|
1042
1194
|
store: z.enum(["personal", "company"]).default("personal").describe("Target store: 'personal' (your own memory) or 'company' (shared company store)"),
|
|
1043
|
-
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"),
|
|
1044
1196
|
}, async ({ source_path, store: storeName, dry_run }) => {
|
|
1045
1197
|
try {
|
|
1046
1198
|
const hmemStore = storeName === "company"
|
|
@@ -1219,19 +1371,135 @@ server.tool("load_project", "Load a project and activate it. Returns L2 content
|
|
|
1219
1371
|
isError: true,
|
|
1220
1372
|
};
|
|
1221
1373
|
}
|
|
1222
|
-
//
|
|
1223
|
-
const
|
|
1374
|
+
// Custom compact rendering for project briefing: L2 content + L3 titles, no dates, compact IDs
|
|
1375
|
+
const e = entries[0];
|
|
1376
|
+
const syncThreshold = getSyncThreshold();
|
|
1377
|
+
const syncTag = syncThreshold && e.updated_at && e.updated_at <= syncThreshold ? " ✓" : "";
|
|
1378
|
+
const lines = [];
|
|
1379
|
+
lines.push(`${e.id}${syncTag} ${e.title}`);
|
|
1380
|
+
if (e.level_1 && e.level_1 !== e.title)
|
|
1381
|
+
lines.push(` ${e.level_1}`);
|
|
1382
|
+
if (e.children) {
|
|
1383
|
+
const { withBody, withChildren } = hmemConfig.loadProjectExpand;
|
|
1384
|
+
for (const child of e.children.filter(c => !c.irrelevant)) {
|
|
1385
|
+
const cId = child.id.replace(e.id, "");
|
|
1386
|
+
const expandBody = withBody.includes(child.seq);
|
|
1387
|
+
const expandChildTitles = withChildren.includes(child.seq);
|
|
1388
|
+
lines.push(` ${cId} ${child.title || child.content.substring(0, 60)}`);
|
|
1389
|
+
if (child.children && child.children.length > 0) {
|
|
1390
|
+
for (const gc of child.children.filter((g) => !g.irrelevant)) {
|
|
1391
|
+
const gcId = gc.id.replace(e.id, "");
|
|
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}`);
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
// Default: compact titles only
|
|
1409
|
+
const gcTitle = gc.title || (gc.content.length > 80 ? gc.content.substring(0, 80) : gc.content);
|
|
1410
|
+
lines.push(` ${gcId} ${gcTitle}`);
|
|
1411
|
+
}
|
|
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}]`);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
else if (child.child_count && child.child_count > 0) {
|
|
1427
|
+
lines.push(` [+${child.child_count}]`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
// Links
|
|
1432
|
+
if (e.linkedEntries && e.linkedEntries.length > 0) {
|
|
1433
|
+
lines.push(" Links:");
|
|
1434
|
+
for (const le of e.linkedEntries) {
|
|
1435
|
+
lines.push(` ${le.id} ${le.title}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
// Context injection: find related E/L entries by weighted tag scoring
|
|
1439
|
+
try {
|
|
1440
|
+
const ctx = hmemStore.findContext(id, 4, 10);
|
|
1441
|
+
const relatedEL = ctx.tagRelated.filter(r => (r.entry.prefix === "E" || r.entry.prefix === "L") && !r.entry.obsolete && !r.entry.irrelevant);
|
|
1442
|
+
if (relatedEL.length > 0) {
|
|
1443
|
+
lines.push(" Related errors & lessons:");
|
|
1444
|
+
for (const r of relatedEL) {
|
|
1445
|
+
lines.push(` ${r.entry.id} [⚡] ${r.entry.title}`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
catch { /* findContext may fail on empty/new entries */ }
|
|
1450
|
+
// Inject R-entries (rules) — always shown at project load
|
|
1451
|
+
const ruleEntries = hmemStore.read({
|
|
1452
|
+
prefix: "R",
|
|
1453
|
+
depth: 1,
|
|
1454
|
+
agentRole: (ROLE || "worker"),
|
|
1455
|
+
}).filter(r => !r.obsolete && !r.irrelevant);
|
|
1456
|
+
if (ruleEntries.length > 0) {
|
|
1457
|
+
lines.push(" Rules:");
|
|
1458
|
+
for (const r of ruleEntries) {
|
|
1459
|
+
lines.push(` ${r.id} ${r.title}`);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
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
|
|
1464
|
+
if (hmemConfig.recentOEntries > 0) {
|
|
1465
|
+
const { text, ids } = formatRecentOEntries(hmemStore, 1, hmemConfig.recentOEntries, id, true);
|
|
1466
|
+
if (text) {
|
|
1467
|
+
lines.push(" " + text.replace(/\n/g, "\n "));
|
|
1468
|
+
sessionCache.registerDelivered(ids);
|
|
1469
|
+
}
|
|
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.`;
|
|
1487
|
+
const output = lines.join("\n");
|
|
1488
|
+
const outputTokens = Math.round(output.length / 4);
|
|
1489
|
+
const totalStats = hmemStore.stats();
|
|
1490
|
+
const totalTokens = Math.round(totalStats.totalChars / 4);
|
|
1491
|
+
const tokenInfo = ` | ${(outputTokens / 1000).toFixed(1)}k/${(totalTokens / 1000).toFixed(0)}k tokens`;
|
|
1224
1492
|
log(`load_project: ${id} activated and loaded (depth=3)`);
|
|
1225
1493
|
// Sync if enabled
|
|
1226
1494
|
const hmemPath = resolveHmemPath(PROJECT_DIR, templateName);
|
|
1227
1495
|
if (storeName === "personal")
|
|
1228
1496
|
syncPush(hmemPath);
|
|
1229
|
-
return {
|
|
1497
|
+
return trackTokens({
|
|
1230
1498
|
content: [{
|
|
1231
1499
|
type: "text",
|
|
1232
|
-
text: `✓ Project ${id} activated
|
|
1500
|
+
text: `✓ Project ${id} activated.${tokenInfo}\n${irrelevantTip}\n\n${output}\n\n${irrelevantTip}`,
|
|
1233
1501
|
}],
|
|
1234
|
-
};
|
|
1502
|
+
});
|
|
1235
1503
|
}
|
|
1236
1504
|
finally {
|
|
1237
1505
|
hmemStore.close();
|
|
@@ -1434,7 +1702,7 @@ server.tool("get_audit_queue", "CURATOR ONLY (ceo role). Returns agents whose .h
|
|
|
1434
1702
|
"Each agent should be audited in a separate spawn to keep context bounded.", {}, async () => {
|
|
1435
1703
|
if (!isCurator()) {
|
|
1436
1704
|
return {
|
|
1437
|
-
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." }],
|
|
1438
1706
|
isError: true,
|
|
1439
1707
|
};
|
|
1440
1708
|
}
|
|
@@ -1520,8 +1788,11 @@ server.tool("read_agent_memory", "CURATOR ONLY (ceo role). Read the full memory
|
|
|
1520
1788
|
const favTag = e.favorite ? " [♥]" : "";
|
|
1521
1789
|
lines.push(`[${e.id}] ${date}${role}${favTag}${obsoleteTag}${irrelevantTag}${access}`);
|
|
1522
1790
|
lines.push(` ${e.title}`);
|
|
1523
|
-
if (e.level_1 !== e.title)
|
|
1524
|
-
|
|
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
|
+
}
|
|
1525
1796
|
if (e.children && e.children.length > 0) {
|
|
1526
1797
|
for (const child of e.children) {
|
|
1527
1798
|
const indent = " ".repeat(child.depth - 1);
|
|
@@ -1555,10 +1826,10 @@ server.tool("fix_agent_memory", "CURATOR ONLY (ceo role). Correct a specific ent
|
|
|
1555
1826
|
content: z.string().optional().describe("New text content. For root entries: replaces the L1 summary. " +
|
|
1556
1827
|
"For node IDs: replaces that node's content."),
|
|
1557
1828
|
min_role: z.enum(["worker", "al", "pl", "ceo"]).optional().describe("Update access clearance (root entries only)."),
|
|
1558
|
-
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). " +
|
|
1559
1830
|
"Obsolete entries stay in memory but are shown with [⚠ OBSOLETE]."),
|
|
1560
|
-
favorite: z.boolean().optional().describe("Set or clear the [♥] favorite flag (root entries only)."),
|
|
1561
|
-
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."),
|
|
1562
1833
|
}, async ({ agent_name, entry_id, content, min_role, obsolete, favorite, irrelevant }) => {
|
|
1563
1834
|
if (!isCurator()) {
|
|
1564
1835
|
return {
|
|
@@ -1779,26 +2050,28 @@ function formatTitlesOnly(entries, config, curator = false) {
|
|
|
1779
2050
|
const desc = config.prefixDescriptions[prefix] ?? config.prefixes[prefix] ?? prefix;
|
|
1780
2051
|
lines.push(`## ${desc} (${prefixEntries.length} total)\n`);
|
|
1781
2052
|
for (const e of prefixEntries) {
|
|
1782
|
-
const mmdd = e.created_at.substring(5, 10);
|
|
1783
2053
|
const fav = e.favorite ? " [♥]" : "";
|
|
1784
2054
|
const act = e.active ? " [*]" : "";
|
|
1785
2055
|
const obs = e.obsolete ? " [!]" : "";
|
|
1786
2056
|
const irr = e.irrelevant ? " [-]" : "";
|
|
2057
|
+
const syncThreshold = getSyncThreshold();
|
|
2058
|
+
const sync = syncThreshold && e.updated_at && e.updated_at <= syncThreshold ? " ✓" : "";
|
|
1787
2059
|
if (e.expanded && e.children && e.children.length > 0) {
|
|
1788
2060
|
const visibleChildren = e.children.filter(c => !c.irrelevant);
|
|
1789
2061
|
const hiddenIrr = e.children.length - visibleChildren.length;
|
|
1790
|
-
|
|
1791
|
-
lines.push(`${e.id}
|
|
2062
|
+
const rootId = e.id;
|
|
2063
|
+
lines.push(`${e.id}${fav}${act}${obs}${sync} ${e.title}${formatTagSuffix(e.tags, curator)}`);
|
|
1792
2064
|
for (const child of visibleChildren) {
|
|
1793
2065
|
const short = child.title || (child.content.length > CHILD_TITLE_LEN
|
|
1794
2066
|
? child.content.substring(0, CHILD_TITLE_LEN)
|
|
1795
2067
|
: child.content);
|
|
1796
2068
|
const grandchildren = (child.child_count ?? 0) > 0 ? ` (${child.child_count})` : "";
|
|
1797
2069
|
const cfav = child.favorite ? " [♥]" : "";
|
|
1798
|
-
|
|
2070
|
+
const compactChildId = child.id.replace(rootId, "");
|
|
2071
|
+
lines.push(` ${compactChildId}${cfav} ${short}${grandchildren}`);
|
|
1799
2072
|
}
|
|
1800
2073
|
if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
|
|
1801
|
-
lines.push(` [+${e.hiddenChildrenCount} more
|
|
2074
|
+
lines.push(` [+${e.hiddenChildrenCount} more]`);
|
|
1802
2075
|
}
|
|
1803
2076
|
if (hiddenIrr > 0) {
|
|
1804
2077
|
lines.push(` (+${hiddenIrr} irrelevant hidden)`);
|
|
@@ -1807,7 +2080,7 @@ function formatTitlesOnly(entries, config, curator = false) {
|
|
|
1807
2080
|
else {
|
|
1808
2081
|
// Non-expanded: compact line with child count
|
|
1809
2082
|
const childHint = (e.hiddenChildrenCount ?? 0) > 0 ? ` (${e.hiddenChildrenCount})` : "";
|
|
1810
|
-
lines.push(`${e.id}
|
|
2083
|
+
lines.push(`${e.id}${fav}${act}${obs}${sync} ${e.title}${formatTagSuffix(e.tags, curator)}${childHint}`);
|
|
1811
2084
|
}
|
|
1812
2085
|
}
|
|
1813
2086
|
lines.push("");
|
|
@@ -1873,6 +2146,17 @@ function nodeMarkers(node) {
|
|
|
1873
2146
|
const irr = node.irrelevant ? " [-]" : "";
|
|
1874
2147
|
return `${fav}${irr}`;
|
|
1875
2148
|
}
|
|
2149
|
+
/** Get the minimum lastPushAt across all sync servers — entries updated before this are fully synced. */
|
|
2150
|
+
function getSyncThreshold() {
|
|
2151
|
+
const servers = getSyncServers(hmemConfig);
|
|
2152
|
+
if (servers.length === 0)
|
|
2153
|
+
return null;
|
|
2154
|
+
const pushTimes = servers.map(s => s.lastPushAt).filter((t) => !!t);
|
|
2155
|
+
if (pushTimes.length === 0)
|
|
2156
|
+
return null;
|
|
2157
|
+
// Min = earliest push → everything before this is on ALL servers
|
|
2158
|
+
return pushTimes.reduce((a, b) => a < b ? a : b);
|
|
2159
|
+
}
|
|
1876
2160
|
function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
1877
2161
|
// O-prefix: title-only rendering — never expand children (raw conversation data, too large)
|
|
1878
2162
|
// Use read_memory(id="O0042") to drill in explicitly.
|
|
@@ -1894,14 +2178,16 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1894
2178
|
else {
|
|
1895
2179
|
lines.push(`${e.id} ${e.title}${tagStr}`);
|
|
1896
2180
|
}
|
|
1897
|
-
// Node drilldown: show
|
|
1898
|
-
if (
|
|
1899
|
-
|
|
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
|
+
}
|
|
1900
2186
|
}
|
|
1901
2187
|
}
|
|
1902
2188
|
else {
|
|
1903
2189
|
if (curator) {
|
|
1904
|
-
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : e.promoted === "subnode" ? " [≡]" : "";
|
|
2190
|
+
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : e.promoted === "subnode" ? " [≡]" : e.promoted === "task" ? " [⚡]" : "";
|
|
1905
2191
|
const activeTag = e.active ? " [*]" : "";
|
|
1906
2192
|
const pinnedTag = e.pinned ? " [P]" : "";
|
|
1907
2193
|
const obsoleteTag = e.obsolete ? " [⚠ OBSOLETE]" : "";
|
|
@@ -1913,31 +2199,36 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1913
2199
|
lines.push(` ${e.title}${tagStr}`);
|
|
1914
2200
|
}
|
|
1915
2201
|
else {
|
|
1916
|
-
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : e.promoted === "subnode" ? " [≡]" : "";
|
|
2202
|
+
const promotedTag = e.promoted === "favorite" ? " [♥]" : e.promoted === "access" ? " [★]" : e.promoted === "subnode" ? " [≡]" : e.promoted === "task" ? " [⚡]" : "";
|
|
1917
2203
|
const activeTag = e.active ? " [*]" : "";
|
|
1918
2204
|
const pinnedTag = e.pinned ? " [P]" : "";
|
|
1919
2205
|
const obsoleteTag = e.obsolete ? " [!]" : "";
|
|
1920
2206
|
const irrelevantTag = e.irrelevant ? " [-]" : "";
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2207
|
+
const syncThreshold = getSyncThreshold();
|
|
2208
|
+
const syncTag = syncThreshold && e.updated_at && e.updated_at <= syncThreshold ? " ✓" : "";
|
|
2209
|
+
lines.push(`${e.id}${promotedTag}${activeTag}${pinnedTag}${obsoleteTag}${irrelevantTag}${syncTag} ${e.title}${tagStr}`);
|
|
2210
|
+
}
|
|
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
|
+
}
|
|
1927
2216
|
}
|
|
1928
2217
|
}
|
|
1929
2218
|
// Children — filter out irrelevant nodes
|
|
2219
|
+
// Root ID for compact child rendering (e.g. P0048.1 → .1)
|
|
2220
|
+
const rootId = e.id.includes(".") ? e.id.split(".")[0] : e.id;
|
|
1930
2221
|
if (e.children && e.children.length > 0) {
|
|
1931
2222
|
const visibleChildren = e.children.filter(c => !c.irrelevant);
|
|
1932
2223
|
const hiddenIrrelevant = e.children.length - visibleChildren.length;
|
|
1933
2224
|
if (expand || e.pinned) {
|
|
1934
2225
|
// Expand mode or pinned: full L2 content + recursive children
|
|
1935
|
-
renderChildrenExpanded(lines, visibleChildren, curator);
|
|
2226
|
+
renderChildrenExpanded(lines, visibleChildren, curator, rootId);
|
|
1936
2227
|
}
|
|
1937
2228
|
else if (e.expanded && !expand) {
|
|
1938
|
-
renderChildrenFormatted(lines, visibleChildren, curator);
|
|
2229
|
+
renderChildrenFormatted(lines, visibleChildren, curator, rootId);
|
|
1939
2230
|
if (e.hiddenChildrenCount && e.hiddenChildrenCount > 0) {
|
|
1940
|
-
lines.push(` [+${e.hiddenChildrenCount} more
|
|
2231
|
+
lines.push(` [+${e.hiddenChildrenCount} more]`);
|
|
1941
2232
|
}
|
|
1942
2233
|
}
|
|
1943
2234
|
else if (e.hiddenChildrenCount !== undefined) {
|
|
@@ -1945,23 +2236,24 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
1945
2236
|
const child = visibleChildren[0];
|
|
1946
2237
|
if (child) {
|
|
1947
2238
|
const fav = nodeMarkers(child);
|
|
2239
|
+
const compactChildId = child.id.replace(rootId, "");
|
|
1948
2240
|
const hint = (child.child_count ?? 0) > 0
|
|
1949
|
-
? ` [+${child.child_count}
|
|
2241
|
+
? ` [+${child.child_count}]`
|
|
1950
2242
|
: "";
|
|
1951
2243
|
if (curator) {
|
|
1952
2244
|
lines.push(` [${child.id}]${fav} ${child.title}${hint}`);
|
|
1953
2245
|
}
|
|
1954
2246
|
else {
|
|
1955
|
-
lines.push(` ${
|
|
2247
|
+
lines.push(` ${compactChildId}${fav} ${child.title}${hint}`);
|
|
1956
2248
|
}
|
|
1957
2249
|
}
|
|
1958
2250
|
if (e.hiddenChildrenCount > 0) {
|
|
1959
|
-
lines.push(` [+${e.hiddenChildrenCount} more
|
|
2251
|
+
lines.push(` [+${e.hiddenChildrenCount} more]`);
|
|
1960
2252
|
}
|
|
1961
2253
|
}
|
|
1962
2254
|
else {
|
|
1963
2255
|
// ID-based read: show all direct children as titles
|
|
1964
|
-
renderChildrenFormatted(lines, visibleChildren, curator);
|
|
2256
|
+
renderChildrenFormatted(lines, visibleChildren, curator, rootId);
|
|
1965
2257
|
}
|
|
1966
2258
|
if (hiddenIrrelevant > 0) {
|
|
1967
2259
|
lines.push(` (+${hiddenIrrelevant} irrelevant hidden)`);
|
|
@@ -2017,21 +2309,21 @@ function renderEntryFormatted(lines, e, curator, expand = false) {
|
|
|
2017
2309
|
* Render a list of child nodes — shows titles for navigation.
|
|
2018
2310
|
* Use read_memory(id=child.id) to see full content.
|
|
2019
2311
|
*/
|
|
2020
|
-
function renderChildrenFormatted(lines, children, curator) {
|
|
2312
|
+
function renderChildrenFormatted(lines, children, curator, rootId) {
|
|
2021
2313
|
for (const child of children) {
|
|
2022
2314
|
const indent = " ".repeat(child.depth - 1);
|
|
2023
2315
|
const fav = nodeMarkers(child);
|
|
2024
2316
|
const ctags = formatTagSuffix(child.tags, curator);
|
|
2317
|
+
const compactId = rootId ? child.id.replace(rootId, "") : child.id;
|
|
2025
2318
|
const hint = (child.child_count ?? 0) > 0
|
|
2026
|
-
? ` [+${child.child_count}
|
|
2319
|
+
? ` [+${child.child_count}]`
|
|
2027
2320
|
: "";
|
|
2028
2321
|
if (curator) {
|
|
2029
2322
|
lines.push(`${indent}[${child.id}]${fav} ${child.title}${ctags}${hint}`);
|
|
2030
2323
|
}
|
|
2031
2324
|
else {
|
|
2032
|
-
lines.push(`${indent}${
|
|
2325
|
+
lines.push(`${indent}${compactId}${fav} ${child.title}${ctags}${hint}`);
|
|
2033
2326
|
}
|
|
2034
|
-
// Don't recurse into grandchildren — titles only, drill for content
|
|
2035
2327
|
}
|
|
2036
2328
|
}
|
|
2037
2329
|
/**
|
|
@@ -2040,40 +2332,53 @@ function renderChildrenFormatted(lines, children, curator) {
|
|
|
2040
2332
|
* At the depth boundary (children loaded but THEIR children are not),
|
|
2041
2333
|
* renders as titles instead of full content.
|
|
2042
2334
|
*/
|
|
2043
|
-
function renderChildrenExpanded(lines, children, curator) {
|
|
2335
|
+
function renderChildrenExpanded(lines, children, curator, rootId) {
|
|
2044
2336
|
for (const child of children) {
|
|
2045
2337
|
const indent = " ".repeat(child.depth - 1);
|
|
2338
|
+
const bodyIndent = indent + " ";
|
|
2046
2339
|
const fav = nodeMarkers(child);
|
|
2340
|
+
const compactId = rootId ? child.id.replace(rootId, "") : child.id;
|
|
2047
2341
|
const visibleGrandchildren = child.children?.filter(c => !c.irrelevant);
|
|
2048
2342
|
const hasLoadedChildren = visibleGrandchildren && visibleGrandchildren.length > 0;
|
|
2049
2343
|
const isBoundary = !hasLoadedChildren && (child.child_count ?? 0) > 0;
|
|
2344
|
+
const hasBody = child.content && child.content !== child.title;
|
|
2050
2345
|
if (hasLoadedChildren) {
|
|
2051
|
-
// Inner node:
|
|
2346
|
+
// Inner node: title + body + recurse
|
|
2052
2347
|
if (curator) {
|
|
2053
|
-
lines.push(`${indent}[${child.id}]${fav} ${child.
|
|
2348
|
+
lines.push(`${indent}[${child.id}]${fav} ${child.title}`);
|
|
2054
2349
|
}
|
|
2055
2350
|
else {
|
|
2056
|
-
lines.push(`${indent}${
|
|
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
|
+
}
|
|
2057
2357
|
}
|
|
2058
|
-
renderChildrenExpanded(lines, visibleGrandchildren, curator);
|
|
2358
|
+
renderChildrenExpanded(lines, visibleGrandchildren, curator, rootId);
|
|
2059
2359
|
}
|
|
2060
2360
|
else if (isBoundary) {
|
|
2061
2361
|
// Boundary: title only + child count hint
|
|
2062
|
-
const hint = ` [+${child.child_count}
|
|
2362
|
+
const hint = ` [+${child.child_count}]`;
|
|
2063
2363
|
if (curator) {
|
|
2064
2364
|
lines.push(`${indent}[${child.id}]${fav} ${child.title}${hint}`);
|
|
2065
2365
|
}
|
|
2066
2366
|
else {
|
|
2067
|
-
lines.push(`${indent}${
|
|
2367
|
+
lines.push(`${indent}${compactId}${fav} ${child.title}${hint}`);
|
|
2068
2368
|
}
|
|
2069
2369
|
}
|
|
2070
2370
|
else {
|
|
2071
|
-
// Leaf node
|
|
2371
|
+
// Leaf node: title + body
|
|
2072
2372
|
if (curator) {
|
|
2073
|
-
lines.push(`${indent}[${child.id}]${fav} ${child.
|
|
2373
|
+
lines.push(`${indent}[${child.id}]${fav} ${child.title}`);
|
|
2074
2374
|
}
|
|
2075
2375
|
else {
|
|
2076
|
-
lines.push(`${indent}${
|
|
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
|
+
}
|
|
2077
2382
|
}
|
|
2078
2383
|
}
|
|
2079
2384
|
}
|
|
@@ -2098,6 +2403,7 @@ function checkForUpdates() {
|
|
|
2098
2403
|
stdio: ["ignore", "pipe", "ignore"],
|
|
2099
2404
|
detached: true,
|
|
2100
2405
|
shell: process.platform === "win32",
|
|
2406
|
+
windowsHide: true,
|
|
2101
2407
|
});
|
|
2102
2408
|
child.unref();
|
|
2103
2409
|
let out = "";
|