hmem-mcp 6.3.1 → 6.3.2
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/cli-context-inject.js +8 -1
- package/dist/cli-context-inject.js.map +1 -1
- package/dist/cli-init.js +96 -1
- package/dist/cli-init.js.map +1 -1
- package/dist/cli-log-exchange.js +9 -4
- package/dist/cli-log-exchange.js.map +1 -1
- package/dist/hmem-config.d.ts +7 -1
- package/dist/hmem-config.js +4 -1
- package/dist/hmem-config.js.map +1 -1
- package/dist/hmem-store.d.ts +10 -31
- package/dist/hmem-store.js +22 -187
- package/dist/hmem-store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +133 -479
- package/dist/mcp-server.js.map +1 -1
- package/opencode-plugin/hmem.js +105 -0
- package/package.json +2 -1
- package/skills/hmem-curate/SKILL.md +158 -134
- package/skills/hmem-migrate-o/SKILL.md +1 -1
- package/skills/hmem-read/SKILL.md +1 -1
- package/skills/hmem-release/SKILL.md +2 -3
- package/skills/hmem-wipe/SKILL.md +6 -4
- package/skills/hmem-write/SKILL.md +28 -5
- package/skills/hmem-self-curate/SKILL.md +0 -194
package/dist/hmem-store.js
CHANGED
|
@@ -31,6 +31,20 @@ import os from "node:os";
|
|
|
31
31
|
import path from "node:path";
|
|
32
32
|
import { DEFAULT_CONFIG, DEFAULT_PREFIX_DESCRIPTIONS } from "./hmem-config.js";
|
|
33
33
|
import { readSessionMarker, writeSessionMarker } from "./session-state.js";
|
|
34
|
+
/**
|
|
35
|
+
* Thrown by write_memory when similar existing entries are detected.
|
|
36
|
+
* Handled specially by callers — surfaced as a non-error hint so the
|
|
37
|
+
* agent can decide whether to append to an existing entry or retry with
|
|
38
|
+
* force=true, without the UI flagging it in red.
|
|
39
|
+
*/
|
|
40
|
+
export class SimilarEntriesError extends Error {
|
|
41
|
+
bestMatch;
|
|
42
|
+
constructor(message, bestMatch) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "SimilarEntriesError";
|
|
45
|
+
this.bestMatch = bestMatch;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
// Prefixes are now loaded from config — see this.cfg.prefixes
|
|
35
49
|
// (limits are now instance-level via this.cfg.maxCharsPerLevel)
|
|
36
50
|
const SCHEMA = `
|
|
@@ -270,7 +284,10 @@ export class HmemStore {
|
|
|
270
284
|
throw new Error("Tags are required. Provide at least 1 tag (3+ recommended) for discoverability. Example: tags=['#hmem', '#sqlite', '#bug']");
|
|
271
285
|
}
|
|
272
286
|
const validatedTags = this.validateTags(tags);
|
|
273
|
-
// Duplicate detection: check for existing entries with significant tag overlap
|
|
287
|
+
// Duplicate detection: check for existing entries with significant tag overlap.
|
|
288
|
+
// Threshold: require at least 3 shared tags for a tag-only match — project/framework
|
|
289
|
+
// tags alone (2 overlapping generic tags like #python+#bug) are not enough to block
|
|
290
|
+
// a new entry. See issue #12.
|
|
274
291
|
if (prefix !== "O" && !force) { // O-entries are auto-generated, skip check
|
|
275
292
|
const tagPlaceholders = validatedTags.map(() => "?").join(", ");
|
|
276
293
|
const overlapRows = this.db.prepare(`
|
|
@@ -290,7 +307,7 @@ export class HmemStore {
|
|
|
290
307
|
AND m.obsolete != 1
|
|
291
308
|
AND m.irrelevant != 1
|
|
292
309
|
GROUP BY root_id
|
|
293
|
-
HAVING shared >=
|
|
310
|
+
HAVING shared >= 3
|
|
294
311
|
ORDER BY shared DESC
|
|
295
312
|
LIMIT 3
|
|
296
313
|
`).all(...validatedTags, prefix);
|
|
@@ -336,9 +353,9 @@ export class HmemStore {
|
|
|
336
353
|
parts.push(`Similar titles:\n${ftsHits}`);
|
|
337
354
|
}
|
|
338
355
|
const bestMatch = overlapRows[0]?.root_id ?? ftsMatches[0]?.root_id;
|
|
339
|
-
throw new
|
|
356
|
+
throw new SimilarEntriesError(`Similar ${prefix}-entries already exist:\n${parts.join("\n")}\n\n` +
|
|
340
357
|
`If this belongs to an existing entry, use: append_memory(id="${bestMatch}", content="...")\n` +
|
|
341
|
-
`If this is intentionally a NEW entry, retry with: force=true
|
|
358
|
+
`If this is intentionally a NEW entry, retry with: force=true`, bestMatch);
|
|
342
359
|
}
|
|
343
360
|
}
|
|
344
361
|
// Run in a transaction
|
|
@@ -3162,20 +3179,7 @@ export class HmemStore {
|
|
|
3162
3179
|
}
|
|
3163
3180
|
catch { /* malformed JSON — skip */ }
|
|
3164
3181
|
}
|
|
3165
|
-
//
|
|
3166
|
-
const nodeRows = this.db.prepare("SELECT id, links FROM memory_nodes WHERE links IS NOT NULL AND links LIKE ?").all(`%"${obsoleteId}"%`);
|
|
3167
|
-
for (const row of nodeRows) {
|
|
3168
|
-
try {
|
|
3169
|
-
const arr = JSON.parse(row.links);
|
|
3170
|
-
if (!arr.includes(obsoleteId))
|
|
3171
|
-
continue;
|
|
3172
|
-
const updated = arr.map(l => l === obsoleteId ? correctionId : l);
|
|
3173
|
-
const deduped = [...new Set(updated)];
|
|
3174
|
-
this.db.prepare("UPDATE memory_nodes SET links = ? WHERE id = ?")
|
|
3175
|
-
.run(JSON.stringify(deduped), row.id);
|
|
3176
|
-
}
|
|
3177
|
-
catch { /* malformed JSON — skip */ }
|
|
3178
|
-
}
|
|
3182
|
+
// memory_nodes has no `links` column — only root entries (memories) carry links.
|
|
3179
3183
|
}
|
|
3180
3184
|
/** Fetch direct children of a node (root or compound), including their grandchild counts. */
|
|
3181
3185
|
/** Bulk-fetch direct child counts for multiple parent IDs in one query. */
|
|
@@ -4112,25 +4116,6 @@ export class HmemStore {
|
|
|
4112
4116
|
return { moved: subtree.length, newId: newSourceId, idMap: Object.fromEntries(idMap) };
|
|
4113
4117
|
}
|
|
4114
4118
|
}
|
|
4115
|
-
// ---- Convenience: resolve .hmem path for an agent ----
|
|
4116
|
-
/**
|
|
4117
|
-
* @deprecated Use resolveHmemPath() (no args) instead. Will be removed in v7.0.
|
|
4118
|
-
*/
|
|
4119
|
-
export function resolveHmemPathLegacy(projectDir, templateName) {
|
|
4120
|
-
console.error("[hmem] DEPRECATED: resolveHmemPathLegacy(projectDir, templateName) — use HMEM_PATH env var instead");
|
|
4121
|
-
// No agent name configured → use memory.hmem directly in project root
|
|
4122
|
-
if (!templateName || templateName === "UNKNOWN") {
|
|
4123
|
-
return path.join(projectDir, "memory.hmem");
|
|
4124
|
-
}
|
|
4125
|
-
// Named agent → Agents/NAME/NAME.hmem (check Assistenten/ as fallback)
|
|
4126
|
-
let agentDir = path.join(projectDir, "Agents", templateName);
|
|
4127
|
-
if (!fs.existsSync(agentDir)) {
|
|
4128
|
-
const alt = path.join(projectDir, "Assistenten", templateName);
|
|
4129
|
-
if (fs.existsSync(alt))
|
|
4130
|
-
agentDir = alt;
|
|
4131
|
-
}
|
|
4132
|
-
return path.join(agentDir, `${templateName}.hmem`);
|
|
4133
|
-
}
|
|
4134
4119
|
/**
|
|
4135
4120
|
* Resolve the path to the personal .hmem database file.
|
|
4136
4121
|
* Priority: HMEM_PATH env var > CWD discovery > ~/.hmem/memory.hmem
|
|
@@ -4186,14 +4171,6 @@ export function resolveHmemPath(cwdOverride) {
|
|
|
4186
4171
|
// Priority 4: default fallback
|
|
4187
4172
|
return path.resolve(safeHomedir(), ".hmem", "memory.hmem");
|
|
4188
4173
|
}
|
|
4189
|
-
/**
|
|
4190
|
-
* @deprecated Use `new HmemStore(resolveHmemPath(), config)` instead.
|
|
4191
|
-
*/
|
|
4192
|
-
export function openAgentMemory(projectDir, templateName, config) {
|
|
4193
|
-
console.error("[hmem] DEPRECATED: openAgentMemory() — use new HmemStore(resolveHmemPath(), config)");
|
|
4194
|
-
const hmemPath = resolveHmemPathLegacy(projectDir, templateName);
|
|
4195
|
-
return new HmemStore(hmemPath, config);
|
|
4196
|
-
}
|
|
4197
4174
|
/**
|
|
4198
4175
|
* Open (or create) the shared company knowledge store (company.hmem).
|
|
4199
4176
|
*/
|
|
@@ -4201,146 +4178,4 @@ export function openCompanyMemory(projectDir, config) {
|
|
|
4201
4178
|
const hmemPath = path.join(projectDir, "company.hmem");
|
|
4202
4179
|
return new HmemStore(hmemPath, config);
|
|
4203
4180
|
}
|
|
4204
|
-
/**
|
|
4205
|
-
* Route a task to the best-matching agent based on memory content.
|
|
4206
|
-
* @deprecated This function scans the Agents/ directory structure which is being phased out.
|
|
4207
|
-
* Future versions will use a config-based file list instead.
|
|
4208
|
-
*
|
|
4209
|
-
* Scans all agent .hmem files in the project directory and scores them
|
|
4210
|
-
* against the provided tags and/or search keywords.
|
|
4211
|
-
*
|
|
4212
|
-
* Scoring: for each agent store, find entries sharing the given tags
|
|
4213
|
-
* with tier-weighted scoring (rare=3, medium=2, common=1).
|
|
4214
|
-
* FTS5 keyword matching supplements tag scoring.
|
|
4215
|
-
*/
|
|
4216
|
-
export function routeTask(projectDir, tags, keywords, limit = 5, config) {
|
|
4217
|
-
// Discover all agent .hmem files — requires multi-agent setup
|
|
4218
|
-
const agentsDir = path.join(projectDir, "Agents");
|
|
4219
|
-
if (!fs.existsSync(agentsDir))
|
|
4220
|
-
return [];
|
|
4221
|
-
const agentDirs = fs.readdirSync(agentsDir).filter(name => {
|
|
4222
|
-
const agentPath = path.join(agentsDir, name);
|
|
4223
|
-
return fs.statSync(agentPath).isDirectory() &&
|
|
4224
|
-
fs.existsSync(path.join(agentPath, `${name}.hmem`));
|
|
4225
|
-
});
|
|
4226
|
-
const results = [];
|
|
4227
|
-
for (const agentName of agentDirs) {
|
|
4228
|
-
const hmemPath = path.join(agentsDir, agentName, `${agentName}.hmem`);
|
|
4229
|
-
let db;
|
|
4230
|
-
try {
|
|
4231
|
-
db = new Database(hmemPath, { readonly: true });
|
|
4232
|
-
}
|
|
4233
|
-
catch {
|
|
4234
|
-
continue;
|
|
4235
|
-
}
|
|
4236
|
-
try {
|
|
4237
|
-
let agentScore = 0;
|
|
4238
|
-
const topEntries = [];
|
|
4239
|
-
// Phase 1: Tag-based scoring
|
|
4240
|
-
if (tags.length > 0) {
|
|
4241
|
-
// Get global tag frequencies for THIS store
|
|
4242
|
-
const freqRows = db.prepare(`
|
|
4243
|
-
SELECT tag, COUNT(DISTINCT
|
|
4244
|
-
CASE WHEN entry_id LIKE '%.%'
|
|
4245
|
-
THEN SUBSTR(entry_id, 1, INSTR(entry_id, '.') - 1)
|
|
4246
|
-
ELSE entry_id END
|
|
4247
|
-
) as freq FROM memory_tags GROUP BY tag
|
|
4248
|
-
`).all();
|
|
4249
|
-
const tagFreq = new Map();
|
|
4250
|
-
for (const r of freqRows)
|
|
4251
|
-
tagFreq.set(r.tag, r.freq);
|
|
4252
|
-
// Find entries sharing any of the given tags
|
|
4253
|
-
const normalizedTags = tags.map(t => t.startsWith("#") ? t.toLowerCase() : `#${t.toLowerCase()}`);
|
|
4254
|
-
const placeholders = normalizedTags.map(() => "?").join(", ");
|
|
4255
|
-
const matchRows = db.prepare(`
|
|
4256
|
-
SELECT
|
|
4257
|
-
CASE WHEN entry_id LIKE '%.%'
|
|
4258
|
-
THEN SUBSTR(entry_id, 1, INSTR(entry_id, '.') - 1)
|
|
4259
|
-
ELSE entry_id END as root_id,
|
|
4260
|
-
tag
|
|
4261
|
-
FROM memory_tags WHERE tag IN (${placeholders})
|
|
4262
|
-
`).all(...normalizedTags);
|
|
4263
|
-
// Group by root and score
|
|
4264
|
-
const byRoot = new Map();
|
|
4265
|
-
for (const r of matchRows) {
|
|
4266
|
-
if (r.root_id.startsWith("O"))
|
|
4267
|
-
continue; // skip O-entries
|
|
4268
|
-
let set = byRoot.get(r.root_id);
|
|
4269
|
-
if (!set) {
|
|
4270
|
-
set = new Set();
|
|
4271
|
-
byRoot.set(r.root_id, set);
|
|
4272
|
-
}
|
|
4273
|
-
set.add(r.tag);
|
|
4274
|
-
}
|
|
4275
|
-
for (const [rootId, matchedTags] of byRoot) {
|
|
4276
|
-
let entryScore = 0;
|
|
4277
|
-
for (const tag of matchedTags) {
|
|
4278
|
-
const freq = tagFreq.get(tag) ?? 999;
|
|
4279
|
-
if (freq <= 5)
|
|
4280
|
-
entryScore += 3;
|
|
4281
|
-
else if (freq <= 20)
|
|
4282
|
-
entryScore += 2;
|
|
4283
|
-
else
|
|
4284
|
-
entryScore += 1;
|
|
4285
|
-
}
|
|
4286
|
-
agentScore += entryScore;
|
|
4287
|
-
// Get entry title
|
|
4288
|
-
const row = db.prepare("SELECT title, level_1 FROM memories WHERE id = ? AND obsolete != 1 AND irrelevant != 1").get(rootId);
|
|
4289
|
-
if (row) {
|
|
4290
|
-
topEntries.push({
|
|
4291
|
-
id: rootId,
|
|
4292
|
-
title: row.title || row.level_1?.substring(0, 50) || rootId,
|
|
4293
|
-
score: entryScore,
|
|
4294
|
-
});
|
|
4295
|
-
}
|
|
4296
|
-
}
|
|
4297
|
-
}
|
|
4298
|
-
// Phase 2: FTS5 keyword supplement
|
|
4299
|
-
if (keywords && keywords.trim().length > 0) {
|
|
4300
|
-
try {
|
|
4301
|
-
const words = keywords.trim().split(/\s+/).filter(w => w.length > 3).slice(0, 5);
|
|
4302
|
-
if (words.length > 0) {
|
|
4303
|
-
const orQuery = words.join(" OR ");
|
|
4304
|
-
const ftsRows = db.prepare(`
|
|
4305
|
-
SELECT rm.root_id, rm.node_id FROM hmem_fts_rowid_map rm
|
|
4306
|
-
JOIN hmem_fts f ON f.rowid = rm.fts_rowid
|
|
4307
|
-
WHERE hmem_fts MATCH ? LIMIT 20
|
|
4308
|
-
`).all(orQuery);
|
|
4309
|
-
const ftsRoots = new Set(ftsRows.map(r => r.root_id).filter(id => !id.startsWith("O")));
|
|
4310
|
-
for (const rootId of ftsRoots) {
|
|
4311
|
-
if (topEntries.some(e => e.id === rootId))
|
|
4312
|
-
continue; // already scored via tags
|
|
4313
|
-
const row = db.prepare("SELECT title, level_1 FROM memories WHERE id = ? AND obsolete != 1 AND irrelevant != 1").get(rootId);
|
|
4314
|
-
if (row) {
|
|
4315
|
-
agentScore += 1; // FTS matches get 1 point each
|
|
4316
|
-
topEntries.push({
|
|
4317
|
-
id: rootId,
|
|
4318
|
-
title: row.title || row.level_1?.substring(0, 50) || rootId,
|
|
4319
|
-
score: 1,
|
|
4320
|
-
});
|
|
4321
|
-
}
|
|
4322
|
-
}
|
|
4323
|
-
}
|
|
4324
|
-
}
|
|
4325
|
-
catch { /* FTS5 may not exist in all stores */ }
|
|
4326
|
-
}
|
|
4327
|
-
if (agentScore > 0) {
|
|
4328
|
-
// Sort entries by score, keep top 5
|
|
4329
|
-
topEntries.sort((a, b) => b.score - a.score);
|
|
4330
|
-
results.push({
|
|
4331
|
-
agent: agentName,
|
|
4332
|
-
score: agentScore,
|
|
4333
|
-
entryCount: topEntries.length,
|
|
4334
|
-
topEntries: topEntries.slice(0, 5),
|
|
4335
|
-
});
|
|
4336
|
-
}
|
|
4337
|
-
}
|
|
4338
|
-
finally {
|
|
4339
|
-
db.close();
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
// Sort agents by score
|
|
4343
|
-
results.sort((a, b) => b.score - a.score);
|
|
4344
|
-
return results.slice(0, limit);
|
|
4345
|
-
}
|
|
4346
4181
|
//# sourceMappingURL=hmem-store.js.map
|