@prih/mcp-graph-memory 1.0.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/LICENSE +15 -0
- package/README.md +512 -0
- package/dist/api/index.js +473 -0
- package/dist/api/rest/code.js +78 -0
- package/dist/api/rest/docs.js +80 -0
- package/dist/api/rest/files.js +64 -0
- package/dist/api/rest/graph.js +56 -0
- package/dist/api/rest/index.js +117 -0
- package/dist/api/rest/knowledge.js +238 -0
- package/dist/api/rest/skills.js +284 -0
- package/dist/api/rest/tasks.js +272 -0
- package/dist/api/rest/tools.js +126 -0
- package/dist/api/rest/validation.js +191 -0
- package/dist/api/rest/websocket.js +65 -0
- package/dist/api/tools/code/get-file-symbols.js +30 -0
- package/dist/api/tools/code/get-symbol.js +22 -0
- package/dist/api/tools/code/list-files.js +18 -0
- package/dist/api/tools/code/search-code.js +27 -0
- package/dist/api/tools/code/search-files.js +22 -0
- package/dist/api/tools/context/get-context.js +19 -0
- package/dist/api/tools/docs/cross-references.js +76 -0
- package/dist/api/tools/docs/explain-symbol.js +55 -0
- package/dist/api/tools/docs/find-examples.js +52 -0
- package/dist/api/tools/docs/get-node.js +24 -0
- package/dist/api/tools/docs/get-toc.js +22 -0
- package/dist/api/tools/docs/list-snippets.js +46 -0
- package/dist/api/tools/docs/list-topics.js +18 -0
- package/dist/api/tools/docs/search-files.js +22 -0
- package/dist/api/tools/docs/search-snippets.js +43 -0
- package/dist/api/tools/docs/search.js +27 -0
- package/dist/api/tools/file-index/get-file-info.js +21 -0
- package/dist/api/tools/file-index/list-all-files.js +28 -0
- package/dist/api/tools/file-index/search-all-files.js +24 -0
- package/dist/api/tools/knowledge/add-attachment.js +31 -0
- package/dist/api/tools/knowledge/create-note.js +20 -0
- package/dist/api/tools/knowledge/create-relation.js +29 -0
- package/dist/api/tools/knowledge/delete-note.js +19 -0
- package/dist/api/tools/knowledge/delete-relation.js +23 -0
- package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
- package/dist/api/tools/knowledge/get-note.js +20 -0
- package/dist/api/tools/knowledge/list-notes.js +18 -0
- package/dist/api/tools/knowledge/list-relations.js +17 -0
- package/dist/api/tools/knowledge/remove-attachment.js +19 -0
- package/dist/api/tools/knowledge/search-notes.js +25 -0
- package/dist/api/tools/knowledge/update-note.js +34 -0
- package/dist/api/tools/skills/add-attachment.js +31 -0
- package/dist/api/tools/skills/bump-usage.js +19 -0
- package/dist/api/tools/skills/create-skill-link.js +25 -0
- package/dist/api/tools/skills/create-skill.js +26 -0
- package/dist/api/tools/skills/delete-skill-link.js +23 -0
- package/dist/api/tools/skills/delete-skill.js +20 -0
- package/dist/api/tools/skills/find-linked-skills.js +25 -0
- package/dist/api/tools/skills/get-skill.js +21 -0
- package/dist/api/tools/skills/link-skill.js +23 -0
- package/dist/api/tools/skills/list-skills.js +20 -0
- package/dist/api/tools/skills/recall-skills.js +18 -0
- package/dist/api/tools/skills/remove-attachment.js +19 -0
- package/dist/api/tools/skills/search-skills.js +25 -0
- package/dist/api/tools/skills/update-skill.js +58 -0
- package/dist/api/tools/tasks/add-attachment.js +31 -0
- package/dist/api/tools/tasks/create-task-link.js +25 -0
- package/dist/api/tools/tasks/create-task.js +25 -0
- package/dist/api/tools/tasks/delete-task-link.js +23 -0
- package/dist/api/tools/tasks/delete-task.js +20 -0
- package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
- package/dist/api/tools/tasks/get-task.js +20 -0
- package/dist/api/tools/tasks/link-task.js +23 -0
- package/dist/api/tools/tasks/list-tasks.js +24 -0
- package/dist/api/tools/tasks/move-task.js +38 -0
- package/dist/api/tools/tasks/remove-attachment.js +19 -0
- package/dist/api/tools/tasks/search-tasks.js +25 -0
- package/dist/api/tools/tasks/update-task.js +55 -0
- package/dist/cli/index.js +451 -0
- package/dist/cli/indexer.js +277 -0
- package/dist/graphs/attachment-types.js +74 -0
- package/dist/graphs/code-types.js +10 -0
- package/dist/graphs/code.js +172 -0
- package/dist/graphs/docs.js +198 -0
- package/dist/graphs/file-index-types.js +10 -0
- package/dist/graphs/file-index.js +310 -0
- package/dist/graphs/file-lang.js +119 -0
- package/dist/graphs/knowledge-types.js +32 -0
- package/dist/graphs/knowledge.js +764 -0
- package/dist/graphs/manager-types.js +87 -0
- package/dist/graphs/skill-types.js +10 -0
- package/dist/graphs/skill.js +1013 -0
- package/dist/graphs/task-types.js +17 -0
- package/dist/graphs/task.js +960 -0
- package/dist/lib/embedder.js +101 -0
- package/dist/lib/events-log.js +400 -0
- package/dist/lib/file-import.js +327 -0
- package/dist/lib/file-mirror.js +446 -0
- package/dist/lib/frontmatter.js +17 -0
- package/dist/lib/mirror-watcher.js +637 -0
- package/dist/lib/multi-config.js +254 -0
- package/dist/lib/parsers/code.js +246 -0
- package/dist/lib/parsers/codeblock.js +66 -0
- package/dist/lib/parsers/docs.js +196 -0
- package/dist/lib/project-manager.js +418 -0
- package/dist/lib/promise-queue.js +22 -0
- package/dist/lib/search/bm25.js +167 -0
- package/dist/lib/search/code.js +103 -0
- package/dist/lib/search/docs.js +108 -0
- package/dist/lib/search/file-index.js +31 -0
- package/dist/lib/search/files.js +61 -0
- package/dist/lib/search/knowledge.js +101 -0
- package/dist/lib/search/skills.js +104 -0
- package/dist/lib/search/tasks.js +103 -0
- package/dist/lib/watcher.js +67 -0
- package/package.json +83 -0
- package/ui/README.md +54 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.KnowledgeGraphManager = exports.createKnowledgeGraph = void 0;
|
|
7
|
+
exports.proxyId = proxyId;
|
|
8
|
+
exports.isProxy = isProxy;
|
|
9
|
+
exports.cleanupProxies = cleanupProxies;
|
|
10
|
+
exports.createNote = createNote;
|
|
11
|
+
exports.updateNote = updateNote;
|
|
12
|
+
exports.deleteNote = deleteNote;
|
|
13
|
+
exports.getNote = getNote;
|
|
14
|
+
exports.listNotes = listNotes;
|
|
15
|
+
exports.createRelation = createRelation;
|
|
16
|
+
exports.deleteRelation = deleteRelation;
|
|
17
|
+
exports.listRelations = listRelations;
|
|
18
|
+
exports.findLinkedNotes = findLinkedNotes;
|
|
19
|
+
exports.createCrossRelation = createCrossRelation;
|
|
20
|
+
exports.deleteCrossRelation = deleteCrossRelation;
|
|
21
|
+
exports.saveKnowledgeGraph = saveKnowledgeGraph;
|
|
22
|
+
exports.loadKnowledgeGraph = loadKnowledgeGraph;
|
|
23
|
+
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const knowledge_types_1 = require("../graphs/knowledge-types");
|
|
26
|
+
Object.defineProperty(exports, "createKnowledgeGraph", { enumerable: true, get: function () { return knowledge_types_1.createKnowledgeGraph; } });
|
|
27
|
+
const manager_types_1 = require("../graphs/manager-types");
|
|
28
|
+
const knowledge_1 = require("../lib/search/knowledge");
|
|
29
|
+
const bm25_1 = require("../lib/search/bm25");
|
|
30
|
+
const file_mirror_1 = require("../lib/file-mirror");
|
|
31
|
+
const attachment_types_1 = require("../graphs/attachment-types");
|
|
32
|
+
const file_import_1 = require("../lib/file-import");
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Proxy helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/** Build the proxy node ID. With projectId: `@docs::frontend::guide.md::Setup`, without: `@docs::guide.md::Setup` */
|
|
37
|
+
function proxyId(targetGraph, nodeId, projectId) {
|
|
38
|
+
return projectId ? `@${targetGraph}::${projectId}::${nodeId}` : `@${targetGraph}::${nodeId}`;
|
|
39
|
+
}
|
|
40
|
+
/** Check whether a node is a cross-graph proxy. */
|
|
41
|
+
function isProxy(graph, nodeId) {
|
|
42
|
+
if (!graph.hasNode(nodeId))
|
|
43
|
+
return false;
|
|
44
|
+
return graph.getNodeAttribute(nodeId, 'proxyFor') !== undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Ensure a proxy node exists for the given external target. Returns its ID. */
|
|
47
|
+
function ensureProxyNode(graph, targetGraph, nodeId, projectId) {
|
|
48
|
+
const id = proxyId(targetGraph, nodeId, projectId);
|
|
49
|
+
if (!graph.hasNode(id)) {
|
|
50
|
+
graph.addNode(id, {
|
|
51
|
+
title: '',
|
|
52
|
+
content: '',
|
|
53
|
+
tags: [],
|
|
54
|
+
embedding: [],
|
|
55
|
+
attachments: [],
|
|
56
|
+
createdAt: 0,
|
|
57
|
+
updatedAt: 0,
|
|
58
|
+
version: 0,
|
|
59
|
+
proxyFor: { graph: targetGraph, nodeId, projectId },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
/** Remove a proxy node if it has zero incident edges. */
|
|
65
|
+
function cleanupProxy(graph, nodeId) {
|
|
66
|
+
if (!graph.hasNode(nodeId))
|
|
67
|
+
return;
|
|
68
|
+
if (!isProxy(graph, nodeId))
|
|
69
|
+
return;
|
|
70
|
+
if (graph.degree(nodeId) === 0) {
|
|
71
|
+
graph.dropNode(nodeId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Remove all proxy nodes whose target no longer exists in the external graph.
|
|
76
|
+
* Called after doc/code file removal in the indexer.
|
|
77
|
+
*/
|
|
78
|
+
function cleanupProxies(graph, targetGraph, externalGraph) {
|
|
79
|
+
const toRemove = [];
|
|
80
|
+
graph.forEachNode((id, attrs) => {
|
|
81
|
+
if (attrs.proxyFor && attrs.proxyFor.graph === targetGraph) {
|
|
82
|
+
if (!externalGraph.hasNode(attrs.proxyFor.nodeId)) {
|
|
83
|
+
toRemove.push(id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
for (const id of toRemove) {
|
|
88
|
+
graph.dropNode(id); // also drops incident edges
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// CRUD — Notes
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/** Create a note, return its slug ID. */
|
|
95
|
+
function createNote(graph, title, content, tags, embedding, author = '') {
|
|
96
|
+
const id = (0, knowledge_types_1.slugify)(title, graph);
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
graph.addNode(id, {
|
|
99
|
+
title,
|
|
100
|
+
content,
|
|
101
|
+
tags,
|
|
102
|
+
embedding,
|
|
103
|
+
attachments: [],
|
|
104
|
+
createdAt: now,
|
|
105
|
+
updatedAt: now,
|
|
106
|
+
version: 1,
|
|
107
|
+
createdBy: author || undefined,
|
|
108
|
+
updatedBy: author || undefined,
|
|
109
|
+
});
|
|
110
|
+
return id;
|
|
111
|
+
}
|
|
112
|
+
/** Partial update of a note. Returns true if found and updated. Throws VersionConflictError if expectedVersion is provided and doesn't match. */
|
|
113
|
+
function updateNote(graph, noteId, patch, embedding, author = '', expectedVersion) {
|
|
114
|
+
if (!graph.hasNode(noteId))
|
|
115
|
+
return false;
|
|
116
|
+
if (expectedVersion !== undefined) {
|
|
117
|
+
const current = graph.getNodeAttribute(noteId, 'version');
|
|
118
|
+
if (current !== expectedVersion)
|
|
119
|
+
throw new manager_types_1.VersionConflictError(current, expectedVersion);
|
|
120
|
+
}
|
|
121
|
+
if (patch.title !== undefined)
|
|
122
|
+
graph.setNodeAttribute(noteId, 'title', patch.title);
|
|
123
|
+
if (patch.content !== undefined)
|
|
124
|
+
graph.setNodeAttribute(noteId, 'content', patch.content);
|
|
125
|
+
if (patch.tags !== undefined)
|
|
126
|
+
graph.setNodeAttribute(noteId, 'tags', patch.tags);
|
|
127
|
+
if (embedding !== undefined)
|
|
128
|
+
graph.setNodeAttribute(noteId, 'embedding', embedding);
|
|
129
|
+
if (author)
|
|
130
|
+
graph.setNodeAttribute(noteId, 'updatedBy', author);
|
|
131
|
+
graph.setNodeAttribute(noteId, 'version', graph.getNodeAttribute(noteId, 'version') + 1);
|
|
132
|
+
graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/** Delete a note and all its incident edges. Also cleans up orphaned proxy nodes. */
|
|
136
|
+
function deleteNote(graph, noteId) {
|
|
137
|
+
if (!graph.hasNode(noteId))
|
|
138
|
+
return false;
|
|
139
|
+
// Collect proxy neighbors before dropping the note
|
|
140
|
+
const proxyNeighbors = [];
|
|
141
|
+
graph.forEachNeighbor(noteId, (neighbor) => {
|
|
142
|
+
if (isProxy(graph, neighbor))
|
|
143
|
+
proxyNeighbors.push(neighbor);
|
|
144
|
+
});
|
|
145
|
+
graph.dropNode(noteId);
|
|
146
|
+
// Cleanup orphaned proxies (they lost an edge when the note was dropped)
|
|
147
|
+
for (const p of proxyNeighbors) {
|
|
148
|
+
cleanupProxy(graph, p);
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
/** Get a note by ID, or null if not found. Excludes proxy nodes. */
|
|
153
|
+
function getNote(graph, noteId) {
|
|
154
|
+
if (!graph.hasNode(noteId))
|
|
155
|
+
return null;
|
|
156
|
+
if (isProxy(graph, noteId))
|
|
157
|
+
return null;
|
|
158
|
+
return { id: noteId, ...graph.getNodeAttributes(noteId) };
|
|
159
|
+
}
|
|
160
|
+
/** List notes with optional filter (substring in title/id) and tag filter. Excludes proxy nodes. */
|
|
161
|
+
function listNotes(graph, filter, tag, limit = 20) {
|
|
162
|
+
const lowerFilter = filter?.toLowerCase();
|
|
163
|
+
const lowerTag = tag?.toLowerCase();
|
|
164
|
+
const results = [];
|
|
165
|
+
graph.forEachNode((id, attrs) => {
|
|
166
|
+
if (attrs.proxyFor)
|
|
167
|
+
return; // skip proxy nodes
|
|
168
|
+
if (lowerFilter) {
|
|
169
|
+
const match = id.toLowerCase().includes(lowerFilter) ||
|
|
170
|
+
attrs.title.toLowerCase().includes(lowerFilter);
|
|
171
|
+
if (!match)
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (lowerTag) {
|
|
175
|
+
if (!attrs.tags.some(t => t.toLowerCase() === lowerTag))
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
results.push({ id, title: attrs.title, content: attrs.content.slice(0, 500), tags: attrs.tags, updatedAt: attrs.updatedAt });
|
|
179
|
+
});
|
|
180
|
+
return results
|
|
181
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
182
|
+
.slice(0, limit);
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// CRUD — Relations (note ↔ note)
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
/** Create a directed relation between two notes. Returns true if created. */
|
|
188
|
+
function createRelation(graph, fromId, toId, kind) {
|
|
189
|
+
if (!graph.hasNode(fromId) || !graph.hasNode(toId))
|
|
190
|
+
return false;
|
|
191
|
+
if (graph.hasEdge(fromId, toId))
|
|
192
|
+
return false;
|
|
193
|
+
graph.addEdgeWithKey(`${fromId}→${toId}`, fromId, toId, { kind });
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
/** Delete a relation. Cleans up orphaned proxy nodes. Returns true if it existed. */
|
|
197
|
+
function deleteRelation(graph, fromId, toId) {
|
|
198
|
+
if (!graph.hasEdge(fromId, toId))
|
|
199
|
+
return false;
|
|
200
|
+
graph.dropEdge(fromId, toId);
|
|
201
|
+
// Clean up proxy if it became orphaned
|
|
202
|
+
cleanupProxy(graph, fromId);
|
|
203
|
+
cleanupProxy(graph, toId);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
/** List all relations for a note (both incoming and outgoing). Resolves proxy IDs and titles. */
|
|
207
|
+
function listRelations(graph, noteId, externalGraphs) {
|
|
208
|
+
if (!graph.hasNode(noteId))
|
|
209
|
+
return [];
|
|
210
|
+
const results = [];
|
|
211
|
+
function resolveTitle(nodeId, targetGraph) {
|
|
212
|
+
if (!targetGraph) {
|
|
213
|
+
// Same-graph note
|
|
214
|
+
if (graph.hasNode(nodeId) && !isProxy(graph, nodeId)) {
|
|
215
|
+
return graph.getNodeAttribute(nodeId, 'title') || undefined;
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
if (!externalGraphs)
|
|
220
|
+
return undefined;
|
|
221
|
+
const extGraph = (0, manager_types_1.resolveExternalGraph)(externalGraphs, targetGraph);
|
|
222
|
+
if (!extGraph || !extGraph.hasNode(nodeId))
|
|
223
|
+
return undefined;
|
|
224
|
+
const attrs = extGraph.getNodeAttributes(nodeId);
|
|
225
|
+
return attrs.title || attrs.name || undefined;
|
|
226
|
+
}
|
|
227
|
+
function resolveEntry(source, target, kind) {
|
|
228
|
+
// Check if either end is a proxy node and resolve it
|
|
229
|
+
const sourceProxy = graph.hasNode(source) ? graph.getNodeAttribute(source, 'proxyFor') : undefined;
|
|
230
|
+
const targetProxy = graph.hasNode(target) ? graph.getNodeAttribute(target, 'proxyFor') : undefined;
|
|
231
|
+
if (targetProxy) {
|
|
232
|
+
const title = resolveTitle(targetProxy.nodeId, targetProxy.graph);
|
|
233
|
+
return { fromId: source, toId: targetProxy.nodeId, kind, targetGraph: targetProxy.graph, ...(title ? { title } : {}) };
|
|
234
|
+
}
|
|
235
|
+
if (sourceProxy) {
|
|
236
|
+
const title = resolveTitle(sourceProxy.nodeId, sourceProxy.graph);
|
|
237
|
+
return { fromId: sourceProxy.nodeId, toId: target, kind, targetGraph: sourceProxy.graph, ...(title ? { title } : {}) };
|
|
238
|
+
}
|
|
239
|
+
// Same-graph note↔note: resolve the "other" side's title
|
|
240
|
+
const otherId = source === noteId ? target : source;
|
|
241
|
+
const title = resolveTitle(otherId);
|
|
242
|
+
return { fromId: source, toId: target, kind, ...(title ? { title } : {}) };
|
|
243
|
+
}
|
|
244
|
+
graph.forEachOutEdge(noteId, (_edge, attrs, source, target) => {
|
|
245
|
+
results.push(resolveEntry(source, target, attrs.kind));
|
|
246
|
+
});
|
|
247
|
+
graph.forEachInEdge(noteId, (_edge, attrs, source, target) => {
|
|
248
|
+
results.push(resolveEntry(source, target, attrs.kind));
|
|
249
|
+
});
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Find all notes that have a cross-graph relation to the given target node.
|
|
254
|
+
* Optionally filter by relation kind.
|
|
255
|
+
*/
|
|
256
|
+
function findLinkedNotes(graph, targetGraph, targetNodeId, kind, projectId) {
|
|
257
|
+
// Check both project-scoped and legacy proxy IDs
|
|
258
|
+
const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
|
|
259
|
+
if (projectId)
|
|
260
|
+
candidates.push(proxyId(targetGraph, targetNodeId));
|
|
261
|
+
const results = [];
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
for (const pId of candidates) {
|
|
264
|
+
if (!graph.hasNode(pId))
|
|
265
|
+
continue;
|
|
266
|
+
graph.forEachInEdge(pId, (_edge, attrs, source) => {
|
|
267
|
+
if (seen.has(source))
|
|
268
|
+
return;
|
|
269
|
+
if (isProxy(graph, source))
|
|
270
|
+
return;
|
|
271
|
+
if (kind && attrs.kind !== kind)
|
|
272
|
+
return;
|
|
273
|
+
const noteAttrs = graph.getNodeAttributes(source);
|
|
274
|
+
seen.add(source);
|
|
275
|
+
results.push({
|
|
276
|
+
noteId: source,
|
|
277
|
+
title: noteAttrs.title,
|
|
278
|
+
kind: attrs.kind,
|
|
279
|
+
tags: noteAttrs.tags,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Cross-graph relations (note → doc/code node)
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* Create a cross-graph relation from a note to a node in the doc or code graph.
|
|
290
|
+
* Optionally validates that the target exists in the external graph.
|
|
291
|
+
*/
|
|
292
|
+
function createCrossRelation(graph, fromNoteId, targetGraph, targetNodeId, kind, externalGraph, projectId) {
|
|
293
|
+
// Source must be a real note (not a proxy)
|
|
294
|
+
if (!graph.hasNode(fromNoteId) || isProxy(graph, fromNoteId))
|
|
295
|
+
return false;
|
|
296
|
+
// Validate target exists in external graph if provided
|
|
297
|
+
if (externalGraph && !externalGraph.hasNode(targetNodeId))
|
|
298
|
+
return false;
|
|
299
|
+
const pId = ensureProxyNode(graph, targetGraph, targetNodeId, projectId);
|
|
300
|
+
if (graph.hasEdge(fromNoteId, pId))
|
|
301
|
+
return false;
|
|
302
|
+
graph.addEdgeWithKey(`${fromNoteId}→${pId}`, fromNoteId, pId, { kind });
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Delete a cross-graph relation. Cleans up orphaned proxy node.
|
|
307
|
+
*/
|
|
308
|
+
function deleteCrossRelation(graph, fromNoteId, targetGraph, targetNodeId, projectId) {
|
|
309
|
+
// Try project-scoped first, then legacy
|
|
310
|
+
const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
|
|
311
|
+
if (projectId)
|
|
312
|
+
candidates.push(proxyId(targetGraph, targetNodeId));
|
|
313
|
+
for (const pId of candidates) {
|
|
314
|
+
if (graph.hasEdge(fromNoteId, pId)) {
|
|
315
|
+
graph.dropEdge(fromNoteId, pId);
|
|
316
|
+
cleanupProxy(graph, pId);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Persistence
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
function saveKnowledgeGraph(graph, graphMemory, embeddingFingerprint) {
|
|
326
|
+
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
327
|
+
const file = path_1.default.join(graphMemory, 'knowledge.json');
|
|
328
|
+
const tmp = file + '.tmp';
|
|
329
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
|
|
330
|
+
fs_1.default.renameSync(tmp, file);
|
|
331
|
+
}
|
|
332
|
+
function loadKnowledgeGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
333
|
+
const graph = (0, knowledge_types_1.createKnowledgeGraph)();
|
|
334
|
+
if (fresh)
|
|
335
|
+
return graph;
|
|
336
|
+
const file = path_1.default.join(graphMemory, 'knowledge.json');
|
|
337
|
+
if (!fs_1.default.existsSync(file))
|
|
338
|
+
return graph;
|
|
339
|
+
try {
|
|
340
|
+
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
341
|
+
const stored = data.embeddingModel;
|
|
342
|
+
if (embeddingFingerprint && stored !== embeddingFingerprint) {
|
|
343
|
+
process.stderr.write(`[knowledge-graph] Embedding config changed, re-indexing knowledge graph\n`);
|
|
344
|
+
return graph;
|
|
345
|
+
}
|
|
346
|
+
graph.import(data.graph);
|
|
347
|
+
process.stderr.write(`[knowledge-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
process.stderr.write(`[knowledge-graph] Failed to load graph, starting fresh: ${err}\n`);
|
|
351
|
+
}
|
|
352
|
+
return graph;
|
|
353
|
+
}
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Bidirectional mirror helpers (Knowledge ↔ Task)
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
/**
|
|
358
|
+
* Create a mirror proxy in TaskGraph when a note links to a task.
|
|
359
|
+
* Creates `@knowledge::noteId` proxy node + edge proxy→taskId in TaskGraph.
|
|
360
|
+
*/
|
|
361
|
+
function createMirrorInTaskGraph(taskGraph, noteId, taskId, kind) {
|
|
362
|
+
const mirrorProxyId = `@knowledge::${noteId}`;
|
|
363
|
+
if (!taskGraph.hasNode(mirrorProxyId)) {
|
|
364
|
+
taskGraph.addNode(mirrorProxyId, {
|
|
365
|
+
title: '',
|
|
366
|
+
description: '',
|
|
367
|
+
status: 'backlog',
|
|
368
|
+
priority: 'low',
|
|
369
|
+
tags: [],
|
|
370
|
+
dueDate: null,
|
|
371
|
+
estimate: null,
|
|
372
|
+
completedAt: null,
|
|
373
|
+
embedding: [],
|
|
374
|
+
attachments: [],
|
|
375
|
+
createdAt: 0,
|
|
376
|
+
updatedAt: 0,
|
|
377
|
+
version: 0,
|
|
378
|
+
proxyFor: { graph: 'knowledge', nodeId: noteId },
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (!taskGraph.hasNode(taskId))
|
|
382
|
+
return;
|
|
383
|
+
const edgeKey = `${mirrorProxyId}→${taskId}`;
|
|
384
|
+
if (!taskGraph.hasEdge(edgeKey)) {
|
|
385
|
+
taskGraph.addEdgeWithKey(edgeKey, mirrorProxyId, taskId, { kind });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Remove the mirror proxy edge/node from TaskGraph when a note→task relation is deleted.
|
|
390
|
+
*/
|
|
391
|
+
function deleteMirrorFromTaskGraph(taskGraph, noteId, taskId) {
|
|
392
|
+
const mirrorProxyId = `@knowledge::${noteId}`;
|
|
393
|
+
const edgeKey = `${mirrorProxyId}→${taskId}`;
|
|
394
|
+
if (taskGraph.hasEdge(edgeKey)) {
|
|
395
|
+
taskGraph.dropEdge(edgeKey);
|
|
396
|
+
}
|
|
397
|
+
// Cleanup orphan proxy
|
|
398
|
+
if (taskGraph.hasNode(mirrorProxyId)) {
|
|
399
|
+
const proxyFor = taskGraph.getNodeAttribute(mirrorProxyId, 'proxyFor');
|
|
400
|
+
if (proxyFor && taskGraph.degree(mirrorProxyId) === 0) {
|
|
401
|
+
taskGraph.dropNode(mirrorProxyId);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// KnowledgeGraphManager — unified API for knowledge graph operations
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
class KnowledgeGraphManager {
|
|
409
|
+
_graph;
|
|
410
|
+
embedFns;
|
|
411
|
+
ctx;
|
|
412
|
+
ext;
|
|
413
|
+
taskGraph;
|
|
414
|
+
mirrorTracker;
|
|
415
|
+
_bm25Index;
|
|
416
|
+
get externalGraphs() { return this.ext; }
|
|
417
|
+
constructor(_graph, embedFns, ctx, ext = {}) {
|
|
418
|
+
this._graph = _graph;
|
|
419
|
+
this.embedFns = embedFns;
|
|
420
|
+
this.ctx = ctx;
|
|
421
|
+
this.ext = ext;
|
|
422
|
+
this.taskGraph = ext.taskGraph;
|
|
423
|
+
this._bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.title} ${attrs.content} ${attrs.tags.join(' ')}`);
|
|
424
|
+
this._graph.forEachNode((id, attrs) => {
|
|
425
|
+
if (!attrs.proxyFor)
|
|
426
|
+
this._bm25Index.addDocument(id, attrs);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
get graph() { return this._graph; }
|
|
430
|
+
get bm25Index() { return this._bm25Index; }
|
|
431
|
+
rebuildBm25Index() {
|
|
432
|
+
this._bm25Index.clear();
|
|
433
|
+
this._graph.forEachNode((id, attrs) => {
|
|
434
|
+
if (!attrs.proxyFor)
|
|
435
|
+
this._bm25Index.addDocument(id, attrs);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
setMirrorTracker(tracker) {
|
|
439
|
+
this.mirrorTracker = tracker;
|
|
440
|
+
}
|
|
441
|
+
/** Returns updatedAt for a node, or null if not found. Used by startup scan. */
|
|
442
|
+
getNodeUpdatedAt(noteId) {
|
|
443
|
+
if (!this._graph.hasNode(noteId))
|
|
444
|
+
return null;
|
|
445
|
+
if (isProxy(this._graph, noteId))
|
|
446
|
+
return null;
|
|
447
|
+
return this._graph.getNodeAttribute(noteId, 'updatedAt') ?? null;
|
|
448
|
+
}
|
|
449
|
+
get notesDir() {
|
|
450
|
+
const base = this.ctx.mirrorDir ?? this.ctx.projectDir;
|
|
451
|
+
return base ? path_1.default.join(base, '.notes') : undefined;
|
|
452
|
+
}
|
|
453
|
+
recordMirrorWrites(noteId) {
|
|
454
|
+
const dir = this.notesDir;
|
|
455
|
+
if (!dir || !this.mirrorTracker)
|
|
456
|
+
return;
|
|
457
|
+
const entityDir = path_1.default.join(dir, noteId);
|
|
458
|
+
this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
|
|
459
|
+
this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'note.md'));
|
|
460
|
+
this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'content.md'));
|
|
461
|
+
}
|
|
462
|
+
// -- Write (mutations with embed + dirty + emit + cross-graph cleanup) --
|
|
463
|
+
async createNote(title, content, tags = []) {
|
|
464
|
+
const embedding = await this.embedFns.document(`${title} ${content}`);
|
|
465
|
+
const noteId = createNote(this._graph, title, content, tags, embedding, this.ctx.author);
|
|
466
|
+
this._bm25Index.addDocument(noteId, this._graph.getNodeAttributes(noteId));
|
|
467
|
+
this.ctx.markDirty();
|
|
468
|
+
this.ctx.emit('note:created', { projectId: this.ctx.projectId, noteId });
|
|
469
|
+
const dir = this.notesDir;
|
|
470
|
+
if (dir) {
|
|
471
|
+
const attrs = this._graph.getNodeAttributes(noteId);
|
|
472
|
+
(0, file_mirror_1.mirrorNoteCreate)(dir, noteId, attrs, []);
|
|
473
|
+
this.recordMirrorWrites(noteId);
|
|
474
|
+
}
|
|
475
|
+
return noteId;
|
|
476
|
+
}
|
|
477
|
+
async updateNote(noteId, patch, expectedVersion) {
|
|
478
|
+
const existing = getNote(this._graph, noteId);
|
|
479
|
+
if (!existing)
|
|
480
|
+
return false;
|
|
481
|
+
const embedText = `${patch.title ?? existing.title} ${patch.content ?? existing.content}`;
|
|
482
|
+
const embedding = await this.embedFns.document(embedText);
|
|
483
|
+
updateNote(this._graph, noteId, patch, embedding, this.ctx.author, expectedVersion);
|
|
484
|
+
this._bm25Index.updateDocument(noteId, this._graph.getNodeAttributes(noteId));
|
|
485
|
+
this.ctx.markDirty();
|
|
486
|
+
this.ctx.emit('note:updated', { projectId: this.ctx.projectId, noteId });
|
|
487
|
+
const dir = this.notesDir;
|
|
488
|
+
if (dir) {
|
|
489
|
+
const attrs = this._graph.getNodeAttributes(noteId);
|
|
490
|
+
const relations = listRelations(this._graph, noteId, this.ext);
|
|
491
|
+
(0, file_mirror_1.mirrorNoteUpdate)(dir, noteId, { ...patch, by: this.ctx.author }, attrs, relations);
|
|
492
|
+
this.recordMirrorWrites(noteId);
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
deleteNote(noteId) {
|
|
497
|
+
if (this.notesDir)
|
|
498
|
+
(0, file_mirror_1.deleteMirrorDir)(this.notesDir, noteId);
|
|
499
|
+
this._bm25Index.removeDocument(noteId);
|
|
500
|
+
const ok = deleteNote(this._graph, noteId);
|
|
501
|
+
if (!ok)
|
|
502
|
+
return false;
|
|
503
|
+
// Clean up proxy in TaskGraph if any task links to this note
|
|
504
|
+
if (this.taskGraph) {
|
|
505
|
+
const toRemove = [];
|
|
506
|
+
this.taskGraph.forEachNode((id, attrs) => {
|
|
507
|
+
if (attrs.proxyFor?.graph === 'knowledge' && attrs.proxyFor.nodeId === noteId) {
|
|
508
|
+
toRemove.push(id);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
for (const id of toRemove)
|
|
512
|
+
this.taskGraph.dropNode(id);
|
|
513
|
+
}
|
|
514
|
+
this.ctx.markDirty();
|
|
515
|
+
this.ctx.emit('note:deleted', { projectId: this.ctx.projectId, noteId });
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
createRelation(fromId, toId, kind, targetGraph, projectId) {
|
|
519
|
+
const pid = projectId || this.ctx.projectId;
|
|
520
|
+
let ok;
|
|
521
|
+
if (targetGraph) {
|
|
522
|
+
const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, targetGraph, pid);
|
|
523
|
+
ok = createCrossRelation(this._graph, fromId, targetGraph, toId, kind, extGraph, pid);
|
|
524
|
+
// Bidirectional: create mirror proxy in TaskGraph
|
|
525
|
+
if (ok && targetGraph === 'tasks' && this.taskGraph) {
|
|
526
|
+
createMirrorInTaskGraph(this.taskGraph, fromId, toId, kind);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
ok = createRelation(this._graph, fromId, toId, kind);
|
|
531
|
+
}
|
|
532
|
+
if (ok) {
|
|
533
|
+
this.ctx.markDirty();
|
|
534
|
+
const dir = this.notesDir;
|
|
535
|
+
if (dir) {
|
|
536
|
+
const attrs = this._graph.getNodeAttributes(fromId);
|
|
537
|
+
const relations = listRelations(this._graph, fromId, this.ext);
|
|
538
|
+
(0, file_mirror_1.mirrorNoteRelation)(dir, fromId, 'add', kind, toId, attrs, relations, targetGraph);
|
|
539
|
+
this.recordMirrorWrites(fromId);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return ok;
|
|
543
|
+
}
|
|
544
|
+
deleteRelation(fromId, toId, targetGraph, projectId) {
|
|
545
|
+
const pid = projectId || this.ctx.projectId;
|
|
546
|
+
// Read edge kind before deleting (for event log)
|
|
547
|
+
let kind = '';
|
|
548
|
+
try {
|
|
549
|
+
const actualToId = targetGraph ? proxyId(targetGraph, toId, pid) : toId;
|
|
550
|
+
if (this._graph.hasEdge(fromId, actualToId)) {
|
|
551
|
+
kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, actualToId), 'kind') ?? '';
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch { /* ignore */ }
|
|
555
|
+
let ok;
|
|
556
|
+
if (targetGraph) {
|
|
557
|
+
ok = deleteCrossRelation(this._graph, fromId, targetGraph, toId, pid);
|
|
558
|
+
// Bidirectional: remove mirror proxy from TaskGraph
|
|
559
|
+
if (ok && targetGraph === 'tasks' && this.taskGraph) {
|
|
560
|
+
deleteMirrorFromTaskGraph(this.taskGraph, fromId, toId);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
ok = deleteRelation(this._graph, fromId, toId);
|
|
565
|
+
}
|
|
566
|
+
if (ok) {
|
|
567
|
+
this.ctx.markDirty();
|
|
568
|
+
const dir = this.notesDir;
|
|
569
|
+
if (dir) {
|
|
570
|
+
const attrs = this._graph.getNodeAttributes(fromId);
|
|
571
|
+
const relations = listRelations(this._graph, fromId, this.ext);
|
|
572
|
+
(0, file_mirror_1.mirrorNoteRelation)(dir, fromId, 'remove', kind, toId, attrs, relations, targetGraph);
|
|
573
|
+
this.recordMirrorWrites(fromId);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return ok;
|
|
577
|
+
}
|
|
578
|
+
// -- Attachments --
|
|
579
|
+
addAttachment(noteId, filename, data) {
|
|
580
|
+
const dir = this.notesDir;
|
|
581
|
+
if (!dir)
|
|
582
|
+
return null;
|
|
583
|
+
if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
|
|
584
|
+
return null;
|
|
585
|
+
const safe = (0, file_mirror_1.sanitizeFilename)(filename);
|
|
586
|
+
if (!safe)
|
|
587
|
+
return null;
|
|
588
|
+
(0, file_mirror_1.writeAttachment)(dir, noteId, safe, data);
|
|
589
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'attachments', safe));
|
|
590
|
+
(0, file_mirror_1.mirrorAttachmentEvent)(path_1.default.join(dir, noteId), 'add', safe);
|
|
591
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'events.jsonl'));
|
|
592
|
+
const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
|
|
593
|
+
this._graph.setNodeAttribute(noteId, 'attachments', attachments);
|
|
594
|
+
this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
|
|
595
|
+
this.ctx.markDirty();
|
|
596
|
+
this.ctx.emit('note:attachment:added', { projectId: this.ctx.projectId, noteId, filename: safe });
|
|
597
|
+
return attachments.find(a => a.filename === safe) ?? null;
|
|
598
|
+
}
|
|
599
|
+
removeAttachment(noteId, filename) {
|
|
600
|
+
const dir = this.notesDir;
|
|
601
|
+
if (!dir)
|
|
602
|
+
return false;
|
|
603
|
+
if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
|
|
604
|
+
return false;
|
|
605
|
+
const safe = (0, file_mirror_1.sanitizeFilename)(filename);
|
|
606
|
+
const deleted = (0, file_mirror_1.deleteAttachment)(dir, noteId, safe);
|
|
607
|
+
if (!deleted)
|
|
608
|
+
return false;
|
|
609
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'attachments', safe));
|
|
610
|
+
(0, file_mirror_1.mirrorAttachmentEvent)(path_1.default.join(dir, noteId), 'remove', safe);
|
|
611
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'events.jsonl'));
|
|
612
|
+
const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
|
|
613
|
+
this._graph.setNodeAttribute(noteId, 'attachments', attachments);
|
|
614
|
+
this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
|
|
615
|
+
this.ctx.markDirty();
|
|
616
|
+
this.ctx.emit('note:attachment:deleted', { projectId: this.ctx.projectId, noteId, filename: safe });
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
syncAttachments(noteId) {
|
|
620
|
+
const dir = this.notesDir;
|
|
621
|
+
if (!dir)
|
|
622
|
+
return;
|
|
623
|
+
if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
|
|
624
|
+
return;
|
|
625
|
+
const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
|
|
626
|
+
this._graph.setNodeAttribute(noteId, 'attachments', attachments);
|
|
627
|
+
this.ctx.markDirty();
|
|
628
|
+
}
|
|
629
|
+
listAttachments(noteId) {
|
|
630
|
+
if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
|
|
631
|
+
return [];
|
|
632
|
+
return this._graph.getNodeAttribute(noteId, 'attachments') ?? [];
|
|
633
|
+
}
|
|
634
|
+
getAttachmentPath(noteId, filename) {
|
|
635
|
+
const dir = this.notesDir;
|
|
636
|
+
if (!dir)
|
|
637
|
+
return null;
|
|
638
|
+
return (0, file_mirror_1.getAttachmentPath)(dir, noteId, filename);
|
|
639
|
+
}
|
|
640
|
+
// -- Import from file (reverse mirror — does NOT write back to file) --
|
|
641
|
+
async importFromFile(parsed) {
|
|
642
|
+
const exists = this._graph.hasNode(parsed.id) && !isProxy(this._graph, parsed.id);
|
|
643
|
+
const embedding = await this.embedFns.document(`${parsed.title} ${parsed.content}`);
|
|
644
|
+
const now = Date.now();
|
|
645
|
+
if (exists) {
|
|
646
|
+
const existing = this._graph.getNodeAttributes(parsed.id);
|
|
647
|
+
this._graph.mergeNodeAttributes(parsed.id, {
|
|
648
|
+
title: parsed.title,
|
|
649
|
+
content: parsed.content,
|
|
650
|
+
tags: parsed.tags,
|
|
651
|
+
embedding,
|
|
652
|
+
attachments: parsed.attachments,
|
|
653
|
+
updatedAt: now,
|
|
654
|
+
createdAt: existing.createdAt,
|
|
655
|
+
version: parsed.version ?? existing.version + 1,
|
|
656
|
+
// preserve createdBy from graph if file doesn't have it
|
|
657
|
+
...(parsed.createdBy != null ? { createdBy: parsed.createdBy } : {}),
|
|
658
|
+
...(parsed.updatedBy != null ? { updatedBy: parsed.updatedBy } : {}),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
this._graph.addNode(parsed.id, {
|
|
663
|
+
title: parsed.title,
|
|
664
|
+
content: parsed.content,
|
|
665
|
+
tags: parsed.tags,
|
|
666
|
+
embedding,
|
|
667
|
+
attachments: parsed.attachments ?? [],
|
|
668
|
+
createdAt: parsed.createdAt ?? now,
|
|
669
|
+
updatedAt: now,
|
|
670
|
+
version: parsed.version ?? 1,
|
|
671
|
+
createdBy: parsed.createdBy ?? undefined,
|
|
672
|
+
updatedBy: parsed.updatedBy ?? undefined,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
this._bm25Index.updateDocument(parsed.id, this._graph.getNodeAttributes(parsed.id));
|
|
676
|
+
// Sync relations
|
|
677
|
+
this.syncRelationsFromFile(parsed.id, parsed.relations);
|
|
678
|
+
this.ctx.markDirty();
|
|
679
|
+
this.ctx.emit(exists ? 'note:updated' : 'note:created', { projectId: this.ctx.projectId, noteId: parsed.id });
|
|
680
|
+
}
|
|
681
|
+
updateContentFromFile(noteId, content) {
|
|
682
|
+
if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
|
|
683
|
+
return;
|
|
684
|
+
this._graph.setNodeAttribute(noteId, 'content', content);
|
|
685
|
+
this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
|
|
686
|
+
this._graph.setNodeAttribute(noteId, 'version', (this._graph.getNodeAttribute(noteId, 'version') ?? 0) + 1);
|
|
687
|
+
this.ctx.markDirty();
|
|
688
|
+
this.ctx.emit('note:updated', { projectId: this.ctx.projectId, noteId });
|
|
689
|
+
}
|
|
690
|
+
deleteFromFile(noteId) {
|
|
691
|
+
if (!this._graph.hasNode(noteId))
|
|
692
|
+
return;
|
|
693
|
+
if (isProxy(this._graph, noteId))
|
|
694
|
+
return;
|
|
695
|
+
this._bm25Index.removeDocument(noteId);
|
|
696
|
+
deleteNote(this._graph, noteId);
|
|
697
|
+
if (this.taskGraph) {
|
|
698
|
+
const pId = `@knowledge::${noteId}`;
|
|
699
|
+
if (this.taskGraph.hasNode(pId))
|
|
700
|
+
this.taskGraph.dropNode(pId);
|
|
701
|
+
}
|
|
702
|
+
this.ctx.markDirty();
|
|
703
|
+
this.ctx.emit('note:deleted', { projectId: this.ctx.projectId, noteId });
|
|
704
|
+
}
|
|
705
|
+
syncRelationsFromFile(noteId, desired) {
|
|
706
|
+
// Build current outgoing relations from graph
|
|
707
|
+
const current = [];
|
|
708
|
+
this._graph.forEachOutEdge(noteId, (_edge, attrs, _src, target) => {
|
|
709
|
+
const proxy = this._graph.hasNode(target) ? this._graph.getNodeAttribute(target, 'proxyFor') : undefined;
|
|
710
|
+
if (proxy) {
|
|
711
|
+
current.push({ to: proxy.nodeId, kind: attrs.kind, graph: proxy.graph });
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
current.push({ to: target, kind: attrs.kind });
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
const diff = (0, file_import_1.diffRelations)(current, desired);
|
|
718
|
+
for (const rel of diff.toRemove) {
|
|
719
|
+
if (rel.graph) {
|
|
720
|
+
deleteCrossRelation(this._graph, noteId, rel.graph, rel.to);
|
|
721
|
+
if (rel.graph === 'tasks' && this.taskGraph) {
|
|
722
|
+
deleteMirrorFromTaskGraph(this.taskGraph, noteId, rel.to);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
deleteRelation(this._graph, noteId, rel.to);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
for (const rel of diff.toAdd) {
|
|
730
|
+
if (rel.graph) {
|
|
731
|
+
const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, rel.graph);
|
|
732
|
+
createCrossRelation(this._graph, noteId, rel.graph, rel.to, rel.kind, extGraph);
|
|
733
|
+
if (rel.graph === 'tasks' && this.taskGraph) {
|
|
734
|
+
createMirrorInTaskGraph(this.taskGraph, noteId, rel.to, rel.kind);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
createRelation(this._graph, noteId, rel.to, rel.kind);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// -- Read --
|
|
743
|
+
getNote(noteId) {
|
|
744
|
+
const note = getNote(this._graph, noteId);
|
|
745
|
+
if (!note)
|
|
746
|
+
return null;
|
|
747
|
+
const relations = listRelations(this._graph, noteId, this.ext);
|
|
748
|
+
return { ...note, relations };
|
|
749
|
+
}
|
|
750
|
+
listNotes(filter, tag, limit) {
|
|
751
|
+
return listNotes(this._graph, filter, tag, limit);
|
|
752
|
+
}
|
|
753
|
+
async searchNotes(query, opts) {
|
|
754
|
+
const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
|
|
755
|
+
return (0, knowledge_1.searchKnowledge)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
|
|
756
|
+
}
|
|
757
|
+
listRelations(noteId) {
|
|
758
|
+
return listRelations(this._graph, noteId, this.ext);
|
|
759
|
+
}
|
|
760
|
+
findLinkedNotes(targetGraph, targetNodeId, kind, projectId) {
|
|
761
|
+
return findLinkedNotes(this._graph, targetGraph, targetNodeId, kind, projectId || this.ctx.projectId);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
exports.KnowledgeGraphManager = KnowledgeGraphManager;
|