claude-brain 0.3.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.
Files changed (200) hide show
  1. package/README.md +157 -0
  2. package/VERSION +1 -0
  3. package/assets/CLAUDE.md +307 -0
  4. package/bunfig.toml +8 -0
  5. package/package.json +74 -0
  6. package/src/automation/auto-context.ts +240 -0
  7. package/src/automation/decision-detector.ts +452 -0
  8. package/src/automation/index.ts +11 -0
  9. package/src/automation/proactive-recall.ts +373 -0
  10. package/src/automation/project-detector.ts +297 -0
  11. package/src/cli/auto-setup.ts +74 -0
  12. package/src/cli/bin.ts +110 -0
  13. package/src/cli/commands/install-mcp.ts +50 -0
  14. package/src/cli/commands/serve.ts +129 -0
  15. package/src/cli/diagnose.ts +4 -0
  16. package/src/cli/health-check.ts +4 -0
  17. package/src/cli/migrate-chroma.ts +106 -0
  18. package/src/cli/setup.ts +4 -0
  19. package/src/config/defaults.ts +47 -0
  20. package/src/config/home.ts +55 -0
  21. package/src/config/index.ts +7 -0
  22. package/src/config/loader.ts +166 -0
  23. package/src/config/migration.ts +76 -0
  24. package/src/config/schema.ts +257 -0
  25. package/src/config/validator.ts +184 -0
  26. package/src/config/watcher.ts +86 -0
  27. package/src/context/assembler.ts +398 -0
  28. package/src/context/cache-manager.ts +101 -0
  29. package/src/context/formatter.ts +84 -0
  30. package/src/context/hierarchy.ts +85 -0
  31. package/src/context/index.ts +83 -0
  32. package/src/context/progress-tracker.ts +174 -0
  33. package/src/context/standards-manager.ts +267 -0
  34. package/src/context/types.ts +252 -0
  35. package/src/context/validator.ts +58 -0
  36. package/src/cross-project/affinity.ts +162 -0
  37. package/src/cross-project/generalizer.ts +283 -0
  38. package/src/cross-project/index.ts +13 -0
  39. package/src/cross-project/transfer.ts +201 -0
  40. package/src/diagnostics/index.ts +123 -0
  41. package/src/health/index.ts +229 -0
  42. package/src/index.ts +7 -0
  43. package/src/knowledge/entity-extractor.ts +416 -0
  44. package/src/knowledge/graph/builder.ts +159 -0
  45. package/src/knowledge/graph/linker.ts +201 -0
  46. package/src/knowledge/graph/memory-graph.ts +359 -0
  47. package/src/knowledge/graph/schema.ts +99 -0
  48. package/src/knowledge/graph/search.ts +168 -0
  49. package/src/knowledge/relationship-extractor.ts +108 -0
  50. package/src/memory/chroma/client.ts +169 -0
  51. package/src/memory/chroma/collection-manager.ts +94 -0
  52. package/src/memory/chroma/config.ts +46 -0
  53. package/src/memory/chroma/embeddings.ts +153 -0
  54. package/src/memory/chroma/index.ts +82 -0
  55. package/src/memory/chroma/migration.ts +270 -0
  56. package/src/memory/chroma/schemas.ts +69 -0
  57. package/src/memory/chroma/search.ts +315 -0
  58. package/src/memory/chroma/store.ts +694 -0
  59. package/src/memory/consolidation/archiver.ts +164 -0
  60. package/src/memory/consolidation/merger.ts +186 -0
  61. package/src/memory/consolidation/scorer.ts +138 -0
  62. package/src/memory/context-builder.ts +236 -0
  63. package/src/memory/database.ts +169 -0
  64. package/src/memory/embedding-utils.ts +156 -0
  65. package/src/memory/embeddings.ts +226 -0
  66. package/src/memory/episodic/detector.ts +108 -0
  67. package/src/memory/episodic/manager.ts +334 -0
  68. package/src/memory/episodic/summarizer.ts +179 -0
  69. package/src/memory/episodic/types.ts +52 -0
  70. package/src/memory/index.ts +395 -0
  71. package/src/memory/knowledge-extractor.ts +455 -0
  72. package/src/memory/learning.ts +378 -0
  73. package/src/memory/patterns.ts +396 -0
  74. package/src/memory/schema.ts +56 -0
  75. package/src/memory/search.ts +309 -0
  76. package/src/memory/store.ts +344 -0
  77. package/src/memory/types.ts +121 -0
  78. package/src/optimization/index.ts +10 -0
  79. package/src/optimization/precompute.ts +202 -0
  80. package/src/optimization/semantic-cache.ts +207 -0
  81. package/src/orchestrator/coordinator.ts +272 -0
  82. package/src/orchestrator/decision-logger.ts +228 -0
  83. package/src/orchestrator/event-emitter.ts +198 -0
  84. package/src/orchestrator/event-queue.ts +184 -0
  85. package/src/orchestrator/handlers/base-handler.ts +70 -0
  86. package/src/orchestrator/handlers/context-handler.ts +73 -0
  87. package/src/orchestrator/handlers/decision-handler.ts +204 -0
  88. package/src/orchestrator/handlers/index.ts +10 -0
  89. package/src/orchestrator/handlers/status-handler.ts +131 -0
  90. package/src/orchestrator/handlers/task-handler.ts +171 -0
  91. package/src/orchestrator/index.ts +275 -0
  92. package/src/orchestrator/task-parser.ts +284 -0
  93. package/src/orchestrator/types.ts +98 -0
  94. package/src/phase12/index.ts +456 -0
  95. package/src/prediction/context-anticipator.ts +198 -0
  96. package/src/prediction/decision-predictor.ts +184 -0
  97. package/src/prediction/index.ts +13 -0
  98. package/src/prediction/recommender.ts +268 -0
  99. package/src/reasoning/chain-retrieval.ts +247 -0
  100. package/src/reasoning/counterfactual.ts +248 -0
  101. package/src/reasoning/index.ts +13 -0
  102. package/src/reasoning/synthesizer.ts +169 -0
  103. package/src/retrieval/bm25/index.ts +300 -0
  104. package/src/retrieval/bm25/tokenizer.ts +184 -0
  105. package/src/retrieval/feedback/adaptive.ts +223 -0
  106. package/src/retrieval/feedback/index.ts +16 -0
  107. package/src/retrieval/feedback/metrics.ts +223 -0
  108. package/src/retrieval/feedback/store.ts +283 -0
  109. package/src/retrieval/fusion/index.ts +194 -0
  110. package/src/retrieval/fusion/rrf.ts +163 -0
  111. package/src/retrieval/index.ts +12 -0
  112. package/src/retrieval/pipeline.ts +375 -0
  113. package/src/retrieval/query/expander.ts +198 -0
  114. package/src/retrieval/query/index.ts +27 -0
  115. package/src/retrieval/query/intent-classifier.ts +236 -0
  116. package/src/retrieval/query/temporal-parser.ts +295 -0
  117. package/src/retrieval/reranker/index.ts +188 -0
  118. package/src/retrieval/reranker/model.ts +95 -0
  119. package/src/retrieval/service.ts +125 -0
  120. package/src/retrieval/types.ts +162 -0
  121. package/src/scripts/health-check.ts +118 -0
  122. package/src/scripts/setup.ts +122 -0
  123. package/src/server/handlers/call-tool.ts +194 -0
  124. package/src/server/handlers/index.ts +9 -0
  125. package/src/server/handlers/list-tools.ts +18 -0
  126. package/src/server/handlers/tools/analyze-decision-evolution.ts +71 -0
  127. package/src/server/handlers/tools/auto-remember.ts +200 -0
  128. package/src/server/handlers/tools/create-project.ts +135 -0
  129. package/src/server/handlers/tools/detect-trends.ts +80 -0
  130. package/src/server/handlers/tools/find-cross-project-patterns.ts +73 -0
  131. package/src/server/handlers/tools/get-activity-log.ts +194 -0
  132. package/src/server/handlers/tools/get-code-standards.ts +124 -0
  133. package/src/server/handlers/tools/get-corrections.ts +154 -0
  134. package/src/server/handlers/tools/get-decision-timeline.ts +86 -0
  135. package/src/server/handlers/tools/get-episode.ts +93 -0
  136. package/src/server/handlers/tools/get-patterns.ts +158 -0
  137. package/src/server/handlers/tools/get-phase12-status.ts +63 -0
  138. package/src/server/handlers/tools/get-project-context.ts +75 -0
  139. package/src/server/handlers/tools/get-recommendations.ts +65 -0
  140. package/src/server/handlers/tools/index.ts +33 -0
  141. package/src/server/handlers/tools/init-project.ts +710 -0
  142. package/src/server/handlers/tools/list-episodes.ts +80 -0
  143. package/src/server/handlers/tools/list-projects.ts +125 -0
  144. package/src/server/handlers/tools/rate-memory.ts +95 -0
  145. package/src/server/handlers/tools/recall-similar.ts +87 -0
  146. package/src/server/handlers/tools/recognize-pattern.ts +126 -0
  147. package/src/server/handlers/tools/record-correction.ts +125 -0
  148. package/src/server/handlers/tools/remember-decision.ts +153 -0
  149. package/src/server/handlers/tools/schemas.ts +241 -0
  150. package/src/server/handlers/tools/search-knowledge-graph.ts +89 -0
  151. package/src/server/handlers/tools/smart-context.ts +124 -0
  152. package/src/server/handlers/tools/update-progress.ts +114 -0
  153. package/src/server/handlers/tools/what-if-analysis.ts +73 -0
  154. package/src/server/http-api.ts +474 -0
  155. package/src/server/index.ts +40 -0
  156. package/src/server/mcp-server.ts +283 -0
  157. package/src/server/providers/index.ts +7 -0
  158. package/src/server/providers/prompts.ts +327 -0
  159. package/src/server/providers/resources.ts +427 -0
  160. package/src/server/services.ts +388 -0
  161. package/src/server/types.ts +39 -0
  162. package/src/server/utils/error-handler.ts +155 -0
  163. package/src/server/utils/index.ts +13 -0
  164. package/src/server/utils/memory-indicator.ts +83 -0
  165. package/src/server/utils/request-context.ts +122 -0
  166. package/src/server/utils/response-formatter.ts +124 -0
  167. package/src/server/utils/validators.ts +210 -0
  168. package/src/setup/index.ts +22 -0
  169. package/src/setup/wizard.ts +321 -0
  170. package/src/temporal/evolution.ts +197 -0
  171. package/src/temporal/index.ts +16 -0
  172. package/src/temporal/query-processor.ts +190 -0
  173. package/src/temporal/timeline.ts +259 -0
  174. package/src/temporal/trends.ts +263 -0
  175. package/src/tools/index.ts +24 -0
  176. package/src/tools/registry.ts +106 -0
  177. package/src/tools/schemas.test.ts +30 -0
  178. package/src/tools/schemas.ts +907 -0
  179. package/src/tools/types.ts +412 -0
  180. package/src/utils/circuit-breaker.ts +130 -0
  181. package/src/utils/cleanup.ts +34 -0
  182. package/src/utils/error-handler.ts +132 -0
  183. package/src/utils/error-messages.ts +60 -0
  184. package/src/utils/fallback.ts +45 -0
  185. package/src/utils/index.ts +54 -0
  186. package/src/utils/logger-utils.ts +80 -0
  187. package/src/utils/logger.ts +88 -0
  188. package/src/utils/phase12-helper.ts +56 -0
  189. package/src/utils/retry.ts +94 -0
  190. package/src/utils/transaction.ts +63 -0
  191. package/src/vault/frontmatter.ts +264 -0
  192. package/src/vault/index.ts +318 -0
  193. package/src/vault/paths.ts +106 -0
  194. package/src/vault/query.ts +422 -0
  195. package/src/vault/reader.ts +264 -0
  196. package/src/vault/templates.ts +186 -0
  197. package/src/vault/types.ts +73 -0
  198. package/src/vault/watcher.ts +277 -0
  199. package/src/vault/writer.ts +393 -0
  200. package/tsconfig.json +30 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Knowledge Graph Builder
3
+ * Auto-populates the graph when decisions/patterns/corrections are stored
4
+ */
5
+
6
+ import type { Logger } from 'pino'
7
+ import type { InMemoryKnowledgeGraph } from './memory-graph'
8
+ import { EntityExtractor, type ExtractedEntity } from '../entity-extractor'
9
+ import { RelationshipExtractor } from '../relationship-extractor'
10
+ import type { StoreDecisionInput, StoredDecision } from '../../memory/chroma/store'
11
+ import type { CollectionManager } from '../../memory/chroma/collection-manager'
12
+
13
+ export class KnowledgeGraphBuilder {
14
+ private graph: InMemoryKnowledgeGraph
15
+ private entityExtractor: EntityExtractor
16
+ private relationshipExtractor: RelationshipExtractor
17
+ private logger: Logger
18
+
19
+ constructor(graph: InMemoryKnowledgeGraph, logger: Logger) {
20
+ this.graph = graph
21
+ this.entityExtractor = new EntityExtractor()
22
+ this.relationshipExtractor = new RelationshipExtractor()
23
+ this.logger = logger.child({ component: 'graph-builder' })
24
+ }
25
+
26
+ async initialize(): Promise<void> {
27
+ await this.entityExtractor.initialize()
28
+ }
29
+
30
+ processDecision(input: StoreDecisionInput & { id: string }): void {
31
+ try {
32
+ const fullText = `${input.decision} ${input.context} ${input.reasoning}`
33
+ const entities = this.entityExtractor.extract(fullText)
34
+ const relationships = this.relationshipExtractor.extract(fullText, entities)
35
+
36
+ // Create decision node
37
+ const decisionNode = this.graph.addNode({
38
+ name: input.decision.slice(0, 100),
39
+ type: 'decision',
40
+ properties: {
41
+ decision_id: input.id,
42
+ project: input.project,
43
+ context: input.context,
44
+ reasoning: input.reasoning,
45
+ full_decision: input.decision
46
+ }
47
+ })
48
+
49
+ // Create project node if it doesn't exist
50
+ let projectNode = this.graph.findNodeByName(input.project, 'project')
51
+ if (!projectNode) {
52
+ projectNode = this.graph.addNode({
53
+ name: input.project,
54
+ type: 'project',
55
+ properties: {}
56
+ })
57
+ }
58
+
59
+ // Link decision to project
60
+ this.graph.addEdge({
61
+ source: decisionNode.id,
62
+ target: projectNode.id,
63
+ relationship: 'decided_in',
64
+ weight: 1.0,
65
+ properties: {}
66
+ })
67
+
68
+ // Create entity nodes and link to decision
69
+ const entityNodeMap = new Map<string, string>() // normalizedName → nodeId
70
+ for (const entity of entities) {
71
+ let entityNode = this.graph.findNodeByName(entity.normalizedName, entity.type)
72
+ if (!entityNode) {
73
+ entityNode = this.graph.addNode({
74
+ name: entity.normalizedName,
75
+ type: entity.type,
76
+ properties: {
77
+ confidence: entity.confidence,
78
+ source: entity.source
79
+ }
80
+ })
81
+ }
82
+ entityNodeMap.set(entity.normalizedName, entityNode.id)
83
+
84
+ // Link decision to entity
85
+ this.graph.addEdge({
86
+ source: decisionNode.id,
87
+ target: entityNode.id,
88
+ relationship: 'relates_to',
89
+ weight: entity.confidence,
90
+ properties: {}
91
+ })
92
+ }
93
+
94
+ // Create relationship edges between entities
95
+ for (const rel of relationships) {
96
+ const sourceId = entityNodeMap.get(rel.sourceEntity)
97
+ const targetId = entityNodeMap.get(rel.targetEntity)
98
+
99
+ if (sourceId && targetId) {
100
+ this.graph.addEdge({
101
+ source: sourceId,
102
+ target: targetId,
103
+ relationship: rel.relationship,
104
+ weight: rel.confidence,
105
+ properties: { evidence: rel.evidence }
106
+ })
107
+ }
108
+ }
109
+
110
+ this.logger.debug({
111
+ decisionId: input.id,
112
+ entities: entities.length,
113
+ relationships: relationships.length
114
+ }, 'Decision processed into knowledge graph')
115
+ } catch (error) {
116
+ this.logger.error({ error, decisionId: input.id }, 'Failed to process decision into graph')
117
+ }
118
+ }
119
+
120
+ async migrateExistingDecisions(collections: CollectionManager): Promise<{ processed: number; errors: number }> {
121
+ let processed = 0
122
+ let errors = 0
123
+
124
+ try {
125
+ const collection = await collections.getDecisions()
126
+ const result = await collection.get({
127
+ include: ['documents', 'metadatas']
128
+ })
129
+
130
+ for (let i = 0; i < result.ids.length; i++) {
131
+ try {
132
+ const id = result.ids[i]
133
+ const document = result.documents?.[i] as string
134
+ const metadata = result.metadatas?.[i] as Record<string, any>
135
+
136
+ if (!document || !metadata) continue
137
+
138
+ this.processDecision({
139
+ id,
140
+ project: metadata.project || 'unknown',
141
+ context: metadata.context || '',
142
+ decision: document,
143
+ reasoning: metadata.reasoning || ''
144
+ })
145
+
146
+ processed++
147
+ } catch {
148
+ errors++
149
+ }
150
+ }
151
+
152
+ this.logger.info({ processed, errors }, 'Migration of existing decisions complete')
153
+ } catch (error) {
154
+ this.logger.error({ error }, 'Failed to migrate existing decisions')
155
+ }
156
+
157
+ return { processed, errors }
158
+ }
159
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Cross-Reference Linker
3
+ * Finds implicit links, contradictions, and decision chains
4
+ */
5
+
6
+ import type { Logger } from 'pino'
7
+ import type { InMemoryKnowledgeGraph } from './memory-graph'
8
+ import type { GraphNode, GraphEdge } from './schema'
9
+
10
+ export interface ImplicitLink {
11
+ sourceId: string
12
+ targetId: string
13
+ sourceName: string
14
+ targetName: string
15
+ reason: string
16
+ confidence: number
17
+ }
18
+
19
+ export interface Contradiction {
20
+ decisionA: GraphNode
21
+ decisionB: GraphNode
22
+ reason: string
23
+ confidence: number
24
+ }
25
+
26
+ export interface DecisionChain {
27
+ topic: string
28
+ decisions: GraphNode[]
29
+ }
30
+
31
+ export class CrossReferenceLinker {
32
+ private graph: InMemoryKnowledgeGraph
33
+ private logger: Logger
34
+
35
+ constructor(graph: InMemoryKnowledgeGraph, logger: Logger) {
36
+ this.graph = graph
37
+ this.logger = logger.child({ component: 'cross-reference-linker' })
38
+ }
39
+
40
+ findImplicitLinks(): ImplicitLink[] {
41
+ const links: ImplicitLink[] = []
42
+ const decisionNodes = this.graph.findNodes({ type: 'decision' })
43
+
44
+ for (let i = 0; i < decisionNodes.length; i++) {
45
+ for (let j = i + 1; j < decisionNodes.length; j++) {
46
+ const nodeA = decisionNodes[i]
47
+ const nodeB = decisionNodes[j]
48
+
49
+ // Check if they share entities (connected via common neighbor)
50
+ const sharedEntities = this.findSharedEntities(nodeA.id, nodeB.id)
51
+
52
+ if (sharedEntities.length >= 2) {
53
+ // Already directly connected?
54
+ const existingEdges = this.graph.getEdges(nodeA.id, 'outgoing')
55
+ const alreadyLinked = existingEdges.some(e => e.target === nodeB.id && e.relationship === 'similar_to')
56
+
57
+ if (!alreadyLinked) {
58
+ links.push({
59
+ sourceId: nodeA.id,
60
+ targetId: nodeB.id,
61
+ sourceName: nodeA.name,
62
+ targetName: nodeB.name,
63
+ reason: `Share ${sharedEntities.length} entities: ${sharedEntities.slice(0, 3).join(', ')}`,
64
+ confidence: Math.min(0.9, 0.5 + sharedEntities.length * 0.1)
65
+ })
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ this.logger.debug({ linkCount: links.length }, 'Found implicit links')
72
+ return links
73
+ }
74
+
75
+ findContradictions(): Contradiction[] {
76
+ const contradictions: Contradiction[] = []
77
+ const decisionNodes = this.graph.findNodes({ type: 'decision' })
78
+
79
+ const contradictionKeywords = ['not', 'avoid', 'don\'t', 'never', 'instead', 'rather than', 'rejected']
80
+ const approvalKeywords = ['chose', 'use', 'adopt', 'selected', 'prefer', 'recommend']
81
+
82
+ for (let i = 0; i < decisionNodes.length; i++) {
83
+ for (let j = i + 1; j < decisionNodes.length; j++) {
84
+ const nodeA = decisionNodes[i]
85
+ const nodeB = decisionNodes[j]
86
+
87
+ // Check if same project
88
+ const projectA = nodeA.properties.project as string
89
+ const projectB = nodeB.properties.project as string
90
+ if (projectA !== projectB) continue
91
+
92
+ // Check if they share entities but with opposing sentiment
93
+ const sharedEntities = this.findSharedEntities(nodeA.id, nodeB.id)
94
+ if (sharedEntities.length === 0) continue
95
+
96
+ const textA = (nodeA.properties.full_decision as string || nodeA.name).toLowerCase()
97
+ const textB = (nodeB.properties.full_decision as string || nodeB.name).toLowerCase()
98
+
99
+ // Check for opposing sentiment
100
+ const aNegative = contradictionKeywords.some(k => textA.includes(k))
101
+ const bNegative = contradictionKeywords.some(k => textB.includes(k))
102
+ const aPositive = approvalKeywords.some(k => textA.includes(k))
103
+ const bPositive = approvalKeywords.some(k => textB.includes(k))
104
+
105
+ if ((aNegative && bPositive) || (aPositive && bNegative)) {
106
+ contradictions.push({
107
+ decisionA: nodeA,
108
+ decisionB: nodeB,
109
+ reason: `Opposing sentiment about shared entities: ${sharedEntities.join(', ')}`,
110
+ confidence: 0.7
111
+ })
112
+ }
113
+ }
114
+ }
115
+
116
+ this.logger.debug({ count: contradictions.length }, 'Found potential contradictions')
117
+ return contradictions
118
+ }
119
+
120
+ findDecisionChains(): DecisionChain[] {
121
+ const chains: DecisionChain[] = []
122
+ const decisionNodes = this.graph.findNodes({ type: 'decision' })
123
+
124
+ // Group by shared topics (entity neighbors)
125
+ const topicGroups = new Map<string, GraphNode[]>()
126
+
127
+ for (const decision of decisionNodes) {
128
+ const edges = this.graph.getEdges(decision.id, 'outgoing')
129
+ for (const edge of edges) {
130
+ if (edge.relationship === 'relates_to') {
131
+ const target = this.graph.getNode(edge.target)
132
+ if (target && target.type === 'technology') {
133
+ const topic = target.name
134
+ if (!topicGroups.has(topic)) {
135
+ topicGroups.set(topic, [])
136
+ }
137
+ topicGroups.get(topic)!.push(decision)
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ // Create chains from groups with 2+ decisions
144
+ for (const [topic, decisions] of topicGroups) {
145
+ if (decisions.length >= 2) {
146
+ // Sort by creation time
147
+ const sorted = [...decisions].sort((a, b) =>
148
+ a.created_at.localeCompare(b.created_at)
149
+ )
150
+ chains.push({ topic, decisions: sorted })
151
+ }
152
+ }
153
+
154
+ this.logger.debug({ chainCount: chains.length }, 'Found decision chains')
155
+ return chains
156
+ }
157
+
158
+ applyImplicitLinks(links: ImplicitLink[]): number {
159
+ let applied = 0
160
+ for (const link of links) {
161
+ try {
162
+ this.graph.addEdge({
163
+ source: link.sourceId,
164
+ target: link.targetId,
165
+ relationship: 'similar_to',
166
+ weight: link.confidence,
167
+ properties: { reason: link.reason, auto_linked: true }
168
+ })
169
+ applied++
170
+ } catch {
171
+ // May fail if nodes were deleted
172
+ }
173
+ }
174
+ return applied
175
+ }
176
+
177
+ private findSharedEntities(nodeAId: string, nodeBId: string): string[] {
178
+ const edgesA = this.graph.getEdges(nodeAId, 'outgoing')
179
+ const edgesB = this.graph.getEdges(nodeBId, 'outgoing')
180
+
181
+ const neighborsA = new Set<string>()
182
+ for (const edge of edgesA) {
183
+ if (edge.relationship === 'relates_to') {
184
+ const target = this.graph.getNode(edge.target)
185
+ if (target) neighborsA.add(target.name)
186
+ }
187
+ }
188
+
189
+ const shared: string[] = []
190
+ for (const edge of edgesB) {
191
+ if (edge.relationship === 'relates_to') {
192
+ const target = this.graph.getNode(edge.target)
193
+ if (target && neighborsA.has(target.name)) {
194
+ shared.push(target.name)
195
+ }
196
+ }
197
+ }
198
+
199
+ return shared
200
+ }
201
+ }
@@ -0,0 +1,359 @@
1
+ /**
2
+ * In-Memory Knowledge Graph
3
+ * Graph data structure with traversal, persistence, and search
4
+ */
5
+
6
+ import { randomUUID } from 'crypto'
7
+ import { readFile, writeFile, mkdir } from 'fs/promises'
8
+ import { dirname } from 'path'
9
+ import type {
10
+ GraphNode,
11
+ GraphEdge,
12
+ KnowledgeGraph,
13
+ TraversalOptions,
14
+ EntityType,
15
+ SerializedGraph
16
+ } from './schema'
17
+ import type { Logger } from 'pino'
18
+
19
+ export class InMemoryKnowledgeGraph implements KnowledgeGraph {
20
+ private nodes: Map<string, GraphNode> = new Map()
21
+ private edges: Map<string, GraphEdge> = new Map()
22
+ private outgoing: Map<string, Set<string>> = new Map()
23
+ private incoming: Map<string, Set<string>> = new Map()
24
+ private logger: Logger
25
+ private autoSaveTimer: ReturnType<typeof setInterval> | null = null
26
+ private dirty = false
27
+
28
+ constructor(logger: Logger) {
29
+ this.logger = logger.child({ component: 'knowledge-graph' })
30
+ }
31
+
32
+ addNode(input: Omit<GraphNode, 'id' | 'created_at' | 'updated_at'>): GraphNode {
33
+ const now = new Date().toISOString()
34
+ const node: GraphNode = {
35
+ ...input,
36
+ id: randomUUID(),
37
+ created_at: now,
38
+ updated_at: now
39
+ }
40
+ this.nodes.set(node.id, node)
41
+ this.outgoing.set(node.id, new Set())
42
+ this.incoming.set(node.id, new Set())
43
+ this.dirty = true
44
+ return node
45
+ }
46
+
47
+ getNode(id: string): GraphNode | undefined {
48
+ return this.nodes.get(id)
49
+ }
50
+
51
+ updateNode(id: string, updates: Partial<Omit<GraphNode, 'id' | 'created_at'>>): GraphNode | undefined {
52
+ const node = this.nodes.get(id)
53
+ if (!node) return undefined
54
+
55
+ const updated: GraphNode = {
56
+ ...node,
57
+ ...updates,
58
+ id: node.id,
59
+ created_at: node.created_at,
60
+ updated_at: new Date().toISOString()
61
+ }
62
+ this.nodes.set(id, updated)
63
+ this.dirty = true
64
+ return updated
65
+ }
66
+
67
+ deleteNode(id: string): boolean {
68
+ if (!this.nodes.has(id)) return false
69
+
70
+ // Delete all connected edges
71
+ const outEdges = this.outgoing.get(id) || new Set()
72
+ const inEdges = this.incoming.get(id) || new Set()
73
+
74
+ for (const edgeId of outEdges) {
75
+ const edge = this.edges.get(edgeId)
76
+ if (edge) {
77
+ this.incoming.get(edge.target)?.delete(edgeId)
78
+ this.edges.delete(edgeId)
79
+ }
80
+ }
81
+
82
+ for (const edgeId of inEdges) {
83
+ const edge = this.edges.get(edgeId)
84
+ if (edge) {
85
+ this.outgoing.get(edge.source)?.delete(edgeId)
86
+ this.edges.delete(edgeId)
87
+ }
88
+ }
89
+
90
+ this.outgoing.delete(id)
91
+ this.incoming.delete(id)
92
+ this.nodes.delete(id)
93
+ this.dirty = true
94
+ return true
95
+ }
96
+
97
+ addEdge(input: Omit<GraphEdge, 'id' | 'created_at'>): GraphEdge {
98
+ if (!this.nodes.has(input.source) || !this.nodes.has(input.target)) {
99
+ throw new Error(`Both source (${input.source}) and target (${input.target}) must exist`)
100
+ }
101
+
102
+ const edge: GraphEdge = {
103
+ ...input,
104
+ id: randomUUID(),
105
+ created_at: new Date().toISOString()
106
+ }
107
+
108
+ this.edges.set(edge.id, edge)
109
+
110
+ if (!this.outgoing.has(input.source)) this.outgoing.set(input.source, new Set())
111
+ if (!this.incoming.has(input.target)) this.incoming.set(input.target, new Set())
112
+
113
+ this.outgoing.get(input.source)!.add(edge.id)
114
+ this.incoming.get(input.target)!.add(edge.id)
115
+ this.dirty = true
116
+ return edge
117
+ }
118
+
119
+ getEdges(nodeId: string, direction: 'outgoing' | 'incoming' | 'both' = 'both'): GraphEdge[] {
120
+ const result: GraphEdge[] = []
121
+
122
+ if (direction === 'outgoing' || direction === 'both') {
123
+ const outEdgeIds = this.outgoing.get(nodeId) || new Set()
124
+ for (const edgeId of outEdgeIds) {
125
+ const edge = this.edges.get(edgeId)
126
+ if (edge) result.push(edge)
127
+ }
128
+ }
129
+
130
+ if (direction === 'incoming' || direction === 'both') {
131
+ const inEdgeIds = this.incoming.get(nodeId) || new Set()
132
+ for (const edgeId of inEdgeIds) {
133
+ const edge = this.edges.get(edgeId)
134
+ if (edge) result.push(edge)
135
+ }
136
+ }
137
+
138
+ return result
139
+ }
140
+
141
+ deleteEdge(id: string): boolean {
142
+ const edge = this.edges.get(id)
143
+ if (!edge) return false
144
+
145
+ this.outgoing.get(edge.source)?.delete(id)
146
+ this.incoming.get(edge.target)?.delete(id)
147
+ this.edges.delete(id)
148
+ this.dirty = true
149
+ return true
150
+ }
151
+
152
+ traverse(startId: string, options: TraversalOptions = {}): GraphNode[] {
153
+ const { maxDepth = 3, nodeTypes, relationshipTypes, limit = 100 } = options
154
+
155
+ if (!this.nodes.has(startId)) return []
156
+
157
+ const visited = new Set<string>()
158
+ const result: GraphNode[] = []
159
+ const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }]
160
+
161
+ while (queue.length > 0 && result.length < limit) {
162
+ const { id, depth } = queue.shift()!
163
+
164
+ if (visited.has(id)) continue
165
+ visited.add(id)
166
+
167
+ const node = this.nodes.get(id)
168
+ if (!node) continue
169
+
170
+ if (nodeTypes && !nodeTypes.includes(node.type)) {
171
+ // Don't add to results but still traverse through
172
+ } else {
173
+ result.push(node)
174
+ }
175
+
176
+ if (depth >= maxDepth) continue
177
+
178
+ // Get neighbors via edges
179
+ const edges = this.getEdges(id)
180
+ for (const edge of edges) {
181
+ if (relationshipTypes && !relationshipTypes.includes(edge.relationship)) continue
182
+
183
+ const neighborId = edge.source === id ? edge.target : edge.source
184
+ if (!visited.has(neighborId)) {
185
+ queue.push({ id: neighborId, depth: depth + 1 })
186
+ }
187
+ }
188
+ }
189
+
190
+ return result
191
+ }
192
+
193
+ shortestPath(sourceId: string, targetId: string): string[] | null {
194
+ if (!this.nodes.has(sourceId) || !this.nodes.has(targetId)) return null
195
+ if (sourceId === targetId) return [sourceId]
196
+
197
+ const visited = new Set<string>()
198
+ const parent = new Map<string, string>()
199
+ const queue: string[] = [sourceId]
200
+ visited.add(sourceId)
201
+
202
+ while (queue.length > 0) {
203
+ const current = queue.shift()!
204
+
205
+ const edges = this.getEdges(current)
206
+ for (const edge of edges) {
207
+ const neighbor = edge.source === current ? edge.target : edge.source
208
+
209
+ if (!visited.has(neighbor)) {
210
+ visited.add(neighbor)
211
+ parent.set(neighbor, current)
212
+
213
+ if (neighbor === targetId) {
214
+ // Reconstruct path
215
+ const path: string[] = [targetId]
216
+ let node = targetId
217
+ while (parent.has(node)) {
218
+ node = parent.get(node)!
219
+ path.unshift(node)
220
+ }
221
+ return path
222
+ }
223
+
224
+ queue.push(neighbor)
225
+ }
226
+ }
227
+ }
228
+
229
+ return null
230
+ }
231
+
232
+ findNodes(query: { type?: EntityType; name?: string; properties?: Record<string, unknown> }): GraphNode[] {
233
+ const results: GraphNode[] = []
234
+
235
+ for (const node of this.nodes.values()) {
236
+ if (query.type && node.type !== query.type) continue
237
+ if (query.name && !node.name.toLowerCase().includes(query.name.toLowerCase())) continue
238
+
239
+ if (query.properties) {
240
+ let match = true
241
+ for (const [key, value] of Object.entries(query.properties)) {
242
+ if (node.properties[key] !== value) {
243
+ match = false
244
+ break
245
+ }
246
+ }
247
+ if (!match) continue
248
+ }
249
+
250
+ results.push(node)
251
+ }
252
+
253
+ return results
254
+ }
255
+
256
+ findNodeByName(name: string, type?: EntityType): GraphNode | undefined {
257
+ const lowerName = name.toLowerCase()
258
+ for (const node of this.nodes.values()) {
259
+ if (node.name.toLowerCase() === lowerName) {
260
+ if (type && node.type !== type) continue
261
+ return node
262
+ }
263
+ }
264
+ return undefined
265
+ }
266
+
267
+ getNodeCount(): number {
268
+ return this.nodes.size
269
+ }
270
+
271
+ getEdgeCount(): number {
272
+ return this.edges.size
273
+ }
274
+
275
+ async save(path: string): Promise<void> {
276
+ const data: SerializedGraph = {
277
+ nodes: Array.from(this.nodes.values()),
278
+ edges: Array.from(this.edges.values()),
279
+ version: '1.0.0',
280
+ saved_at: new Date().toISOString()
281
+ }
282
+
283
+ try {
284
+ await mkdir(dirname(path), { recursive: true })
285
+ await writeFile(path, JSON.stringify(data, null, 2), 'utf-8')
286
+ this.dirty = false
287
+ this.logger.debug({ path, nodes: data.nodes.length, edges: data.edges.length }, 'Graph saved')
288
+ } catch (error) {
289
+ this.logger.error({ error, path }, 'Failed to save graph')
290
+ throw error
291
+ }
292
+ }
293
+
294
+ async load(path: string): Promise<void> {
295
+ try {
296
+ const content = await readFile(path, 'utf-8')
297
+ const data: SerializedGraph = JSON.parse(content)
298
+
299
+ this.nodes.clear()
300
+ this.edges.clear()
301
+ this.outgoing.clear()
302
+ this.incoming.clear()
303
+
304
+ for (const node of data.nodes) {
305
+ this.nodes.set(node.id, node)
306
+ this.outgoing.set(node.id, new Set())
307
+ this.incoming.set(node.id, new Set())
308
+ }
309
+
310
+ for (const edge of data.edges) {
311
+ this.edges.set(edge.id, edge)
312
+ this.outgoing.get(edge.source)?.add(edge.id)
313
+ this.incoming.get(edge.target)?.add(edge.id)
314
+ }
315
+
316
+ this.dirty = false
317
+ this.logger.info({ path, nodes: data.nodes.length, edges: data.edges.length }, 'Graph loaded')
318
+ } catch (error: any) {
319
+ if (error.code === 'ENOENT') {
320
+ this.logger.info({ path }, 'No existing graph file, starting fresh')
321
+ return
322
+ }
323
+ this.logger.error({ error, path }, 'Failed to load graph')
324
+ throw error
325
+ }
326
+ }
327
+
328
+ startAutoSave(path: string, intervalMs: number = 60000): void {
329
+ this.stopAutoSave()
330
+ this.autoSaveTimer = setInterval(async () => {
331
+ if (this.dirty) {
332
+ try {
333
+ await this.save(path)
334
+ } catch (error) {
335
+ this.logger.error({ error }, 'Auto-save failed')
336
+ }
337
+ }
338
+ }, intervalMs)
339
+ }
340
+
341
+ stopAutoSave(): void {
342
+ if (this.autoSaveTimer) {
343
+ clearInterval(this.autoSaveTimer)
344
+ this.autoSaveTimer = null
345
+ }
346
+ }
347
+
348
+ isDirty(): boolean {
349
+ return this.dirty
350
+ }
351
+
352
+ clear(): void {
353
+ this.nodes.clear()
354
+ this.edges.clear()
355
+ this.outgoing.clear()
356
+ this.incoming.clear()
357
+ this.dirty = true
358
+ }
359
+ }