claude-brain 0.30.2 → 0.30.3
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 +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- package/skills/persistent-memory/references/tool-reference.md +0 -90
|
@@ -1,359 +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:
|
|
319
|
-
if (error
|
|
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
|
-
}
|
|
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: unknown) {
|
|
319
|
+
if ((error as Record<string, unknown>)?.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
|
+
}
|