memorix 1.4.3 → 1.4.4

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +11 -4
  3. package/README.zh-CN.md +7 -1
  4. package/TEAM.md +86 -86
  5. package/assets/star-history-dark.svg +8 -8
  6. package/assets/star-history-light.svg +8 -8
  7. package/assets/star-history.json +16 -2
  8. package/dist/cli/index.js +194 -109
  9. package/dist/cli/memcode.js +0 -0
  10. package/dist/index.js +39 -12
  11. package/dist/maintenance-runner.js +9 -7
  12. package/dist/media-audio-runner.js +9 -7
  13. package/dist/media-video-runner.js +9 -7
  14. package/dist/memcode/index.js +9 -7
  15. package/dist/memcode-runtime/CHANGELOG.md +19 -0
  16. package/dist/sdk.js +38 -11
  17. package/dist/vector-backfill-runner.js +9 -7
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +3 -3
  19. package/docs/API_REFERENCE.md +9 -5
  20. package/docs/CONFIGURATION.md +15 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +2 -2
  23. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +3 -3
  24. package/docs/MODULES.md +1 -1
  25. package/docs/README.md +1 -1
  26. package/docs/SETUP.md +3 -3
  27. package/docs/hooks-architecture.md +2 -1
  28. package/llms-full.txt +4 -4
  29. package/llms.txt +2 -2
  30. package/package.json +1 -1
  31. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  32. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  33. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  34. package/plugins/copilot/memorix/plugin.json +1 -1
  35. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  36. package/plugins/omp/memorix/package.json +1 -1
  37. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  38. package/plugins/pi/memorix/package.json +1 -1
  39. package/server.json +2 -2
  40. package/src/audit/index.ts +156 -156
  41. package/src/cli/commands/audit-list.ts +89 -89
  42. package/src/cli/commands/formation.ts +48 -48
  43. package/src/cli/commands/git-hook-install.ts +111 -111
  44. package/src/cli/commands/handoff.ts +54 -54
  45. package/src/cli/commands/hooks-status.ts +63 -63
  46. package/src/cli/commands/ingest.ts +44 -44
  47. package/src/cli/commands/integrate-shared.ts +15 -15
  48. package/src/cli/commands/lock.ts +82 -82
  49. package/src/cli/commands/media-image.ts +27 -0
  50. package/src/cli/commands/media.ts +8 -1
  51. package/src/cli/commands/memory.ts +44 -34
  52. package/src/cli/commands/message.ts +104 -104
  53. package/src/cli/commands/operator-shared.ts +62 -18
  54. package/src/cli/commands/poll.ts +58 -58
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +118 -118
  58. package/src/cli/commands/skills.ts +114 -114
  59. package/src/cli/commands/task.ts +167 -167
  60. package/src/cli/commands/transfer.ts +47 -47
  61. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  62. package/src/cli/tui/ChatView.tsx +234 -234
  63. package/src/cli/tui/CommandBar.tsx +312 -312
  64. package/src/cli/tui/ContextRail.tsx +118 -118
  65. package/src/cli/tui/HeaderBar.tsx +72 -72
  66. package/src/cli/tui/LogoBanner.tsx +51 -51
  67. package/src/cli/tui/Sidebar.tsx +179 -179
  68. package/src/cli/tui/index.ts +41 -41
  69. package/src/cli/tui/markdown-render.tsx +371 -371
  70. package/src/cli/tui/use-mouse.ts +157 -157
  71. package/src/cli/tui/useNavigation.ts +56 -56
  72. package/src/cli/version.ts +7 -7
  73. package/src/cli/workbench.ts +1 -1
  74. package/src/dashboard/project-classification.ts +64 -64
  75. package/src/git/hooks-path.ts +85 -85
  76. package/src/hooks/pattern-detector.ts +173 -173
  77. package/src/hooks/significance-filter.ts +250 -250
  78. package/src/llm/memory-manager.ts +328 -328
  79. package/src/llm/provider.ts +885 -885
  80. package/src/llm/quality.ts +248 -248
  81. package/src/media/minimax.ts +9 -1
  82. package/src/memory/attribution-guard.ts +249 -249
  83. package/src/memory/disclosure-policy.ts +135 -135
  84. package/src/memory/entity-extractor.ts +197 -197
  85. package/src/memory/formation/evaluate.ts +217 -217
  86. package/src/memory/formation/extract.ts +361 -361
  87. package/src/memory/formation/index.ts +417 -417
  88. package/src/memory/formation/resolve.ts +344 -344
  89. package/src/memory/formation/types.ts +315 -315
  90. package/src/memory/freshness.ts +122 -122
  91. package/src/memory/graph.ts +197 -197
  92. package/src/memory/refs.ts +94 -94
  93. package/src/memory/secret-filter.ts +79 -79
  94. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  95. package/src/orchestrate/adapters/claude.ts +111 -111
  96. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  97. package/src/orchestrate/adapters/codex.ts +41 -41
  98. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  99. package/src/orchestrate/adapters/gemini.ts +42 -42
  100. package/src/orchestrate/adapters/index.ts +73 -73
  101. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  102. package/src/orchestrate/adapters/opencode.ts +47 -47
  103. package/src/orchestrate/adapters/types.ts +77 -77
  104. package/src/orchestrate/capability-router.ts +284 -284
  105. package/src/orchestrate/context-compact.ts +188 -188
  106. package/src/orchestrate/cost-tracker.ts +219 -219
  107. package/src/orchestrate/error-recovery.ts +191 -191
  108. package/src/orchestrate/evidence.ts +140 -140
  109. package/src/orchestrate/ledger.ts +110 -110
  110. package/src/orchestrate/memorix-bridge.ts +343 -343
  111. package/src/orchestrate/output-budget.ts +80 -80
  112. package/src/orchestrate/permission.ts +152 -152
  113. package/src/orchestrate/pipeline-trace.ts +131 -131
  114. package/src/orchestrate/prompt-builder.ts +155 -155
  115. package/src/orchestrate/ring-buffer.ts +37 -37
  116. package/src/orchestrate/task-graph.ts +389 -389
  117. package/src/project/aliases.ts +374 -374
  118. package/src/rules/adapters/trae.ts +56 -56
  119. package/src/rules/syncer.ts +235 -235
  120. package/src/search/query-expansion.ts +52 -52
  121. package/src/server/formation-timeout.ts +27 -27
  122. package/src/server.ts +23 -1
  123. package/src/skills/mini-skills.ts +386 -386
  124. package/src/store/chat-store.ts +119 -119
  125. package/src/store/graph-store.ts +249 -249
  126. package/src/store/mini-skill-store.ts +349 -349
  127. package/src/store/persistence-json.ts +212 -212
  128. package/src/store/persistence.ts +291 -291
  129. package/src/store/project-affinity.ts +195 -195
  130. package/src/team/event-bus.ts +76 -76
  131. package/src/team/file-locks.ts +173 -173
  132. package/src/team/handoff.ts +161 -161
  133. package/src/team/messages.ts +203 -203
  134. package/src/team/poll.ts +132 -132
  135. package/src/team/tasks.ts +211 -211
  136. package/src/workspace/mcp-adapters/codex.ts +191 -191
  137. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  138. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  139. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  140. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  141. package/src/workspace/mcp-adapters/trae.ts +134 -134
  142. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  143. package/src/workspace/sanitizer.ts +60 -60
  144. package/dist/memcode-runtime/docs/images/exy.png +0 -0
@@ -1,122 +1,122 @@
1
- /**
2
- * Unified Freshness Gate (Phase 3a)
3
- *
4
- * Replaces withFreshObservations() as the public API for all retrieval
5
- * surfaces. Checks both observation and mini-skill generation counters
6
- * to ensure the Orama index is fully up-to-date before any read.
7
- *
8
- * withFreshObservations() remains in observations.ts as @internal —
9
- * only called by this module and legacy test code.
10
- */
11
-
12
- import { ensureFreshObservations } from './observations.js';
13
- import { getMiniSkillStore, isMiniSkillStoreInitialized } from '../store/mini-skill-store.js';
14
- import { miniSkillToDocument } from '../skills/mini-skills.js';
15
- import { insert, remove } from '@orama/orama';
16
-
17
- // ── Mini-skill index state ──────────────────────────────────────
18
-
19
- let lastMiniSkillGeneration = -1;
20
-
21
- /**
22
- * Check if mini-skills have changed since our last index sync.
23
- * If stale, reindex all mini-skills in Orama.
24
- *
25
- * Returns true if the index was refreshed.
26
- */
27
- export async function ensureFreshMiniSkills(): Promise<boolean> {
28
- // Skip silently when the store has not been initialized yet —
29
- // observation-only paths never call initMiniSkillStore().
30
- if (!isMiniSkillStoreInitialized()) return false;
31
-
32
- try {
33
- const store = getMiniSkillStore();
34
- const wasStale = await store.ensureFresh();
35
- const currentGen = store.getGeneration();
36
-
37
- if (wasStale || currentGen !== lastMiniSkillGeneration) {
38
- await reindexMiniSkills();
39
- lastMiniSkillGeneration = currentGen;
40
- return true;
41
- }
42
- } catch (err) {
43
- // Best-effort — don't crash the read path, but log for diagnostics
44
- console.warn(`[memorix] ensureFreshMiniSkills failed: ${err instanceof Error ? err.message : err}`);
45
- }
46
- return false;
47
- }
48
-
49
- // Deterministic tracking of indexed mini-skill doc IDs — avoids empty-term search
50
- const indexedSkillDocIds = new Set<string>();
51
-
52
- /**
53
- * Reindex all mini-skills into the Orama database.
54
- * Uses deterministic doc ID tracking instead of empty-term search to ensure
55
- * all stale documents are removed reliably.
56
- */
57
- export async function reindexMiniSkills(): Promise<number> {
58
- // Lazy import to avoid circular dependency
59
- const { getDb } = await import('../store/orama-store.js');
60
- const database = await getDb();
61
-
62
- // Remove previously tracked mini-skill documents by their known IDs
63
- for (const docId of indexedSkillDocIds) {
64
- try { await remove(database, docId); } catch { /* already removed or never existed */ }
65
- }
66
- indexedSkillDocIds.clear();
67
-
68
- // Load and index all current mini-skills
69
- const store = getMiniSkillStore();
70
- const skills = await store.loadAll();
71
- let indexed = 0;
72
-
73
- for (const skill of skills) {
74
- try {
75
- const doc = miniSkillToDocument(skill);
76
- await insert(database, doc);
77
- indexedSkillDocIds.add(doc.id);
78
- indexed++;
79
- } catch (err) {
80
- console.error(`[memorix] Failed to index mini-skill ${skill.id}: ${err instanceof Error ? err.message : err}`);
81
- }
82
- }
83
-
84
- return indexed;
85
- }
86
-
87
- // ── Unified gate ────────────────────────────────────────────────
88
-
89
- /**
90
- * Ensure both observations and mini-skills are fresh in the Orama index.
91
- * Returns true if any data source was refreshed.
92
- */
93
- export async function ensureFreshIndex(): Promise<boolean> {
94
- let anyStale = false;
95
- const obsStale = await ensureFreshObservations();
96
- if (obsStale) anyStale = true;
97
- const skillsStale = await ensureFreshMiniSkills();
98
- if (skillsStale) anyStale = true;
99
- return anyStale;
100
- }
101
-
102
- /**
103
- * Centralized freshness gate — wraps a read-facing function with
104
- * ensureFreshIndex() so callers cannot forget the freshness check.
105
- *
106
- * Usage:
107
- * return withFreshIndex(async () => { ... read from Orama ... });
108
- *
109
- * Phase 3a: replaces withFreshObservations() at all retrieval call sites.
110
- */
111
- export async function withFreshIndex<T>(fn: () => T | Promise<T>): Promise<T> {
112
- await ensureFreshIndex();
113
- return fn();
114
- }
115
-
116
- /**
117
- * Reset mini-skill freshness tracking. Used in tests.
118
- */
119
- export function resetMiniSkillFreshness(): void {
120
- lastMiniSkillGeneration = -1;
121
- indexedSkillDocIds.clear();
122
- }
1
+ /**
2
+ * Unified Freshness Gate (Phase 3a)
3
+ *
4
+ * Replaces withFreshObservations() as the public API for all retrieval
5
+ * surfaces. Checks both observation and mini-skill generation counters
6
+ * to ensure the Orama index is fully up-to-date before any read.
7
+ *
8
+ * withFreshObservations() remains in observations.ts as @internal —
9
+ * only called by this module and legacy test code.
10
+ */
11
+
12
+ import { ensureFreshObservations } from './observations.js';
13
+ import { getMiniSkillStore, isMiniSkillStoreInitialized } from '../store/mini-skill-store.js';
14
+ import { miniSkillToDocument } from '../skills/mini-skills.js';
15
+ import { insert, remove } from '@orama/orama';
16
+
17
+ // ── Mini-skill index state ──────────────────────────────────────
18
+
19
+ let lastMiniSkillGeneration = -1;
20
+
21
+ /**
22
+ * Check if mini-skills have changed since our last index sync.
23
+ * If stale, reindex all mini-skills in Orama.
24
+ *
25
+ * Returns true if the index was refreshed.
26
+ */
27
+ export async function ensureFreshMiniSkills(): Promise<boolean> {
28
+ // Skip silently when the store has not been initialized yet —
29
+ // observation-only paths never call initMiniSkillStore().
30
+ if (!isMiniSkillStoreInitialized()) return false;
31
+
32
+ try {
33
+ const store = getMiniSkillStore();
34
+ const wasStale = await store.ensureFresh();
35
+ const currentGen = store.getGeneration();
36
+
37
+ if (wasStale || currentGen !== lastMiniSkillGeneration) {
38
+ await reindexMiniSkills();
39
+ lastMiniSkillGeneration = currentGen;
40
+ return true;
41
+ }
42
+ } catch (err) {
43
+ // Best-effort — don't crash the read path, but log for diagnostics
44
+ console.warn(`[memorix] ensureFreshMiniSkills failed: ${err instanceof Error ? err.message : err}`);
45
+ }
46
+ return false;
47
+ }
48
+
49
+ // Deterministic tracking of indexed mini-skill doc IDs — avoids empty-term search
50
+ const indexedSkillDocIds = new Set<string>();
51
+
52
+ /**
53
+ * Reindex all mini-skills into the Orama database.
54
+ * Uses deterministic doc ID tracking instead of empty-term search to ensure
55
+ * all stale documents are removed reliably.
56
+ */
57
+ export async function reindexMiniSkills(): Promise<number> {
58
+ // Lazy import to avoid circular dependency
59
+ const { getDb } = await import('../store/orama-store.js');
60
+ const database = await getDb();
61
+
62
+ // Remove previously tracked mini-skill documents by their known IDs
63
+ for (const docId of indexedSkillDocIds) {
64
+ try { await remove(database, docId); } catch { /* already removed or never existed */ }
65
+ }
66
+ indexedSkillDocIds.clear();
67
+
68
+ // Load and index all current mini-skills
69
+ const store = getMiniSkillStore();
70
+ const skills = await store.loadAll();
71
+ let indexed = 0;
72
+
73
+ for (const skill of skills) {
74
+ try {
75
+ const doc = miniSkillToDocument(skill);
76
+ await insert(database, doc);
77
+ indexedSkillDocIds.add(doc.id);
78
+ indexed++;
79
+ } catch (err) {
80
+ console.error(`[memorix] Failed to index mini-skill ${skill.id}: ${err instanceof Error ? err.message : err}`);
81
+ }
82
+ }
83
+
84
+ return indexed;
85
+ }
86
+
87
+ // ── Unified gate ────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Ensure both observations and mini-skills are fresh in the Orama index.
91
+ * Returns true if any data source was refreshed.
92
+ */
93
+ export async function ensureFreshIndex(): Promise<boolean> {
94
+ let anyStale = false;
95
+ const obsStale = await ensureFreshObservations();
96
+ if (obsStale) anyStale = true;
97
+ const skillsStale = await ensureFreshMiniSkills();
98
+ if (skillsStale) anyStale = true;
99
+ return anyStale;
100
+ }
101
+
102
+ /**
103
+ * Centralized freshness gate — wraps a read-facing function with
104
+ * ensureFreshIndex() so callers cannot forget the freshness check.
105
+ *
106
+ * Usage:
107
+ * return withFreshIndex(async () => { ... read from Orama ... });
108
+ *
109
+ * Phase 3a: replaces withFreshObservations() at all retrieval call sites.
110
+ */
111
+ export async function withFreshIndex<T>(fn: () => T | Promise<T>): Promise<T> {
112
+ await ensureFreshIndex();
113
+ return fn();
114
+ }
115
+
116
+ /**
117
+ * Reset mini-skill freshness tracking. Used in tests.
118
+ */
119
+ export function resetMiniSkillFreshness(): void {
120
+ lastMiniSkillGeneration = -1;
121
+ indexedSkillDocIds.clear();
122
+ }
@@ -1,197 +1,197 @@
1
- /**
2
- * Knowledge Graph Manager
3
- *
4
- * Manages the Entity-Relation knowledge graph.
5
- * Source: MCP Official Memory Server v0.6.3 (complete rewrite with same API).
6
- *
7
- * Key differences from official implementation:
8
- * - Uses per-project JSONL files (official uses single file)
9
- * - Async initialization with persistence layer
10
- * - Project-scoped operations
11
- */
12
-
13
- import type { Entity, Relation, KnowledgeGraph } from '../types.js';
14
- import { initGraphStore, getGraphStore } from '../store/graph-store.js';
15
-
16
- export class KnowledgeGraphManager {
17
- private entities: Entity[] = [];
18
- private relations: Relation[] = [];
19
- private projectDir: string;
20
- private initialized = false;
21
- /** Index: lowercase entity name → Entity for O(1) lookups */
22
- private entityIndex = new Map<string, Entity>();
23
-
24
- constructor(projectDir: string) {
25
- this.projectDir = projectDir;
26
- }
27
-
28
- /** Rebuild the entity name index */
29
- private rebuildIndex(): void {
30
- this.entityIndex.clear();
31
- for (const e of this.entities) {
32
- this.entityIndex.set(e.name.toLowerCase(), e);
33
- }
34
- }
35
-
36
- /** Load graph from SQLite on first access */
37
- async init(): Promise<void> {
38
- if (this.initialized) return;
39
- await initGraphStore(this.projectDir);
40
- const store = getGraphStore();
41
- this.entities = store.loadEntities();
42
- this.relations = store.loadRelations();
43
- this.rebuildIndex();
44
- this.initialized = true;
45
- }
46
-
47
- /** Find entity by name (case-insensitive, O(1)) */
48
- findEntityByName(name: string): Entity | undefined {
49
- return this.entityIndex.get(name.toLowerCase());
50
- }
51
-
52
- /** Get all entity names as a Set (lowercase, for fast membership checks) */
53
- getEntityNameSet(): Set<string> {
54
- return new Set(this.entityIndex.keys());
55
- }
56
-
57
- /** Persist current state to SQLite */
58
- private async save(): Promise<void> {
59
- const store = getGraphStore();
60
- store.replaceAll(this.entities, this.relations);
61
- }
62
-
63
- /** Create new entities (skip duplicates by name) */
64
- async createEntities(entities: Entity[]): Promise<Entity[]> {
65
- await this.init();
66
- const newEntities = entities.filter(
67
- (e) => !this.entityIndex.has(e.name.toLowerCase()),
68
- );
69
- this.entities.push(...newEntities);
70
- if (newEntities.length > 0) this.rebuildIndex();
71
- await this.save();
72
- return newEntities;
73
- }
74
-
75
- /** Create new relations (skip duplicates) */
76
- async createRelations(relations: Relation[]): Promise<Relation[]> {
77
- await this.init();
78
- const newRelations = relations.filter(
79
- (r) =>
80
- !this.relations.some(
81
- (existing) =>
82
- existing.from === r.from &&
83
- existing.to === r.to &&
84
- existing.relationType === r.relationType,
85
- ),
86
- );
87
- this.relations.push(...newRelations);
88
- await this.save();
89
- return newRelations;
90
- }
91
-
92
- /** Add observations to existing entities */
93
- async addObservations(
94
- observations: { entityName: string; contents: string[] }[],
95
- ): Promise<{ entityName: string; addedObservations: string[] }[]> {
96
- await this.init();
97
- const results = observations.map((o) => {
98
- const entity = this.entities.find((e) => e.name === o.entityName);
99
- if (!entity) {
100
- throw new Error(`Entity with name ${o.entityName} not found`);
101
- }
102
- const newObs = o.contents.filter((c) => !entity.observations.includes(c));
103
- entity.observations.push(...newObs);
104
- return { entityName: o.entityName, addedObservations: newObs };
105
- });
106
- await this.save();
107
- return results;
108
- }
109
-
110
- /** Delete entities and their associated relations */
111
- async deleteEntities(entityNames: string[]): Promise<void> {
112
- await this.init();
113
- this.entities = this.entities.filter((e) => !entityNames.includes(e.name));
114
- this.relations = this.relations.filter(
115
- (r) => !entityNames.includes(r.from) && !entityNames.includes(r.to),
116
- );
117
- this.rebuildIndex();
118
- await this.save();
119
- }
120
-
121
- /** Delete specific observations from entities */
122
- async deleteObservations(
123
- deletions: { entityName: string; observations: string[] }[],
124
- ): Promise<void> {
125
- await this.init();
126
- for (const d of deletions) {
127
- const entity = this.entities.find((e) => e.name === d.entityName);
128
- if (entity) {
129
- entity.observations = entity.observations.filter(
130
- (o) => !d.observations.includes(o),
131
- );
132
- }
133
- }
134
- await this.save();
135
- }
136
-
137
- /** Delete specific relations */
138
- async deleteRelations(relations: Relation[]): Promise<void> {
139
- await this.init();
140
- this.relations = this.relations.filter(
141
- (r) =>
142
- !relations.some(
143
- (del) =>
144
- r.from === del.from &&
145
- r.to === del.to &&
146
- r.relationType === del.relationType,
147
- ),
148
- );
149
- await this.save();
150
- }
151
-
152
- /** Get all entity names (for Formation Pipeline entity resolution) */
153
- getEntityNames(): string[] {
154
- return this.entities.map(e => e.name);
155
- }
156
-
157
- /** Read the entire graph */
158
- async readGraph(): Promise<KnowledgeGraph> {
159
- await this.init();
160
- return { entities: this.entities, relations: this.relations };
161
- }
162
-
163
- /** Search nodes by query string (upgraded from official's basic includes) */
164
- async searchNodes(query: string): Promise<KnowledgeGraph> {
165
- await this.init();
166
- const lowerQuery = query.toLowerCase();
167
-
168
- const filteredEntities = this.entities.filter(
169
- (e) =>
170
- e.name.toLowerCase().includes(lowerQuery) ||
171
- e.entityType.toLowerCase().includes(lowerQuery) ||
172
- e.observations.some((o) => o.toLowerCase().includes(lowerQuery)),
173
- );
174
-
175
- const filteredNames = new Set(filteredEntities.map((e) => e.name));
176
-
177
- const filteredRelations = this.relations.filter(
178
- (r) => filteredNames.has(r.from) && filteredNames.has(r.to),
179
- );
180
-
181
- return { entities: filteredEntities, relations: filteredRelations };
182
- }
183
-
184
- /** Open specific nodes by name */
185
- async openNodes(names: string[]): Promise<KnowledgeGraph> {
186
- await this.init();
187
-
188
- const filteredEntities = this.entities.filter((e) => names.includes(e.name));
189
- const filteredNames = new Set(filteredEntities.map((e) => e.name));
190
-
191
- const filteredRelations = this.relations.filter(
192
- (r) => filteredNames.has(r.from) && filteredNames.has(r.to),
193
- );
194
-
195
- return { entities: filteredEntities, relations: filteredRelations };
196
- }
197
- }
1
+ /**
2
+ * Knowledge Graph Manager
3
+ *
4
+ * Manages the Entity-Relation knowledge graph.
5
+ * Source: MCP Official Memory Server v0.6.3 (complete rewrite with same API).
6
+ *
7
+ * Key differences from official implementation:
8
+ * - Uses per-project JSONL files (official uses single file)
9
+ * - Async initialization with persistence layer
10
+ * - Project-scoped operations
11
+ */
12
+
13
+ import type { Entity, Relation, KnowledgeGraph } from '../types.js';
14
+ import { initGraphStore, getGraphStore } from '../store/graph-store.js';
15
+
16
+ export class KnowledgeGraphManager {
17
+ private entities: Entity[] = [];
18
+ private relations: Relation[] = [];
19
+ private projectDir: string;
20
+ private initialized = false;
21
+ /** Index: lowercase entity name → Entity for O(1) lookups */
22
+ private entityIndex = new Map<string, Entity>();
23
+
24
+ constructor(projectDir: string) {
25
+ this.projectDir = projectDir;
26
+ }
27
+
28
+ /** Rebuild the entity name index */
29
+ private rebuildIndex(): void {
30
+ this.entityIndex.clear();
31
+ for (const e of this.entities) {
32
+ this.entityIndex.set(e.name.toLowerCase(), e);
33
+ }
34
+ }
35
+
36
+ /** Load graph from SQLite on first access */
37
+ async init(): Promise<void> {
38
+ if (this.initialized) return;
39
+ await initGraphStore(this.projectDir);
40
+ const store = getGraphStore();
41
+ this.entities = store.loadEntities();
42
+ this.relations = store.loadRelations();
43
+ this.rebuildIndex();
44
+ this.initialized = true;
45
+ }
46
+
47
+ /** Find entity by name (case-insensitive, O(1)) */
48
+ findEntityByName(name: string): Entity | undefined {
49
+ return this.entityIndex.get(name.toLowerCase());
50
+ }
51
+
52
+ /** Get all entity names as a Set (lowercase, for fast membership checks) */
53
+ getEntityNameSet(): Set<string> {
54
+ return new Set(this.entityIndex.keys());
55
+ }
56
+
57
+ /** Persist current state to SQLite */
58
+ private async save(): Promise<void> {
59
+ const store = getGraphStore();
60
+ store.replaceAll(this.entities, this.relations);
61
+ }
62
+
63
+ /** Create new entities (skip duplicates by name) */
64
+ async createEntities(entities: Entity[]): Promise<Entity[]> {
65
+ await this.init();
66
+ const newEntities = entities.filter(
67
+ (e) => !this.entityIndex.has(e.name.toLowerCase()),
68
+ );
69
+ this.entities.push(...newEntities);
70
+ if (newEntities.length > 0) this.rebuildIndex();
71
+ await this.save();
72
+ return newEntities;
73
+ }
74
+
75
+ /** Create new relations (skip duplicates) */
76
+ async createRelations(relations: Relation[]): Promise<Relation[]> {
77
+ await this.init();
78
+ const newRelations = relations.filter(
79
+ (r) =>
80
+ !this.relations.some(
81
+ (existing) =>
82
+ existing.from === r.from &&
83
+ existing.to === r.to &&
84
+ existing.relationType === r.relationType,
85
+ ),
86
+ );
87
+ this.relations.push(...newRelations);
88
+ await this.save();
89
+ return newRelations;
90
+ }
91
+
92
+ /** Add observations to existing entities */
93
+ async addObservations(
94
+ observations: { entityName: string; contents: string[] }[],
95
+ ): Promise<{ entityName: string; addedObservations: string[] }[]> {
96
+ await this.init();
97
+ const results = observations.map((o) => {
98
+ const entity = this.entities.find((e) => e.name === o.entityName);
99
+ if (!entity) {
100
+ throw new Error(`Entity with name ${o.entityName} not found`);
101
+ }
102
+ const newObs = o.contents.filter((c) => !entity.observations.includes(c));
103
+ entity.observations.push(...newObs);
104
+ return { entityName: o.entityName, addedObservations: newObs };
105
+ });
106
+ await this.save();
107
+ return results;
108
+ }
109
+
110
+ /** Delete entities and their associated relations */
111
+ async deleteEntities(entityNames: string[]): Promise<void> {
112
+ await this.init();
113
+ this.entities = this.entities.filter((e) => !entityNames.includes(e.name));
114
+ this.relations = this.relations.filter(
115
+ (r) => !entityNames.includes(r.from) && !entityNames.includes(r.to),
116
+ );
117
+ this.rebuildIndex();
118
+ await this.save();
119
+ }
120
+
121
+ /** Delete specific observations from entities */
122
+ async deleteObservations(
123
+ deletions: { entityName: string; observations: string[] }[],
124
+ ): Promise<void> {
125
+ await this.init();
126
+ for (const d of deletions) {
127
+ const entity = this.entities.find((e) => e.name === d.entityName);
128
+ if (entity) {
129
+ entity.observations = entity.observations.filter(
130
+ (o) => !d.observations.includes(o),
131
+ );
132
+ }
133
+ }
134
+ await this.save();
135
+ }
136
+
137
+ /** Delete specific relations */
138
+ async deleteRelations(relations: Relation[]): Promise<void> {
139
+ await this.init();
140
+ this.relations = this.relations.filter(
141
+ (r) =>
142
+ !relations.some(
143
+ (del) =>
144
+ r.from === del.from &&
145
+ r.to === del.to &&
146
+ r.relationType === del.relationType,
147
+ ),
148
+ );
149
+ await this.save();
150
+ }
151
+
152
+ /** Get all entity names (for Formation Pipeline entity resolution) */
153
+ getEntityNames(): string[] {
154
+ return this.entities.map(e => e.name);
155
+ }
156
+
157
+ /** Read the entire graph */
158
+ async readGraph(): Promise<KnowledgeGraph> {
159
+ await this.init();
160
+ return { entities: this.entities, relations: this.relations };
161
+ }
162
+
163
+ /** Search nodes by query string (upgraded from official's basic includes) */
164
+ async searchNodes(query: string): Promise<KnowledgeGraph> {
165
+ await this.init();
166
+ const lowerQuery = query.toLowerCase();
167
+
168
+ const filteredEntities = this.entities.filter(
169
+ (e) =>
170
+ e.name.toLowerCase().includes(lowerQuery) ||
171
+ e.entityType.toLowerCase().includes(lowerQuery) ||
172
+ e.observations.some((o) => o.toLowerCase().includes(lowerQuery)),
173
+ );
174
+
175
+ const filteredNames = new Set(filteredEntities.map((e) => e.name));
176
+
177
+ const filteredRelations = this.relations.filter(
178
+ (r) => filteredNames.has(r.from) && filteredNames.has(r.to),
179
+ );
180
+
181
+ return { entities: filteredEntities, relations: filteredRelations };
182
+ }
183
+
184
+ /** Open specific nodes by name */
185
+ async openNodes(names: string[]): Promise<KnowledgeGraph> {
186
+ await this.init();
187
+
188
+ const filteredEntities = this.entities.filter((e) => names.includes(e.name));
189
+ const filteredNames = new Set(filteredEntities.map((e) => e.name));
190
+
191
+ const filteredRelations = this.relations.filter(
192
+ (r) => filteredNames.has(r.from) && filteredNames.has(r.to),
193
+ );
194
+
195
+ return { entities: filteredEntities, relations: filteredRelations };
196
+ }
197
+ }