@gamaze/hicortex 0.3.15 → 0.3.17

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.
@@ -307,9 +307,10 @@ async function stageLinks(db, memories, embedFn, dryRun) {
307
307
  for (const neighbor of neighbors) {
308
308
  const similarity = 1.0 - neighbor.distance;
309
309
  if (similarity > CONSOLIDATE_LINK_THRESHOLD) {
310
+ const relationship = classifyRelationship(mem, neighbor, similarity);
310
311
  if (!dryRun) {
311
312
  try {
312
- storage.addLink(db, mem.id, neighbor.id, "relates_to", similarity);
313
+ storage.addLink(db, mem.id, neighbor.id, relationship, similarity);
313
314
  autoLinked++;
314
315
  }
315
316
  catch {
@@ -328,6 +329,29 @@ async function stageLinks(db, memories, embedFn, dryRun) {
328
329
  }
329
330
  return { auto_linked: autoLinked, failed };
330
331
  }
332
+ /**
333
+ * Classify the relationship between two memories based on type, temporal ordering, and similarity.
334
+ */
335
+ function classifyRelationship(source, target, similarity) {
336
+ // Lesson derived from episode(s)
337
+ if (source.memory_type === "lesson" && target.memory_type === "episode")
338
+ return "derives";
339
+ if (target.memory_type === "lesson" && source.memory_type === "episode")
340
+ return "derives";
341
+ // Same type + very high similarity + different timestamps → newer updates older
342
+ if (source.memory_type === target.memory_type &&
343
+ similarity > 0.8 &&
344
+ source.created_at !== target.created_at) {
345
+ return "updates";
346
+ }
347
+ // Same project, moderate similarity → extends
348
+ if (source.project && target.project &&
349
+ source.project === target.project &&
350
+ similarity > 0.55 && similarity <= 0.8) {
351
+ return "extends";
352
+ }
353
+ return "relates_to";
354
+ }
331
355
  // ---------------------------------------------------------------------------
332
356
  // Stage 4: Decay & Prune
333
357
  // ---------------------------------------------------------------------------
package/dist/db.js CHANGED
@@ -103,7 +103,8 @@ CREATE TABLE IF NOT EXISTS memories (
103
103
  source_session TEXT,
104
104
  project TEXT,
105
105
  privacy TEXT DEFAULT 'WORK',
106
- memory_type TEXT DEFAULT 'episode'
106
+ memory_type TEXT DEFAULT 'episode',
107
+ updated_at TIMESTAMP
107
108
  );
108
109
 
109
110
  CREATE TABLE IF NOT EXISTS memory_links (
@@ -184,6 +185,9 @@ function migrate(db) {
184
185
  db.exec("UPDATE memories SET ingested_at = created_at");
185
186
  db.exec("CREATE INDEX IF NOT EXISTS idx_memories_ingested ON memories(ingested_at)");
186
187
  }
188
+ if (!colNames.has("updated_at")) {
189
+ db.exec("ALTER TABLE memories ADD COLUMN updated_at TIMESTAMP");
190
+ }
187
191
  }
188
192
  /**
189
193
  * Return database statistics.
package/dist/index.js CHANGED
@@ -352,11 +352,88 @@ exports.default = {
352
352
  }
353
353
  },
354
354
  }), { name: "hicortex_lessons" });
355
+ api.registerTool((_ctx) => ({
356
+ name: "hicortex_update",
357
+ description: "Update an existing memory. Use after searching to fix incorrect information. If content changes, the embedding is re-computed.",
358
+ parameters: {
359
+ type: "object",
360
+ properties: {
361
+ id: { type: "string", description: "Memory ID (from search results, first 8 chars or full UUID)" },
362
+ content: { type: "string", description: "New content text" },
363
+ project: { type: "string", description: "New project name" },
364
+ memory_type: { type: "string", enum: ["episode", "lesson", "fact", "decision"], description: "New memory type" },
365
+ },
366
+ required: ["id"],
367
+ },
368
+ async execute(_callId, args, _ctx) {
369
+ if (!db)
370
+ return { error: "Hicortex not initialized" };
371
+ try {
372
+ const fullId = resolveMemoryId(db, args.id);
373
+ if (!fullId)
374
+ return { error: `Memory not found: ${args.id}` };
375
+ const fields = {};
376
+ if (args.content !== undefined)
377
+ fields.content = args.content;
378
+ if (args.project !== undefined)
379
+ fields.project = args.project;
380
+ if (args.memory_type !== undefined)
381
+ fields.memory_type = args.memory_type;
382
+ if (Object.keys(fields).length === 0)
383
+ return { error: "No fields to update" };
384
+ storage.updateMemory(db, fullId, fields);
385
+ if (args.content !== undefined) {
386
+ const embedding = await (0, embedder_js_1.embed)(args.content);
387
+ db.prepare("DELETE FROM memory_vectors WHERE id = ?").run(fullId);
388
+ db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)").run(fullId, Buffer.from(embedding.buffer));
389
+ }
390
+ return { content: [{ type: "text", text: `Memory updated (id: ${fullId.slice(0, 8)})` }] };
391
+ }
392
+ catch (err) {
393
+ return { error: `Update failed: ${err instanceof Error ? err.message : String(err)}` };
394
+ }
395
+ },
396
+ }), { name: "hicortex_update" });
397
+ api.registerTool((_ctx) => ({
398
+ name: "hicortex_delete",
399
+ description: "Permanently delete a memory and its links. Use when a memory is incorrect and should be removed entirely.",
400
+ parameters: {
401
+ type: "object",
402
+ properties: {
403
+ id: { type: "string", description: "Memory ID (from search results, first 8 chars or full UUID)" },
404
+ },
405
+ required: ["id"],
406
+ },
407
+ execute(_callId, args, _ctx) {
408
+ if (!db)
409
+ return { error: "Hicortex not initialized" };
410
+ try {
411
+ const fullId = resolveMemoryId(db, args.id);
412
+ if (!fullId)
413
+ return { error: `Memory not found: ${args.id}` };
414
+ storage.deleteMemory(db, fullId);
415
+ return { content: [{ type: "text", text: `Memory deleted (id: ${fullId.slice(0, 8)})` }] };
416
+ }
417
+ catch (err) {
418
+ return { error: `Delete failed: ${err instanceof Error ? err.message : String(err)}` };
419
+ }
420
+ },
421
+ }), { name: "hicortex_delete" });
355
422
  },
356
423
  };
357
424
  // ---------------------------------------------------------------------------
358
425
  // Helpers
359
426
  // ---------------------------------------------------------------------------
427
+ function resolveMemoryId(database, idPrefix) {
428
+ if (idPrefix.length >= 36) {
429
+ const row = database.prepare("SELECT id FROM memories WHERE id = ?").get(idPrefix);
430
+ return row?.id ?? null;
431
+ }
432
+ const rows = database.prepare("SELECT id FROM memories WHERE id LIKE ?").all(`${idPrefix}%`);
433
+ if (rows.length === 1)
434
+ return rows[0].id;
435
+ return null;
436
+ }
360
437
  // ---------------------------------------------------------------------------
361
438
  // Auto-configure LLM: resolve config → test connection → persist if new
362
439
  // ---------------------------------------------------------------------------
@@ -477,6 +554,8 @@ const HICORTEX_TOOLS = [
477
554
  "hicortex_context",
478
555
  "hicortex_ingest",
479
556
  "hicortex_lessons",
557
+ "hicortex_update",
558
+ "hicortex_delete",
480
559
  ];
481
560
  /**
482
561
  * Ensure hicortex tools are in tools.allow so they're visible to agents
@@ -147,6 +147,64 @@ function createMcpServer() {
147
147
  return { content: [{ type: "text", text: `Ingest failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
148
148
  }
149
149
  });
150
+ // -- hicortex_update --
151
+ server.tool("hicortex_update", "Update an existing memory. Use after searching to fix incorrect information. If content changes, the embedding is re-computed.", {
152
+ id: zod_1.z.string().describe("Memory ID (from search results, first 8 chars or full UUID)"),
153
+ content: zod_1.z.string().optional().describe("New content text"),
154
+ project: zod_1.z.string().optional().describe("New project name"),
155
+ memory_type: zod_1.z.enum(["episode", "lesson", "fact", "decision"]).optional().describe("New memory type"),
156
+ }, async ({ id, content, project, memory_type }) => {
157
+ if (!db)
158
+ return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
159
+ try {
160
+ // Resolve short ID prefix to full ID
161
+ const fullId = resolveMemoryId(db, id);
162
+ if (!fullId)
163
+ return { content: [{ type: "text", text: `Memory not found: ${id}` }], isError: true };
164
+ const fields = {};
165
+ if (content !== undefined)
166
+ fields.content = content;
167
+ if (project !== undefined)
168
+ fields.project = project;
169
+ if (memory_type !== undefined)
170
+ fields.memory_type = memory_type;
171
+ if (Object.keys(fields).length === 0) {
172
+ return { content: [{ type: "text", text: "No fields to update" }], isError: true };
173
+ }
174
+ const before = storage.getMemory(db, fullId);
175
+ storage.updateMemory(db, fullId, fields);
176
+ // Re-embed if content changed
177
+ if (content !== undefined) {
178
+ const embedding = await (0, embedder_js_1.embed)(content);
179
+ db.prepare("DELETE FROM memory_vectors WHERE id = ?").run(fullId);
180
+ db.prepare("INSERT INTO memory_vectors (id, embedding) VALUES (?, ?)").run(fullId, Buffer.from(embedding.buffer));
181
+ }
182
+ const changed = Object.keys(fields).map(k => `${k}: "${String(before?.[k] ?? "").slice(0, 80)}" → "${String(fields[k]).slice(0, 80)}"`).join(", ");
183
+ return { content: [{ type: "text", text: `Memory updated (id: ${fullId.slice(0, 8)}). Changed: ${changed}` }] };
184
+ }
185
+ catch (err) {
186
+ return { content: [{ type: "text", text: `Update failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
187
+ }
188
+ });
189
+ // -- hicortex_delete --
190
+ server.tool("hicortex_delete", "Permanently delete a memory and its links. Use when a memory is incorrect and should be removed entirely.", {
191
+ id: zod_1.z.string().describe("Memory ID (from search results, first 8 chars or full UUID)"),
192
+ }, async ({ id }) => {
193
+ if (!db)
194
+ return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
195
+ try {
196
+ const fullId = resolveMemoryId(db, id);
197
+ if (!fullId)
198
+ return { content: [{ type: "text", text: `Memory not found: ${id}` }], isError: true };
199
+ const memory = storage.getMemory(db, fullId);
200
+ storage.deleteMemory(db, fullId);
201
+ const preview = memory?.content?.slice(0, 200) ?? "(unknown)";
202
+ return { content: [{ type: "text", text: `Memory deleted (id: ${fullId.slice(0, 8)}). Content was: ${preview}` }] };
203
+ }
204
+ catch (err) {
205
+ return { content: [{ type: "text", text: `Delete failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
206
+ }
207
+ });
150
208
  // -- hicortex_lessons --
151
209
  server.tool("hicortex_lessons", "Get actionable lessons learned from past sessions. Auto-generated insights about mistakes to avoid.", {
152
210
  days: zod_1.z.coerce.number().optional().describe("Look back N days (default 7)"),
@@ -378,6 +436,23 @@ async function startServer(options = {}) {
378
436
  // ---------------------------------------------------------------------------
379
437
  // Helpers
380
438
  // ---------------------------------------------------------------------------
439
+ /**
440
+ * Resolve a short ID prefix (e.g. "a1b2c3d4") to a full memory UUID.
441
+ */
442
+ function resolveMemoryId(database, idPrefix) {
443
+ if (idPrefix.length >= 36) {
444
+ // Full UUID — check existence
445
+ const row = database.prepare("SELECT id FROM memories WHERE id = ?").get(idPrefix);
446
+ return row?.id ?? null;
447
+ }
448
+ // Short prefix — find matching memory
449
+ const rows = database.prepare("SELECT id FROM memories WHERE id LIKE ?").all(`${idPrefix}%`);
450
+ if (rows.length === 1)
451
+ return rows[0].id;
452
+ if (rows.length > 1)
453
+ return null; // Ambiguous
454
+ return null;
455
+ }
381
456
  /**
382
457
  * Read ~/.hicortex/config.json (persisted by init with LLM and license config).
383
458
  */
package/dist/storage.js CHANGED
@@ -77,21 +77,22 @@ const ALLOWED_UPDATE_FIELDS = new Set([
77
77
  "project",
78
78
  "privacy",
79
79
  "memory_type",
80
+ "updated_at",
80
81
  ]);
81
82
  /**
82
83
  * Update specific fields on a memory.
83
84
  */
84
85
  function updateMemory(db, memoryId, fields) {
85
- const keys = Object.keys(fields);
86
- if (keys.length === 0)
87
- return;
86
+ // Auto-set updated_at timestamp
87
+ const fieldsWithTimestamp = { ...fields, updated_at: new Date().toISOString() };
88
+ const keys = Object.keys(fieldsWithTimestamp);
88
89
  for (const k of keys) {
89
90
  if (!ALLOWED_UPDATE_FIELDS.has(k)) {
90
91
  throw new Error(`Cannot update field: ${k}`);
91
92
  }
92
93
  }
93
94
  const setClause = keys.map((k) => `"${k}" = ?`).join(", ");
94
- const values = keys.map((k) => fields[k]);
95
+ const values = keys.map((k) => fieldsWithTimestamp[k]);
95
96
  values.push(memoryId);
96
97
  db.prepare(`UPDATE memories SET ${setClause} WHERE id = ?`).run(...values);
97
98
  }
package/dist/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface Memory {
16
16
  project: string | null;
17
17
  privacy: "PUBLIC" | "WORK" | "PERSONAL" | "SENSITIVE";
18
18
  memory_type: "episode" | "lesson" | "fact" | "decision";
19
+ updated_at: string | null;
19
20
  }
20
21
  /** A link between two memories. */
21
22
  export interface MemoryLink {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "Human-like memory for self-improving AI agents. Automatic capturing, nightly reflection, and cross-agent learning. Works with Claude Code and OpenClaw.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {