@gamaze/hicortex 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/consolidate.js +75 -8
- package/dist/graph.d.ts +54 -0
- package/dist/graph.js +251 -0
- package/dist/mcp-server.js +56 -0
- package/dist/types.d.ts +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,12 +45,14 @@ openclaw gateway restart
|
|
|
45
45
|
|
|
46
46
|
## Agent Tools (MCP)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
8 tools available via MCP:
|
|
49
49
|
|
|
50
50
|
- **hicortex_search** — Semantic search across all stored memories
|
|
51
51
|
- **hicortex_context** — Get recent decisions and project state
|
|
52
52
|
- **hicortex_ingest** — Store a memory directly
|
|
53
53
|
- **hicortex_lessons** — Get actionable lessons from reflection
|
|
54
|
+
- **hicortex_index** — Get the knowledge domain index (what topics are stored)
|
|
55
|
+
- **hicortex_graph** — Graph traversal: neighbors, hubs, shortest paths
|
|
54
56
|
- **hicortex_update** — Fix incorrect memories (re-embeds on content change)
|
|
55
57
|
- **hicortex_delete** — Remove memories with cascade cleanup
|
|
56
58
|
|
|
@@ -104,6 +106,9 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
104
106
|
| `reflectBaseUrl` | Separate Ollama instance for reflection |
|
|
105
107
|
| `authToken` | Bearer token for endpoint auth |
|
|
106
108
|
| `licenseKey` | License key for higher tiers |
|
|
109
|
+
| `lessonTarget` | Injection target file (default: `~/.claude/CLAUDE.md`) |
|
|
110
|
+
| `moduleIndexTokenBudget` | Max tokens for domain index in injection (default: 500) |
|
|
111
|
+
| `telemetry` | Anonymous usage telemetry, `false` to opt out |
|
|
107
112
|
|
|
108
113
|
Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze.com/docs/configuration.html)
|
|
109
114
|
|
package/dist/consolidate.js
CHANGED
|
@@ -48,6 +48,7 @@ const storage = __importStar(require("./storage.js"));
|
|
|
48
48
|
const prompts_js_1 = require("./prompts.js");
|
|
49
49
|
const node_crypto_1 = require("node:crypto");
|
|
50
50
|
const features_js_1 = require("./features.js");
|
|
51
|
+
const graph_js_1 = require("./graph.js");
|
|
51
52
|
const state_js_1 = require("./state.js");
|
|
52
53
|
// Default config constants (matching Python config.py)
|
|
53
54
|
const CONSOLIDATE_MAX_LLM_CALLS = 200;
|
|
@@ -352,14 +353,49 @@ async function stageDomainCuration(db, llm, budget, dryRun, stateDir) {
|
|
|
352
353
|
const totalLessons = lessonRows.reduce((s, r) => s + r.cnt, 0);
|
|
353
354
|
let domains;
|
|
354
355
|
if (!(0, features_js_1.isPro)()) {
|
|
355
|
-
// OSS
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
356
|
+
// OSS: Louvain community detection on the memory_links graph (zero LLM cost)
|
|
357
|
+
const graph = (0, graph_js_1.louvainCommunities)(db);
|
|
358
|
+
if (graph.communities.length > 1 && graph.edgeCount >= 5) {
|
|
359
|
+
// Map communities to domains by finding the dominant project in each
|
|
360
|
+
// Pre-load all memory→project mappings in one query (avoids N+1)
|
|
361
|
+
const allProjectRows = db
|
|
362
|
+
.prepare("SELECT id, project FROM memories WHERE project IS NOT NULL")
|
|
363
|
+
.all();
|
|
364
|
+
const memProject = new Map(allProjectRows.map((r) => [r.id, r.project]));
|
|
365
|
+
domains = [];
|
|
366
|
+
for (const comm of graph.communities) {
|
|
367
|
+
const projectCounts = new Map();
|
|
368
|
+
for (const memId of comm.members) {
|
|
369
|
+
const proj = memProject.get(memId);
|
|
370
|
+
if (proj) {
|
|
371
|
+
projectCounts.set(proj, (projectCounts.get(proj) ?? 0) + 1);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const projects = [...projectCounts.keys()];
|
|
375
|
+
if (projects.length === 0)
|
|
376
|
+
continue;
|
|
377
|
+
// Name domain after the dominant project or combine top 2
|
|
378
|
+
const sorted = [...projectCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
379
|
+
const name = sorted.length >= 2 && sorted[1][1] > sorted[0][1] * 0.3
|
|
380
|
+
? `${sorted[0][0]} + ${sorted[1][0]}`
|
|
381
|
+
: sorted[0][0];
|
|
382
|
+
const memoryCount = projects.reduce((s, p) => s + (projectRows.find((r) => r.project === p)?.cnt ?? 0), 0);
|
|
383
|
+
const lessonCount = projects.reduce((s, p) => s + (lessonsByProject.get(p) ?? 0), 0);
|
|
384
|
+
domains.push({ name, projects, memoryCount, lessonCount, keywords: [] });
|
|
385
|
+
}
|
|
386
|
+
domains.sort((a, b) => b.memoryCount - a.memoryCount);
|
|
387
|
+
console.log(`[hicortex] Louvain clustering: ${graph.communities.length} communities, modularity ${graph.modularity.toFixed(3)}`);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// Not enough edges for meaningful clustering — fall back to project=domain
|
|
391
|
+
domains = projectRows.map((r) => ({
|
|
392
|
+
name: r.project,
|
|
393
|
+
projects: [r.project],
|
|
394
|
+
memoryCount: r.cnt,
|
|
395
|
+
lessonCount: lessonsByProject.get(r.project) ?? 0,
|
|
396
|
+
keywords: [],
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
363
399
|
}
|
|
364
400
|
else {
|
|
365
401
|
// Pro: LLM-curated domains
|
|
@@ -510,6 +546,35 @@ function classifyRelationship(source, target, similarity) {
|
|
|
510
546
|
return "relates_to";
|
|
511
547
|
}
|
|
512
548
|
// ---------------------------------------------------------------------------
|
|
549
|
+
// Stage 3.5: Hub Detection & Strength Boost
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
const HUB_BOOST = 0.1;
|
|
552
|
+
const HUB_STRENGTH_CAP = 1.0;
|
|
553
|
+
function stageHubBoost(db, dryRun) {
|
|
554
|
+
const hubs = (0, graph_js_1.detectHubs)(db);
|
|
555
|
+
if (hubs.length === 0)
|
|
556
|
+
return { hubs_found: 0, boosted: 0 };
|
|
557
|
+
let boosted = 0;
|
|
558
|
+
if (!dryRun) {
|
|
559
|
+
const stmt = db.prepare("UPDATE memories SET base_strength = MIN(?, base_strength + ?) WHERE id = ? AND base_strength < ?");
|
|
560
|
+
const tx = db.transaction(() => {
|
|
561
|
+
for (const hub of hubs) {
|
|
562
|
+
const result = stmt.run(HUB_STRENGTH_CAP, HUB_BOOST, hub.id, HUB_STRENGTH_CAP);
|
|
563
|
+
if (result.changes > 0)
|
|
564
|
+
boosted++;
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
tx();
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
boosted = hubs.length;
|
|
571
|
+
}
|
|
572
|
+
if (hubs.length > 0) {
|
|
573
|
+
console.log(`[hicortex] Hub detection: ${hubs.length} hubs found, ${boosted} boosted (+${HUB_BOOST})`);
|
|
574
|
+
}
|
|
575
|
+
return { hubs_found: hubs.length, boosted };
|
|
576
|
+
}
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
513
578
|
// Stage 4: Decay & Prune
|
|
514
579
|
// ---------------------------------------------------------------------------
|
|
515
580
|
function stageDecayPrune(db, dryRun) {
|
|
@@ -597,6 +662,8 @@ async function runConsolidation(db, llm, embedFn, dryRun = false, skipReflection
|
|
|
597
662
|
report.stages.domain_curation = await stageDomainCuration(db, llm, budget, dryRun, stateDir);
|
|
598
663
|
// Stage 3: Link Discovery
|
|
599
664
|
report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun);
|
|
665
|
+
// Stage 3.5: Hub Detection — boost highly-connected memories
|
|
666
|
+
report.stages.hub_boost = stageHubBoost(db, dryRun);
|
|
600
667
|
// Stage 4: Decay & Prune
|
|
601
668
|
report.stages.decay_prune = stageDecayPrune(db, dryRun);
|
|
602
669
|
}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph analysis for the memory link network.
|
|
3
|
+
*
|
|
4
|
+
* Pure-JS Louvain community detection + hub node identification.
|
|
5
|
+
* Operates on the memory_links table without external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Louvain algorithm: iteratively merges nodes into communities to maximize
|
|
8
|
+
* modularity. Produces quality comparable to Leiden for the graph sizes
|
|
9
|
+
* we deal with (hundreds to low thousands of nodes).
|
|
10
|
+
*/
|
|
11
|
+
import type Database from "better-sqlite3";
|
|
12
|
+
export interface GraphCommunity {
|
|
13
|
+
id: number;
|
|
14
|
+
members: string[];
|
|
15
|
+
size: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HubNode {
|
|
18
|
+
id: string;
|
|
19
|
+
linkCount: number;
|
|
20
|
+
project: string | null;
|
|
21
|
+
domain: string | null;
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
export interface GraphAnalysis {
|
|
25
|
+
communities: GraphCommunity[];
|
|
26
|
+
hubs: HubNode[];
|
|
27
|
+
nodeCount: number;
|
|
28
|
+
edgeCount: number;
|
|
29
|
+
modularity: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run Louvain community detection. Returns community assignments.
|
|
33
|
+
*/
|
|
34
|
+
export declare function louvainCommunities(db: Database.Database): {
|
|
35
|
+
communities: GraphCommunity[];
|
|
36
|
+
modularity: number;
|
|
37
|
+
nodeCount: number;
|
|
38
|
+
edgeCount: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Find hub nodes — memories with link count significantly above median.
|
|
42
|
+
* Returns nodes with links > threshold (default: 2x median, minimum 3 links).
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectHubs(db: Database.Database, thresholdMultiplier?: number, minLinks?: number): HubNode[];
|
|
45
|
+
export interface GraphNeighbor {
|
|
46
|
+
id: string;
|
|
47
|
+
relationship: string;
|
|
48
|
+
strength: number;
|
|
49
|
+
direction: "outgoing" | "incoming";
|
|
50
|
+
content: string;
|
|
51
|
+
project: string | null;
|
|
52
|
+
}
|
|
53
|
+
export declare function getNeighbors(db: Database.Database, memoryId: string, limit?: number): GraphNeighbor[];
|
|
54
|
+
export declare function shortestPath(db: Database.Database, fromId: string, toId: string, maxDepth?: number): string[] | null;
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Graph analysis for the memory link network.
|
|
4
|
+
*
|
|
5
|
+
* Pure-JS Louvain community detection + hub node identification.
|
|
6
|
+
* Operates on the memory_links table without external dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Louvain algorithm: iteratively merges nodes into communities to maximize
|
|
9
|
+
* modularity. Produces quality comparable to Leiden for the graph sizes
|
|
10
|
+
* we deal with (hundreds to low thousands of nodes).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.louvainCommunities = louvainCommunities;
|
|
14
|
+
exports.detectHubs = detectHubs;
|
|
15
|
+
exports.getNeighbors = getNeighbors;
|
|
16
|
+
exports.shortestPath = shortestPath;
|
|
17
|
+
function loadGraph(db) {
|
|
18
|
+
const rows = db
|
|
19
|
+
.prepare("SELECT source_id, target_id, strength FROM memory_links")
|
|
20
|
+
.all();
|
|
21
|
+
const adj = new Map();
|
|
22
|
+
const nodes = new Set();
|
|
23
|
+
for (const { source_id, target_id, strength } of rows) {
|
|
24
|
+
nodes.add(source_id);
|
|
25
|
+
nodes.add(target_id);
|
|
26
|
+
const w = strength || 0.5;
|
|
27
|
+
if (!adj.has(source_id))
|
|
28
|
+
adj.set(source_id, []);
|
|
29
|
+
adj.get(source_id).push({ neighbor: target_id, weight: w });
|
|
30
|
+
if (!adj.has(target_id))
|
|
31
|
+
adj.set(target_id, []);
|
|
32
|
+
adj.get(target_id).push({ neighbor: source_id, weight: w });
|
|
33
|
+
}
|
|
34
|
+
return { adj, nodes, edgeCount: rows.length };
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Louvain community detection
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* Compute modularity of the current partition.
|
|
41
|
+
* Q = (1/2m) * sum_ij [ A_ij - k_i*k_j/(2m) ] * delta(c_i, c_j)
|
|
42
|
+
*/
|
|
43
|
+
function computeModularity(adj, community, totalWeight) {
|
|
44
|
+
if (totalWeight === 0)
|
|
45
|
+
return 0;
|
|
46
|
+
const m2 = 2 * totalWeight;
|
|
47
|
+
let q = 0;
|
|
48
|
+
for (const [node, neighbors] of adj) {
|
|
49
|
+
const ki = neighbors.reduce((s, e) => s + e.weight, 0);
|
|
50
|
+
const ci = community.get(node);
|
|
51
|
+
for (const { neighbor, weight } of neighbors) {
|
|
52
|
+
const kj = adj.get(neighbor)?.reduce((s, e) => s + e.weight, 0) ?? 0;
|
|
53
|
+
const cj = community.get(neighbor);
|
|
54
|
+
if (ci === cj) {
|
|
55
|
+
q += weight - (ki * kj) / m2;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return q / m2;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run Louvain community detection. Returns community assignments.
|
|
63
|
+
*/
|
|
64
|
+
function louvainCommunities(db) {
|
|
65
|
+
const { adj, nodes, edgeCount } = loadGraph(db);
|
|
66
|
+
if (nodes.size === 0) {
|
|
67
|
+
return { communities: [], modularity: 0, nodeCount: 0, edgeCount: 0 };
|
|
68
|
+
}
|
|
69
|
+
const totalWeight = [...adj.values()]
|
|
70
|
+
.reduce((s, edges) => s + edges.reduce((s2, e) => s2 + e.weight, 0), 0) / 2;
|
|
71
|
+
// Initialize: each node in its own community
|
|
72
|
+
const community = new Map();
|
|
73
|
+
let nextId = 0;
|
|
74
|
+
for (const node of nodes) {
|
|
75
|
+
community.set(node, nextId++);
|
|
76
|
+
}
|
|
77
|
+
// Phase 1: local moves — repeatedly move nodes to neighbor community with best modularity gain
|
|
78
|
+
let improved = true;
|
|
79
|
+
let iterations = 0;
|
|
80
|
+
const MAX_ITERATIONS = 50;
|
|
81
|
+
while (improved && iterations < MAX_ITERATIONS) {
|
|
82
|
+
improved = false;
|
|
83
|
+
iterations++;
|
|
84
|
+
for (const node of nodes) {
|
|
85
|
+
const currentComm = community.get(node);
|
|
86
|
+
const neighbors = adj.get(node) ?? [];
|
|
87
|
+
// Compute weight to each neighbor community
|
|
88
|
+
const commWeights = new Map();
|
|
89
|
+
for (const { neighbor, weight } of neighbors) {
|
|
90
|
+
const nc = community.get(neighbor);
|
|
91
|
+
commWeights.set(nc, (commWeights.get(nc) ?? 0) + weight);
|
|
92
|
+
}
|
|
93
|
+
// Find best community to move to
|
|
94
|
+
let bestComm = currentComm;
|
|
95
|
+
let bestGain = 0;
|
|
96
|
+
const ki = neighbors.reduce((s, e) => s + e.weight, 0);
|
|
97
|
+
for (const [targetComm, weightToComm] of commWeights) {
|
|
98
|
+
if (targetComm === currentComm)
|
|
99
|
+
continue;
|
|
100
|
+
// Simplified modularity gain: delta_Q ~ weight_to_comm - ki * sum_comm / (2m)
|
|
101
|
+
const sumComm = [...community.entries()]
|
|
102
|
+
.filter(([, c]) => c === targetComm)
|
|
103
|
+
.reduce((s, [n]) => s + (adj.get(n)?.reduce((s2, e) => s2 + e.weight, 0) ?? 0), 0);
|
|
104
|
+
const gain = weightToComm - (ki * sumComm) / (2 * totalWeight);
|
|
105
|
+
if (gain > bestGain) {
|
|
106
|
+
bestGain = gain;
|
|
107
|
+
bestComm = targetComm;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (bestComm !== currentComm) {
|
|
111
|
+
community.set(node, bestComm);
|
|
112
|
+
improved = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Build community list
|
|
117
|
+
const commMembers = new Map();
|
|
118
|
+
for (const [node, comm] of community) {
|
|
119
|
+
if (!commMembers.has(comm))
|
|
120
|
+
commMembers.set(comm, []);
|
|
121
|
+
commMembers.get(comm).push(node);
|
|
122
|
+
}
|
|
123
|
+
// Renumber communities 0..N-1
|
|
124
|
+
const communities = [];
|
|
125
|
+
let idx = 0;
|
|
126
|
+
for (const [, members] of commMembers) {
|
|
127
|
+
if (members.length > 0) {
|
|
128
|
+
communities.push({ id: idx++, members, size: members.length });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Sort by size desc
|
|
132
|
+
communities.sort((a, b) => b.size - a.size);
|
|
133
|
+
const modularity = computeModularity(adj, community, totalWeight);
|
|
134
|
+
return { communities, modularity, nodeCount: nodes.size, edgeCount };
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Hub node detection
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* Find hub nodes — memories with link count significantly above median.
|
|
141
|
+
* Returns nodes with links > threshold (default: 2x median, minimum 3 links).
|
|
142
|
+
*/
|
|
143
|
+
function detectHubs(db, thresholdMultiplier = 2, minLinks = 3) {
|
|
144
|
+
const rows = db
|
|
145
|
+
.prepare(`SELECT id, cnt FROM (
|
|
146
|
+
SELECT id, COUNT(*) as cnt FROM (
|
|
147
|
+
SELECT source_id AS id FROM memory_links
|
|
148
|
+
UNION ALL
|
|
149
|
+
SELECT target_id AS id FROM memory_links
|
|
150
|
+
) GROUP BY id
|
|
151
|
+
) ORDER BY cnt DESC`)
|
|
152
|
+
.all();
|
|
153
|
+
if (rows.length === 0)
|
|
154
|
+
return [];
|
|
155
|
+
// Compute median link count
|
|
156
|
+
const sorted = rows.map((r) => r.cnt).sort((a, b) => a - b);
|
|
157
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
158
|
+
const threshold = Math.max(median * thresholdMultiplier, minLinks);
|
|
159
|
+
const hubs = [];
|
|
160
|
+
for (const { id, cnt } of rows) {
|
|
161
|
+
if (cnt < threshold)
|
|
162
|
+
break; // sorted desc, no more hubs
|
|
163
|
+
const mem = db
|
|
164
|
+
.prepare("SELECT content, project, domain FROM memories WHERE id = ?")
|
|
165
|
+
.get(id);
|
|
166
|
+
if (!mem)
|
|
167
|
+
continue;
|
|
168
|
+
hubs.push({
|
|
169
|
+
id,
|
|
170
|
+
linkCount: cnt,
|
|
171
|
+
project: mem.project,
|
|
172
|
+
domain: mem.domain,
|
|
173
|
+
content: mem.content.slice(0, 200),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return hubs;
|
|
177
|
+
}
|
|
178
|
+
function getNeighbors(db, memoryId, limit = 10) {
|
|
179
|
+
const rows = db
|
|
180
|
+
.prepare(`SELECT source_id, target_id, relationship, strength
|
|
181
|
+
FROM memory_links
|
|
182
|
+
WHERE source_id = ? OR target_id = ?
|
|
183
|
+
ORDER BY strength DESC
|
|
184
|
+
LIMIT ?`)
|
|
185
|
+
.all(memoryId, memoryId, limit);
|
|
186
|
+
const results = [];
|
|
187
|
+
for (const row of rows) {
|
|
188
|
+
const isOutgoing = row.source_id === memoryId;
|
|
189
|
+
const neighborId = isOutgoing ? row.target_id : row.source_id;
|
|
190
|
+
const mem = db
|
|
191
|
+
.prepare("SELECT content, project FROM memories WHERE id = ?")
|
|
192
|
+
.get(neighborId);
|
|
193
|
+
if (!mem)
|
|
194
|
+
continue;
|
|
195
|
+
results.push({
|
|
196
|
+
id: neighborId,
|
|
197
|
+
relationship: row.relationship,
|
|
198
|
+
strength: row.strength,
|
|
199
|
+
direction: isOutgoing ? "outgoing" : "incoming",
|
|
200
|
+
content: mem.content.slice(0, 200),
|
|
201
|
+
project: mem.project,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Shortest path (for MCP tool)
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
function shortestPath(db, fromId, toId, maxDepth = 5) {
|
|
210
|
+
// BFS on the memory_links graph
|
|
211
|
+
const links = db
|
|
212
|
+
.prepare("SELECT source_id, target_id FROM memory_links")
|
|
213
|
+
.all();
|
|
214
|
+
const adj = new Map();
|
|
215
|
+
for (const { source_id, target_id } of links) {
|
|
216
|
+
if (!adj.has(source_id))
|
|
217
|
+
adj.set(source_id, new Set());
|
|
218
|
+
adj.get(source_id).add(target_id);
|
|
219
|
+
if (!adj.has(target_id))
|
|
220
|
+
adj.set(target_id, new Set());
|
|
221
|
+
adj.get(target_id).add(source_id);
|
|
222
|
+
}
|
|
223
|
+
if (!adj.has(fromId) || !adj.has(toId))
|
|
224
|
+
return null;
|
|
225
|
+
const visited = new Set([fromId]);
|
|
226
|
+
const parent = new Map();
|
|
227
|
+
const queue = [{ node: fromId, depth: 0 }];
|
|
228
|
+
while (queue.length > 0) {
|
|
229
|
+
const { node, depth } = queue.shift();
|
|
230
|
+
if (node === toId) {
|
|
231
|
+
// Reconstruct path
|
|
232
|
+
const path = [toId];
|
|
233
|
+
let current = toId;
|
|
234
|
+
while (parent.has(current)) {
|
|
235
|
+
current = parent.get(current);
|
|
236
|
+
path.unshift(current);
|
|
237
|
+
}
|
|
238
|
+
return path;
|
|
239
|
+
}
|
|
240
|
+
if (depth >= maxDepth)
|
|
241
|
+
continue;
|
|
242
|
+
for (const neighbor of adj.get(node) ?? []) {
|
|
243
|
+
if (!visited.has(neighbor)) {
|
|
244
|
+
visited.add(neighbor);
|
|
245
|
+
parent.set(neighbor, node);
|
|
246
|
+
queue.push({ node: neighbor, depth: depth + 1 });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -59,6 +59,7 @@ const features_js_1 = require("./features.js");
|
|
|
59
59
|
const state_js_1 = require("./state.js");
|
|
60
60
|
const embedder_js_1 = require("./embedder.js");
|
|
61
61
|
const storage = __importStar(require("./storage.js"));
|
|
62
|
+
const graph_js_1 = require("./graph.js");
|
|
62
63
|
const retrieval = __importStar(require("./retrieval.js"));
|
|
63
64
|
const consolidate_js_1 = require("./consolidate.js");
|
|
64
65
|
const seed_lesson_js_1 = require("./seed-lesson.js");
|
|
@@ -243,6 +244,61 @@ function createMcpServer() {
|
|
|
243
244
|
: "No memories yet.";
|
|
244
245
|
return { content: [{ type: "text", text }] };
|
|
245
246
|
});
|
|
247
|
+
// -- hicortex_graph --
|
|
248
|
+
server.tool("hicortex_graph", "Query the memory knowledge graph — find connected memories, hub nodes, or paths between memories.", {
|
|
249
|
+
operation: zod_1.z.enum(["neighbors", "hubs", "path"]).describe("Graph operation to perform"),
|
|
250
|
+
id: zod_1.z.string().optional().describe("Memory ID (required for neighbors and path operations)"),
|
|
251
|
+
target_id: zod_1.z.string().optional().describe("Target memory ID (required for path operation)"),
|
|
252
|
+
limit: zod_1.z.coerce.number().optional().describe("Max results (default 10)"),
|
|
253
|
+
domain: zod_1.z.string().optional().describe("Filter hubs by domain"),
|
|
254
|
+
}, async ({ operation, id, target_id, limit: resultLimit, domain: filterDomain }) => {
|
|
255
|
+
if (!db)
|
|
256
|
+
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
257
|
+
try {
|
|
258
|
+
if (operation === "neighbors") {
|
|
259
|
+
if (!id)
|
|
260
|
+
return { content: [{ type: "text", text: "id is required for neighbors operation" }], isError: true };
|
|
261
|
+
const resolvedId = resolveMemoryId(db, id);
|
|
262
|
+
if (!resolvedId)
|
|
263
|
+
return { content: [{ type: "text", text: `Memory not found: ${id}` }], isError: true };
|
|
264
|
+
const neighbors = (0, graph_js_1.getNeighbors)(db, resolvedId, resultLimit ?? 10);
|
|
265
|
+
if (neighbors.length === 0)
|
|
266
|
+
return { content: [{ type: "text", text: "No connected memories found." }] };
|
|
267
|
+
const text = neighbors.map((n) => `[${n.direction}] ${n.relationship} (${n.strength.toFixed(2)})\n ${n.id.slice(0, 8)} | ${n.project ?? "global"} | ${n.content}`).join("\n\n");
|
|
268
|
+
return { content: [{ type: "text", text }] };
|
|
269
|
+
}
|
|
270
|
+
if (operation === "hubs") {
|
|
271
|
+
let hubs = (0, graph_js_1.detectHubs)(db);
|
|
272
|
+
if (filterDomain) {
|
|
273
|
+
hubs = hubs.filter((h) => h.domain === filterDomain || h.project === filterDomain);
|
|
274
|
+
}
|
|
275
|
+
if (hubs.length === 0)
|
|
276
|
+
return { content: [{ type: "text", text: "No hub memories found." }] };
|
|
277
|
+
const text = hubs.slice(0, resultLimit ?? 10).map((h) => `**${h.id.slice(0, 8)}** (${h.linkCount} links) | ${h.domain ?? h.project ?? "global"}\n ${h.content}`).join("\n\n");
|
|
278
|
+
return { content: [{ type: "text", text }] };
|
|
279
|
+
}
|
|
280
|
+
if (operation === "path") {
|
|
281
|
+
if (!id || !target_id)
|
|
282
|
+
return { content: [{ type: "text", text: "id and target_id are required for path operation" }], isError: true };
|
|
283
|
+
const fromId = resolveMemoryId(db, id);
|
|
284
|
+
const toId = resolveMemoryId(db, target_id);
|
|
285
|
+
if (!fromId || !toId)
|
|
286
|
+
return { content: [{ type: "text", text: "One or both memory IDs not found" }], isError: true };
|
|
287
|
+
const path = (0, graph_js_1.shortestPath)(db, fromId, toId);
|
|
288
|
+
if (!path)
|
|
289
|
+
return { content: [{ type: "text", text: "No path found between these memories." }] };
|
|
290
|
+
const text = path.map((nodeId, i) => {
|
|
291
|
+
const mem = storage.getMemory(db, nodeId);
|
|
292
|
+
return `${i + 1}. ${nodeId.slice(0, 8)} | ${mem?.project ?? "?"} | ${mem?.content.slice(0, 150) ?? "?"}`;
|
|
293
|
+
}).join("\n");
|
|
294
|
+
return { content: [{ type: "text", text: `Path (${path.length} hops):\n${text}` }] };
|
|
295
|
+
}
|
|
296
|
+
return { content: [{ type: "text", text: `Unknown operation: ${operation}` }], isError: true };
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
return { content: [{ type: "text", text: `Graph query failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
300
|
+
}
|
|
301
|
+
});
|
|
246
302
|
return server;
|
|
247
303
|
}
|
|
248
304
|
// ---------------------------------------------------------------------------
|
package/dist/types.d.ts
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hicortex",
|
|
3
3
|
"name": "Hicortex — Long-term Memory That Learns",
|
|
4
4
|
"description": "Your agents remember past decisions, avoid repeated mistakes, and get smarter every day. Nightly reflection generates actionable lessons that automatically update agent behavior.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.7.0",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"skills": ["./skills/hicortex-memory", "./skills/hicortex-learn", "./skills/hicortex-activate"],
|
|
8
8
|
"configSchema": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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": {
|