@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,1013 @@
|
|
|
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.SkillGraphManager = exports.createSkillGraph = void 0;
|
|
7
|
+
exports.proxyId = proxyId;
|
|
8
|
+
exports.isProxy = isProxy;
|
|
9
|
+
exports.cleanupProxies = cleanupProxies;
|
|
10
|
+
exports.createSkill = createSkill;
|
|
11
|
+
exports.updateSkill = updateSkill;
|
|
12
|
+
exports.bumpUsage = bumpUsage;
|
|
13
|
+
exports.deleteSkill = deleteSkill;
|
|
14
|
+
exports.getSkill = getSkill;
|
|
15
|
+
exports.listSkills = listSkills;
|
|
16
|
+
exports.createSkillRelation = createSkillRelation;
|
|
17
|
+
exports.deleteSkillRelation = deleteSkillRelation;
|
|
18
|
+
exports.listSkillRelations = listSkillRelations;
|
|
19
|
+
exports.findLinkedSkills = findLinkedSkills;
|
|
20
|
+
exports.createCrossRelation = createCrossRelation;
|
|
21
|
+
exports.deleteCrossRelation = deleteCrossRelation;
|
|
22
|
+
exports.saveSkillGraph = saveSkillGraph;
|
|
23
|
+
exports.loadSkillGraph = loadSkillGraph;
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const skill_types_1 = require("../graphs/skill-types");
|
|
27
|
+
Object.defineProperty(exports, "createSkillGraph", { enumerable: true, get: function () { return skill_types_1.createSkillGraph; } });
|
|
28
|
+
const knowledge_types_1 = require("../graphs/knowledge-types");
|
|
29
|
+
const manager_types_1 = require("../graphs/manager-types");
|
|
30
|
+
const skills_1 = require("../lib/search/skills");
|
|
31
|
+
const bm25_1 = require("../lib/search/bm25");
|
|
32
|
+
const file_mirror_1 = require("../lib/file-mirror");
|
|
33
|
+
const attachment_types_1 = require("../graphs/attachment-types");
|
|
34
|
+
const file_import_1 = require("../lib/file-import");
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Proxy helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/** Build the proxy node ID. With projectId: `@docs::frontend::guide.md::Setup`, without: `@docs::guide.md::Setup` */
|
|
39
|
+
function proxyId(targetGraph, nodeId, projectId) {
|
|
40
|
+
return projectId ? `@${targetGraph}::${projectId}::${nodeId}` : `@${targetGraph}::${nodeId}`;
|
|
41
|
+
}
|
|
42
|
+
/** Check whether a node is a cross-graph proxy. */
|
|
43
|
+
function isProxy(graph, nodeId) {
|
|
44
|
+
if (!graph.hasNode(nodeId))
|
|
45
|
+
return false;
|
|
46
|
+
return graph.getNodeAttribute(nodeId, 'proxyFor') !== undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Ensure a proxy node exists for the given external target. Returns its ID. */
|
|
49
|
+
function ensureProxyNode(graph, targetGraph, nodeId, projectId) {
|
|
50
|
+
const id = proxyId(targetGraph, nodeId, projectId);
|
|
51
|
+
if (!graph.hasNode(id)) {
|
|
52
|
+
graph.addNode(id, {
|
|
53
|
+
title: '',
|
|
54
|
+
description: '',
|
|
55
|
+
steps: [],
|
|
56
|
+
triggers: [],
|
|
57
|
+
inputHints: [],
|
|
58
|
+
filePatterns: [],
|
|
59
|
+
tags: [],
|
|
60
|
+
source: 'user',
|
|
61
|
+
confidence: 1,
|
|
62
|
+
usageCount: 0,
|
|
63
|
+
lastUsedAt: null,
|
|
64
|
+
embedding: [],
|
|
65
|
+
attachments: [],
|
|
66
|
+
createdAt: 0,
|
|
67
|
+
updatedAt: 0,
|
|
68
|
+
version: 0,
|
|
69
|
+
proxyFor: { graph: targetGraph, nodeId, projectId },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
/** Remove a proxy node if it has zero incident edges. */
|
|
75
|
+
function cleanupProxy(graph, nodeId) {
|
|
76
|
+
if (!graph.hasNode(nodeId))
|
|
77
|
+
return;
|
|
78
|
+
if (!isProxy(graph, nodeId))
|
|
79
|
+
return;
|
|
80
|
+
if (graph.degree(nodeId) === 0) {
|
|
81
|
+
graph.dropNode(nodeId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Remove all proxy nodes whose target no longer exists in the external graph.
|
|
86
|
+
* Called after doc/code/file removal in the indexer.
|
|
87
|
+
*/
|
|
88
|
+
function cleanupProxies(graph, targetGraph, externalGraph) {
|
|
89
|
+
const toRemove = [];
|
|
90
|
+
graph.forEachNode((id, attrs) => {
|
|
91
|
+
if (attrs.proxyFor && attrs.proxyFor.graph === targetGraph) {
|
|
92
|
+
if (!externalGraph.hasNode(attrs.proxyFor.nodeId)) {
|
|
93
|
+
toRemove.push(id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
for (const id of toRemove) {
|
|
98
|
+
graph.dropNode(id); // also drops incident edges
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// CRUD — Skills
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
/** Create a skill, return its slug ID. */
|
|
105
|
+
function createSkill(graph, title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence, embedding, author = '') {
|
|
106
|
+
const id = (0, knowledge_types_1.slugify)(title, graph);
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
graph.addNode(id, {
|
|
109
|
+
title,
|
|
110
|
+
description,
|
|
111
|
+
steps,
|
|
112
|
+
triggers,
|
|
113
|
+
inputHints,
|
|
114
|
+
filePatterns,
|
|
115
|
+
tags,
|
|
116
|
+
source,
|
|
117
|
+
confidence,
|
|
118
|
+
usageCount: 0,
|
|
119
|
+
lastUsedAt: null,
|
|
120
|
+
embedding,
|
|
121
|
+
attachments: [],
|
|
122
|
+
createdAt: now,
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
version: 1,
|
|
125
|
+
createdBy: author || undefined,
|
|
126
|
+
updatedBy: author || undefined,
|
|
127
|
+
});
|
|
128
|
+
return id;
|
|
129
|
+
}
|
|
130
|
+
/** Partial update of a skill. Returns true if found and updated. Throws VersionConflictError if expectedVersion is provided and doesn't match. */
|
|
131
|
+
function updateSkill(graph, skillId, patch, embedding, author = '', expectedVersion) {
|
|
132
|
+
if (!graph.hasNode(skillId))
|
|
133
|
+
return false;
|
|
134
|
+
if (isProxy(graph, skillId))
|
|
135
|
+
return false;
|
|
136
|
+
if (expectedVersion !== undefined) {
|
|
137
|
+
const current = graph.getNodeAttribute(skillId, 'version');
|
|
138
|
+
if (current !== expectedVersion)
|
|
139
|
+
throw new manager_types_1.VersionConflictError(current, expectedVersion);
|
|
140
|
+
}
|
|
141
|
+
if (patch.title !== undefined)
|
|
142
|
+
graph.setNodeAttribute(skillId, 'title', patch.title);
|
|
143
|
+
if (patch.description !== undefined)
|
|
144
|
+
graph.setNodeAttribute(skillId, 'description', patch.description);
|
|
145
|
+
if (patch.steps !== undefined)
|
|
146
|
+
graph.setNodeAttribute(skillId, 'steps', patch.steps);
|
|
147
|
+
if (patch.triggers !== undefined)
|
|
148
|
+
graph.setNodeAttribute(skillId, 'triggers', patch.triggers);
|
|
149
|
+
if (patch.inputHints !== undefined)
|
|
150
|
+
graph.setNodeAttribute(skillId, 'inputHints', patch.inputHints);
|
|
151
|
+
if (patch.filePatterns !== undefined)
|
|
152
|
+
graph.setNodeAttribute(skillId, 'filePatterns', patch.filePatterns);
|
|
153
|
+
if (patch.tags !== undefined)
|
|
154
|
+
graph.setNodeAttribute(skillId, 'tags', patch.tags);
|
|
155
|
+
if (patch.source !== undefined)
|
|
156
|
+
graph.setNodeAttribute(skillId, 'source', patch.source);
|
|
157
|
+
if (patch.confidence !== undefined)
|
|
158
|
+
graph.setNodeAttribute(skillId, 'confidence', patch.confidence);
|
|
159
|
+
if (embedding !== undefined)
|
|
160
|
+
graph.setNodeAttribute(skillId, 'embedding', embedding);
|
|
161
|
+
if (author)
|
|
162
|
+
graph.setNodeAttribute(skillId, 'updatedBy', author);
|
|
163
|
+
graph.setNodeAttribute(skillId, 'version', graph.getNodeAttribute(skillId, 'version') + 1);
|
|
164
|
+
graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
/** Increment usageCount and set lastUsedAt. Returns true if found. */
|
|
168
|
+
function bumpUsage(graph, skillId) {
|
|
169
|
+
if (!graph.hasNode(skillId))
|
|
170
|
+
return false;
|
|
171
|
+
if (isProxy(graph, skillId))
|
|
172
|
+
return false;
|
|
173
|
+
const count = graph.getNodeAttribute(skillId, 'usageCount');
|
|
174
|
+
graph.setNodeAttribute(skillId, 'usageCount', count + 1);
|
|
175
|
+
graph.setNodeAttribute(skillId, 'lastUsedAt', Date.now());
|
|
176
|
+
graph.setNodeAttribute(skillId, 'version', graph.getNodeAttribute(skillId, 'version') + 1);
|
|
177
|
+
graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
/** Delete a skill and all its incident edges. Also cleans up orphaned proxy nodes. */
|
|
181
|
+
function deleteSkill(graph, skillId) {
|
|
182
|
+
if (!graph.hasNode(skillId))
|
|
183
|
+
return false;
|
|
184
|
+
if (isProxy(graph, skillId))
|
|
185
|
+
return false;
|
|
186
|
+
const proxyNeighbors = [];
|
|
187
|
+
graph.forEachNeighbor(skillId, (neighbor) => {
|
|
188
|
+
if (isProxy(graph, neighbor))
|
|
189
|
+
proxyNeighbors.push(neighbor);
|
|
190
|
+
});
|
|
191
|
+
graph.dropNode(skillId);
|
|
192
|
+
for (const p of proxyNeighbors) {
|
|
193
|
+
cleanupProxy(graph, p);
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
/** Get a skill by ID, or null if not found. Excludes proxy nodes. */
|
|
198
|
+
function getSkill(graph, skillId) {
|
|
199
|
+
if (!graph.hasNode(skillId))
|
|
200
|
+
return null;
|
|
201
|
+
if (isProxy(graph, skillId))
|
|
202
|
+
return null;
|
|
203
|
+
const attrs = graph.getNodeAttributes(skillId);
|
|
204
|
+
const dependsOn = [];
|
|
205
|
+
const dependedBy = [];
|
|
206
|
+
const related = [];
|
|
207
|
+
const variants = [];
|
|
208
|
+
const crossLinks = [];
|
|
209
|
+
// Incoming edges
|
|
210
|
+
graph.forEachInEdge(skillId, (_edge, edgeAttrs, source) => {
|
|
211
|
+
if (isProxy(graph, source)) {
|
|
212
|
+
const proxyFor = graph.getNodeAttribute(source, 'proxyFor');
|
|
213
|
+
if (proxyFor) {
|
|
214
|
+
crossLinks.push({
|
|
215
|
+
nodeId: proxyFor.nodeId,
|
|
216
|
+
targetGraph: proxyFor.graph,
|
|
217
|
+
kind: edgeAttrs.kind,
|
|
218
|
+
direction: 'incoming',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const srcAttrs = graph.getNodeAttributes(source);
|
|
224
|
+
if (edgeAttrs.kind === 'depends_on') {
|
|
225
|
+
// source depends_on this skill → this skill is depended by source
|
|
226
|
+
dependedBy.push({ id: source, title: srcAttrs.title });
|
|
227
|
+
}
|
|
228
|
+
else if (edgeAttrs.kind === 'related_to') {
|
|
229
|
+
related.push({ id: source, title: srcAttrs.title });
|
|
230
|
+
}
|
|
231
|
+
else if (edgeAttrs.kind === 'variant_of') {
|
|
232
|
+
variants.push({ id: source, title: srcAttrs.title });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// Outgoing edges
|
|
236
|
+
graph.forEachOutEdge(skillId, (_edge, edgeAttrs, _source, target) => {
|
|
237
|
+
if (isProxy(graph, target)) {
|
|
238
|
+
const proxyFor = graph.getNodeAttribute(target, 'proxyFor');
|
|
239
|
+
if (proxyFor) {
|
|
240
|
+
crossLinks.push({
|
|
241
|
+
nodeId: proxyFor.nodeId,
|
|
242
|
+
targetGraph: proxyFor.graph,
|
|
243
|
+
kind: edgeAttrs.kind,
|
|
244
|
+
direction: 'outgoing',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const tgtAttrs = graph.getNodeAttributes(target);
|
|
250
|
+
if (edgeAttrs.kind === 'depends_on') {
|
|
251
|
+
dependsOn.push({ id: target, title: tgtAttrs.title });
|
|
252
|
+
}
|
|
253
|
+
else if (edgeAttrs.kind === 'related_to') {
|
|
254
|
+
if (!related.some(r => r.id === target)) {
|
|
255
|
+
related.push({ id: target, title: tgtAttrs.title });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (edgeAttrs.kind === 'variant_of') {
|
|
259
|
+
if (!variants.some(v => v.id === target)) {
|
|
260
|
+
variants.push({ id: target, title: tgtAttrs.title });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
id: skillId,
|
|
266
|
+
title: attrs.title,
|
|
267
|
+
description: attrs.description,
|
|
268
|
+
steps: attrs.steps,
|
|
269
|
+
triggers: attrs.triggers,
|
|
270
|
+
inputHints: attrs.inputHints,
|
|
271
|
+
filePatterns: attrs.filePatterns,
|
|
272
|
+
tags: attrs.tags,
|
|
273
|
+
source: attrs.source,
|
|
274
|
+
confidence: attrs.confidence,
|
|
275
|
+
usageCount: attrs.usageCount,
|
|
276
|
+
lastUsedAt: attrs.lastUsedAt,
|
|
277
|
+
version: attrs.version,
|
|
278
|
+
createdAt: attrs.createdAt,
|
|
279
|
+
updatedAt: attrs.updatedAt,
|
|
280
|
+
attachments: attrs.attachments ?? [],
|
|
281
|
+
dependsOn,
|
|
282
|
+
dependedBy,
|
|
283
|
+
related,
|
|
284
|
+
variants,
|
|
285
|
+
crossLinks,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/** List skills with optional filters. Excludes proxy nodes. */
|
|
289
|
+
function listSkills(graph, opts = {}) {
|
|
290
|
+
const { source, tag, filter, limit = 50 } = opts;
|
|
291
|
+
const lowerFilter = filter?.toLowerCase();
|
|
292
|
+
const lowerTag = tag?.toLowerCase();
|
|
293
|
+
const results = [];
|
|
294
|
+
graph.forEachNode((id, attrs) => {
|
|
295
|
+
if (attrs.proxyFor)
|
|
296
|
+
return;
|
|
297
|
+
if (source && attrs.source !== source)
|
|
298
|
+
return;
|
|
299
|
+
if (lowerTag && !attrs.tags.some(t => t.toLowerCase() === lowerTag))
|
|
300
|
+
return;
|
|
301
|
+
if (lowerFilter) {
|
|
302
|
+
const match = id.toLowerCase().includes(lowerFilter) ||
|
|
303
|
+
attrs.title.toLowerCase().includes(lowerFilter);
|
|
304
|
+
if (!match)
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
results.push({
|
|
308
|
+
id,
|
|
309
|
+
title: attrs.title,
|
|
310
|
+
description: attrs.description?.slice(0, 500),
|
|
311
|
+
steps: attrs.steps,
|
|
312
|
+
triggers: attrs.triggers,
|
|
313
|
+
inputHints: attrs.inputHints,
|
|
314
|
+
filePatterns: attrs.filePatterns,
|
|
315
|
+
tags: attrs.tags,
|
|
316
|
+
source: attrs.source,
|
|
317
|
+
confidence: attrs.confidence,
|
|
318
|
+
usageCount: attrs.usageCount,
|
|
319
|
+
lastUsedAt: attrs.lastUsedAt,
|
|
320
|
+
version: attrs.version,
|
|
321
|
+
createdAt: attrs.createdAt,
|
|
322
|
+
updatedAt: attrs.updatedAt,
|
|
323
|
+
attachments: attrs.attachments ?? [],
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
return results
|
|
327
|
+
.sort((a, b) => {
|
|
328
|
+
// Sort by usageCount desc, then updatedAt desc
|
|
329
|
+
if (a.usageCount !== b.usageCount)
|
|
330
|
+
return b.usageCount - a.usageCount;
|
|
331
|
+
return b.updatedAt - a.updatedAt;
|
|
332
|
+
})
|
|
333
|
+
.slice(0, limit);
|
|
334
|
+
}
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// CRUD — Skill Relations (skill ↔ skill)
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
/** Create a directed relation between two skills. Returns true if created. */
|
|
339
|
+
function createSkillRelation(graph, fromId, toId, kind) {
|
|
340
|
+
if (!graph.hasNode(fromId) || !graph.hasNode(toId))
|
|
341
|
+
return false;
|
|
342
|
+
if (isProxy(graph, fromId) || isProxy(graph, toId))
|
|
343
|
+
return false;
|
|
344
|
+
if (graph.hasEdge(fromId, toId))
|
|
345
|
+
return false;
|
|
346
|
+
graph.addEdgeWithKey(`${fromId}→${toId}`, fromId, toId, { kind });
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
/** Delete a skill relation. Returns true if it existed. */
|
|
350
|
+
function deleteSkillRelation(graph, fromId, toId) {
|
|
351
|
+
if (!graph.hasEdge(fromId, toId))
|
|
352
|
+
return false;
|
|
353
|
+
graph.dropEdge(fromId, toId);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
/** List all relations for a skill (both incoming and outgoing). Resolves proxy IDs and titles. */
|
|
357
|
+
function listSkillRelations(graph, skillId, externalGraphs) {
|
|
358
|
+
if (!graph.hasNode(skillId))
|
|
359
|
+
return [];
|
|
360
|
+
const results = [];
|
|
361
|
+
function resolveTitle(nodeId, targetGraph) {
|
|
362
|
+
if (!targetGraph) {
|
|
363
|
+
if (graph.hasNode(nodeId) && !isProxy(graph, nodeId)) {
|
|
364
|
+
return graph.getNodeAttribute(nodeId, 'title') || undefined;
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
if (!externalGraphs)
|
|
369
|
+
return undefined;
|
|
370
|
+
const extGraph = (0, manager_types_1.resolveExternalGraph)(externalGraphs, targetGraph);
|
|
371
|
+
if (!extGraph || !extGraph.hasNode(nodeId))
|
|
372
|
+
return undefined;
|
|
373
|
+
const attrs = extGraph.getNodeAttributes(nodeId);
|
|
374
|
+
return attrs.title || attrs.name || undefined;
|
|
375
|
+
}
|
|
376
|
+
function resolveEntry(source, target, kind) {
|
|
377
|
+
const sourceProxy = graph.hasNode(source) ? graph.getNodeAttribute(source, 'proxyFor') : undefined;
|
|
378
|
+
const targetProxy = graph.hasNode(target) ? graph.getNodeAttribute(target, 'proxyFor') : undefined;
|
|
379
|
+
if (targetProxy) {
|
|
380
|
+
const title = resolveTitle(targetProxy.nodeId, targetProxy.graph);
|
|
381
|
+
return { fromId: source, toId: targetProxy.nodeId, kind, targetGraph: targetProxy.graph, ...(title ? { title } : {}) };
|
|
382
|
+
}
|
|
383
|
+
if (sourceProxy) {
|
|
384
|
+
const title = resolveTitle(sourceProxy.nodeId, sourceProxy.graph);
|
|
385
|
+
return { fromId: sourceProxy.nodeId, toId: target, kind, targetGraph: sourceProxy.graph, ...(title ? { title } : {}) };
|
|
386
|
+
}
|
|
387
|
+
const otherId = source === skillId ? target : source;
|
|
388
|
+
const title = resolveTitle(otherId);
|
|
389
|
+
return { fromId: source, toId: target, kind, ...(title ? { title } : {}) };
|
|
390
|
+
}
|
|
391
|
+
graph.forEachOutEdge(skillId, (_edge, attrs, source, target) => {
|
|
392
|
+
results.push(resolveEntry(source, target, attrs.kind));
|
|
393
|
+
});
|
|
394
|
+
graph.forEachInEdge(skillId, (_edge, attrs, source, target) => {
|
|
395
|
+
results.push(resolveEntry(source, target, attrs.kind));
|
|
396
|
+
});
|
|
397
|
+
return results;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Find all skills that have a cross-graph relation to the given target node.
|
|
401
|
+
* Optionally filter by relation kind.
|
|
402
|
+
*/
|
|
403
|
+
function findLinkedSkills(graph, targetGraph, targetNodeId, kind, projectId) {
|
|
404
|
+
const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
|
|
405
|
+
if (projectId)
|
|
406
|
+
candidates.push(proxyId(targetGraph, targetNodeId));
|
|
407
|
+
const results = [];
|
|
408
|
+
const seen = new Set();
|
|
409
|
+
for (const pId of candidates) {
|
|
410
|
+
if (!graph.hasNode(pId))
|
|
411
|
+
continue;
|
|
412
|
+
graph.forEachInEdge(pId, (_edge, attrs, source) => {
|
|
413
|
+
if (seen.has(source))
|
|
414
|
+
return;
|
|
415
|
+
if (isProxy(graph, source))
|
|
416
|
+
return;
|
|
417
|
+
if (kind && attrs.kind !== kind)
|
|
418
|
+
return;
|
|
419
|
+
const skillAttrs = graph.getNodeAttributes(source);
|
|
420
|
+
seen.add(source);
|
|
421
|
+
results.push({
|
|
422
|
+
skillId: source,
|
|
423
|
+
title: skillAttrs.title,
|
|
424
|
+
kind: attrs.kind,
|
|
425
|
+
source: skillAttrs.source,
|
|
426
|
+
confidence: skillAttrs.confidence,
|
|
427
|
+
tags: skillAttrs.tags,
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return results;
|
|
432
|
+
}
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Cross-graph relations (skill → doc/code/file/knowledge/task node)
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
/**
|
|
437
|
+
* Create a cross-graph relation from a skill to a node in an external graph.
|
|
438
|
+
* Optionally validates that the target exists in the external graph.
|
|
439
|
+
*/
|
|
440
|
+
function createCrossRelation(graph, fromSkillId, targetGraph, targetNodeId, kind, externalGraph, projectId) {
|
|
441
|
+
if (!graph.hasNode(fromSkillId) || isProxy(graph, fromSkillId))
|
|
442
|
+
return false;
|
|
443
|
+
if (externalGraph && !externalGraph.hasNode(targetNodeId))
|
|
444
|
+
return false;
|
|
445
|
+
const pId = ensureProxyNode(graph, targetGraph, targetNodeId, projectId);
|
|
446
|
+
if (graph.hasEdge(fromSkillId, pId))
|
|
447
|
+
return false;
|
|
448
|
+
graph.addEdgeWithKey(`${fromSkillId}→${pId}`, fromSkillId, pId, { kind });
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Delete a cross-graph relation. Cleans up orphaned proxy node.
|
|
453
|
+
*/
|
|
454
|
+
function deleteCrossRelation(graph, fromSkillId, targetGraph, targetNodeId, projectId) {
|
|
455
|
+
const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
|
|
456
|
+
if (projectId)
|
|
457
|
+
candidates.push(proxyId(targetGraph, targetNodeId));
|
|
458
|
+
for (const pId of candidates) {
|
|
459
|
+
if (graph.hasEdge(fromSkillId, pId)) {
|
|
460
|
+
graph.dropEdge(fromSkillId, pId);
|
|
461
|
+
cleanupProxy(graph, pId);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Persistence
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
function saveSkillGraph(graph, graphMemory, embeddingFingerprint) {
|
|
471
|
+
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
472
|
+
const file = path_1.default.join(graphMemory, 'skills.json');
|
|
473
|
+
const tmp = file + '.tmp';
|
|
474
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
|
|
475
|
+
fs_1.default.renameSync(tmp, file);
|
|
476
|
+
}
|
|
477
|
+
function loadSkillGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
478
|
+
const graph = (0, skill_types_1.createSkillGraph)();
|
|
479
|
+
if (fresh)
|
|
480
|
+
return graph;
|
|
481
|
+
const file = path_1.default.join(graphMemory, 'skills.json');
|
|
482
|
+
if (!fs_1.default.existsSync(file))
|
|
483
|
+
return graph;
|
|
484
|
+
try {
|
|
485
|
+
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
486
|
+
const stored = data.embeddingModel;
|
|
487
|
+
if (embeddingFingerprint && stored !== embeddingFingerprint) {
|
|
488
|
+
process.stderr.write(`[skill-graph] Embedding config changed, re-indexing skill graph\n`);
|
|
489
|
+
return graph;
|
|
490
|
+
}
|
|
491
|
+
graph.import(data.graph);
|
|
492
|
+
process.stderr.write(`[skill-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
process.stderr.write(`[skill-graph] Failed to load graph, starting fresh: ${err}\n`);
|
|
496
|
+
}
|
|
497
|
+
return graph;
|
|
498
|
+
}
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Bidirectional mirror helpers (Skill ↔ Knowledge, Skill ↔ Task)
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
/**
|
|
503
|
+
* Create a mirror proxy in KnowledgeGraph when a skill links to a note.
|
|
504
|
+
*/
|
|
505
|
+
function createMirrorInKnowledgeGraph(knowledgeGraph, skillId, noteId, kind) {
|
|
506
|
+
const mirrorProxyId = `@skills::${skillId}`;
|
|
507
|
+
if (!knowledgeGraph.hasNode(mirrorProxyId)) {
|
|
508
|
+
knowledgeGraph.addNode(mirrorProxyId, {
|
|
509
|
+
title: '',
|
|
510
|
+
content: '',
|
|
511
|
+
tags: [],
|
|
512
|
+
embedding: [],
|
|
513
|
+
attachments: [],
|
|
514
|
+
createdAt: 0,
|
|
515
|
+
updatedAt: 0,
|
|
516
|
+
version: 0,
|
|
517
|
+
proxyFor: { graph: 'skills', nodeId: skillId },
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (!knowledgeGraph.hasNode(noteId))
|
|
521
|
+
return;
|
|
522
|
+
const edgeKey = `${mirrorProxyId}→${noteId}`;
|
|
523
|
+
if (!knowledgeGraph.hasEdge(edgeKey)) {
|
|
524
|
+
knowledgeGraph.addEdgeWithKey(edgeKey, mirrorProxyId, noteId, { kind });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function deleteMirrorFromKnowledgeGraph(knowledgeGraph, skillId, noteId) {
|
|
528
|
+
const mirrorProxyId = `@skills::${skillId}`;
|
|
529
|
+
const edgeKey = `${mirrorProxyId}→${noteId}`;
|
|
530
|
+
if (knowledgeGraph.hasEdge(edgeKey)) {
|
|
531
|
+
knowledgeGraph.dropEdge(edgeKey);
|
|
532
|
+
}
|
|
533
|
+
if (knowledgeGraph.hasNode(mirrorProxyId)) {
|
|
534
|
+
const proxyFor = knowledgeGraph.getNodeAttribute(mirrorProxyId, 'proxyFor');
|
|
535
|
+
if (proxyFor && knowledgeGraph.degree(mirrorProxyId) === 0) {
|
|
536
|
+
knowledgeGraph.dropNode(mirrorProxyId);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Create a mirror proxy in TaskGraph when a skill links to a task.
|
|
542
|
+
*/
|
|
543
|
+
function createMirrorInTaskGraph(taskGraph, skillId, taskId, kind) {
|
|
544
|
+
const mirrorProxyId = `@skills::${skillId}`;
|
|
545
|
+
if (!taskGraph.hasNode(mirrorProxyId)) {
|
|
546
|
+
taskGraph.addNode(mirrorProxyId, {
|
|
547
|
+
title: '',
|
|
548
|
+
description: '',
|
|
549
|
+
status: 'backlog',
|
|
550
|
+
priority: 'low',
|
|
551
|
+
tags: [],
|
|
552
|
+
dueDate: null,
|
|
553
|
+
estimate: null,
|
|
554
|
+
completedAt: null,
|
|
555
|
+
embedding: [],
|
|
556
|
+
attachments: [],
|
|
557
|
+
createdAt: 0,
|
|
558
|
+
updatedAt: 0,
|
|
559
|
+
version: 0,
|
|
560
|
+
proxyFor: { graph: 'skills', nodeId: skillId },
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (!taskGraph.hasNode(taskId))
|
|
564
|
+
return;
|
|
565
|
+
const edgeKey = `${mirrorProxyId}→${taskId}`;
|
|
566
|
+
if (!taskGraph.hasEdge(edgeKey)) {
|
|
567
|
+
taskGraph.addEdgeWithKey(edgeKey, mirrorProxyId, taskId, { kind });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function deleteMirrorFromTaskGraph(taskGraph, skillId, taskId) {
|
|
571
|
+
const mirrorProxyId = `@skills::${skillId}`;
|
|
572
|
+
const edgeKey = `${mirrorProxyId}→${taskId}`;
|
|
573
|
+
if (taskGraph.hasEdge(edgeKey)) {
|
|
574
|
+
taskGraph.dropEdge(edgeKey);
|
|
575
|
+
}
|
|
576
|
+
if (taskGraph.hasNode(mirrorProxyId)) {
|
|
577
|
+
const proxyFor = taskGraph.getNodeAttribute(mirrorProxyId, 'proxyFor');
|
|
578
|
+
if (proxyFor && taskGraph.degree(mirrorProxyId) === 0) {
|
|
579
|
+
taskGraph.dropNode(mirrorProxyId);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
// SkillGraphManager — unified API for skill graph operations
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
class SkillGraphManager {
|
|
587
|
+
_graph;
|
|
588
|
+
embedFns;
|
|
589
|
+
ctx;
|
|
590
|
+
ext;
|
|
591
|
+
knowledgeGraph;
|
|
592
|
+
taskGraph;
|
|
593
|
+
mirrorTracker;
|
|
594
|
+
_bm25Index;
|
|
595
|
+
constructor(_graph, embedFns, ctx, ext = {}) {
|
|
596
|
+
this._graph = _graph;
|
|
597
|
+
this.embedFns = embedFns;
|
|
598
|
+
this.ctx = ctx;
|
|
599
|
+
this.ext = ext;
|
|
600
|
+
this.knowledgeGraph = ext.knowledgeGraph;
|
|
601
|
+
this.taskGraph = ext.taskGraph;
|
|
602
|
+
this._bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.title} ${attrs.description} ${attrs.triggers.join(' ')} ${attrs.tags.join(' ')}`);
|
|
603
|
+
this._graph.forEachNode((id, attrs) => {
|
|
604
|
+
if (!attrs.proxyFor)
|
|
605
|
+
this._bm25Index.addDocument(id, attrs);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
get graph() { return this._graph; }
|
|
609
|
+
get bm25Index() { return this._bm25Index; }
|
|
610
|
+
rebuildBm25Index() {
|
|
611
|
+
this._bm25Index.clear();
|
|
612
|
+
this._graph.forEachNode((id, attrs) => {
|
|
613
|
+
if (!attrs.proxyFor)
|
|
614
|
+
this._bm25Index.addDocument(id, attrs);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
setMirrorTracker(tracker) {
|
|
618
|
+
this.mirrorTracker = tracker;
|
|
619
|
+
}
|
|
620
|
+
/** Returns updatedAt for a node, or null if not found. Used by startup scan. */
|
|
621
|
+
getNodeUpdatedAt(skillId) {
|
|
622
|
+
if (!this._graph.hasNode(skillId))
|
|
623
|
+
return null;
|
|
624
|
+
if (isProxy(this._graph, skillId))
|
|
625
|
+
return null;
|
|
626
|
+
return this._graph.getNodeAttribute(skillId, 'updatedAt') ?? null;
|
|
627
|
+
}
|
|
628
|
+
get skillsDir() {
|
|
629
|
+
const base = this.ctx.mirrorDir ?? this.ctx.projectDir;
|
|
630
|
+
return base ? path_1.default.join(base, '.skills') : undefined;
|
|
631
|
+
}
|
|
632
|
+
recordMirrorWrites(skillId) {
|
|
633
|
+
const dir = this.skillsDir;
|
|
634
|
+
if (!dir || !this.mirrorTracker)
|
|
635
|
+
return;
|
|
636
|
+
const entityDir = path_1.default.join(dir, skillId);
|
|
637
|
+
this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
|
|
638
|
+
this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'skill.md'));
|
|
639
|
+
this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'description.md'));
|
|
640
|
+
}
|
|
641
|
+
// -- Write (mutations with embed + dirty + emit + cross-graph cleanup) --
|
|
642
|
+
async createSkill(title, description, steps = [], triggers = [], inputHints = [], filePatterns = [], tags = [], source = 'user', confidence = 1) {
|
|
643
|
+
const embedding = await this.embedFns.document(`${title} ${description}`);
|
|
644
|
+
const skillId = createSkill(this._graph, title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence, embedding, this.ctx.author);
|
|
645
|
+
this._bm25Index.addDocument(skillId, this._graph.getNodeAttributes(skillId));
|
|
646
|
+
this.ctx.markDirty();
|
|
647
|
+
this.ctx.emit('skill:created', { projectId: this.ctx.projectId, skillId });
|
|
648
|
+
const dir = this.skillsDir;
|
|
649
|
+
if (dir) {
|
|
650
|
+
const attrs = this._graph.getNodeAttributes(skillId);
|
|
651
|
+
(0, file_mirror_1.mirrorSkillCreate)(dir, skillId, attrs, []);
|
|
652
|
+
this.recordMirrorWrites(skillId);
|
|
653
|
+
}
|
|
654
|
+
return skillId;
|
|
655
|
+
}
|
|
656
|
+
async updateSkill(skillId, patch, expectedVersion) {
|
|
657
|
+
const existing = getSkill(this._graph, skillId);
|
|
658
|
+
if (!existing)
|
|
659
|
+
return false;
|
|
660
|
+
const embedText = `${patch.title ?? existing.title} ${patch.description ?? existing.description}`;
|
|
661
|
+
const embedding = await this.embedFns.document(embedText);
|
|
662
|
+
updateSkill(this._graph, skillId, patch, embedding, this.ctx.author, expectedVersion);
|
|
663
|
+
this._bm25Index.updateDocument(skillId, this._graph.getNodeAttributes(skillId));
|
|
664
|
+
this.ctx.markDirty();
|
|
665
|
+
this.ctx.emit('skill:updated', { projectId: this.ctx.projectId, skillId });
|
|
666
|
+
const dir = this.skillsDir;
|
|
667
|
+
if (dir) {
|
|
668
|
+
const attrs = this._graph.getNodeAttributes(skillId);
|
|
669
|
+
const relations = listSkillRelations(this._graph, skillId, this.ext);
|
|
670
|
+
(0, file_mirror_1.mirrorSkillUpdate)(dir, skillId, { ...patch, by: this.ctx.author }, attrs, relations);
|
|
671
|
+
this.recordMirrorWrites(skillId);
|
|
672
|
+
}
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
deleteSkill(skillId) {
|
|
676
|
+
if (this.skillsDir)
|
|
677
|
+
(0, file_mirror_1.deleteMirrorDir)(this.skillsDir, skillId);
|
|
678
|
+
this._bm25Index.removeDocument(skillId);
|
|
679
|
+
const ok = deleteSkill(this._graph, skillId);
|
|
680
|
+
if (!ok)
|
|
681
|
+
return false;
|
|
682
|
+
// Clean up proxy in KnowledgeGraph if any note links to this skill
|
|
683
|
+
if (this.knowledgeGraph) {
|
|
684
|
+
const pId = `@skills::${skillId}`;
|
|
685
|
+
if (this.knowledgeGraph.hasNode(pId)) {
|
|
686
|
+
this.knowledgeGraph.dropNode(pId);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// Clean up proxy in TaskGraph if any task links to this skill
|
|
690
|
+
if (this.taskGraph) {
|
|
691
|
+
const pId = `@skills::${skillId}`;
|
|
692
|
+
if (this.taskGraph.hasNode(pId)) {
|
|
693
|
+
this.taskGraph.dropNode(pId);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
this.ctx.markDirty();
|
|
697
|
+
this.ctx.emit('skill:deleted', { projectId: this.ctx.projectId, skillId });
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
bumpUsage(skillId) {
|
|
701
|
+
const ok = bumpUsage(this._graph, skillId);
|
|
702
|
+
if (!ok)
|
|
703
|
+
return false;
|
|
704
|
+
this.ctx.markDirty();
|
|
705
|
+
this.ctx.emit('skill:updated', { projectId: this.ctx.projectId, skillId });
|
|
706
|
+
const dir = this.skillsDir;
|
|
707
|
+
if (dir) {
|
|
708
|
+
const attrs = this._graph.getNodeAttributes(skillId);
|
|
709
|
+
const relations = listSkillRelations(this._graph, skillId, this.ext);
|
|
710
|
+
(0, file_mirror_1.mirrorSkillUpdate)(dir, skillId, { usageCount: attrs.usageCount, lastUsedAt: attrs.lastUsedAt, by: this.ctx.author }, attrs, relations);
|
|
711
|
+
this.recordMirrorWrites(skillId);
|
|
712
|
+
}
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
linkSkills(fromId, toId, kind) {
|
|
716
|
+
const ok = createSkillRelation(this._graph, fromId, toId, kind);
|
|
717
|
+
if (ok) {
|
|
718
|
+
this.ctx.markDirty();
|
|
719
|
+
const dir = this.skillsDir;
|
|
720
|
+
if (dir) {
|
|
721
|
+
const fromAttrs = this._graph.getNodeAttributes(fromId);
|
|
722
|
+
const fromRels = listSkillRelations(this._graph, fromId, this.ext);
|
|
723
|
+
(0, file_mirror_1.mirrorSkillRelation)(dir, fromId, 'add', kind, toId, fromAttrs, fromRels);
|
|
724
|
+
this.recordMirrorWrites(fromId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return ok;
|
|
728
|
+
}
|
|
729
|
+
createCrossLink(skillId, targetId, targetGraph, kind, projectId) {
|
|
730
|
+
const pid = projectId || this.ctx.projectId;
|
|
731
|
+
const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, targetGraph, pid);
|
|
732
|
+
const ok = createCrossRelation(this._graph, skillId, targetGraph, targetId, kind, extGraph, pid);
|
|
733
|
+
// Bidirectional: create mirror proxy in KnowledgeGraph
|
|
734
|
+
if (ok && targetGraph === 'knowledge' && this.knowledgeGraph) {
|
|
735
|
+
createMirrorInKnowledgeGraph(this.knowledgeGraph, skillId, targetId, kind);
|
|
736
|
+
}
|
|
737
|
+
// Bidirectional: create mirror proxy in TaskGraph
|
|
738
|
+
if (ok && targetGraph === 'tasks' && this.taskGraph) {
|
|
739
|
+
createMirrorInTaskGraph(this.taskGraph, skillId, targetId, kind);
|
|
740
|
+
}
|
|
741
|
+
if (ok) {
|
|
742
|
+
this.ctx.markDirty();
|
|
743
|
+
const dir = this.skillsDir;
|
|
744
|
+
if (dir) {
|
|
745
|
+
const attrs = this._graph.getNodeAttributes(skillId);
|
|
746
|
+
const relations = listSkillRelations(this._graph, skillId, this.ext);
|
|
747
|
+
(0, file_mirror_1.mirrorSkillRelation)(dir, skillId, 'add', kind, targetId, attrs, relations, targetGraph);
|
|
748
|
+
this.recordMirrorWrites(skillId);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return ok;
|
|
752
|
+
}
|
|
753
|
+
deleteCrossLink(skillId, targetId, targetGraph, projectId) {
|
|
754
|
+
const pid = projectId || this.ctx.projectId;
|
|
755
|
+
// Read edge kind before deleting
|
|
756
|
+
let kind = '';
|
|
757
|
+
try {
|
|
758
|
+
const proxyNodeId = proxyId(targetGraph, targetId, pid);
|
|
759
|
+
if (this._graph.hasEdge(skillId, proxyNodeId)) {
|
|
760
|
+
kind = this._graph.getEdgeAttribute(this._graph.edge(skillId, proxyNodeId), 'kind') ?? '';
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch { /* ignore */ }
|
|
764
|
+
const ok = deleteCrossRelation(this._graph, skillId, targetGraph, targetId, pid);
|
|
765
|
+
if (ok && targetGraph === 'knowledge' && this.knowledgeGraph) {
|
|
766
|
+
deleteMirrorFromKnowledgeGraph(this.knowledgeGraph, skillId, targetId);
|
|
767
|
+
}
|
|
768
|
+
if (ok && targetGraph === 'tasks' && this.taskGraph) {
|
|
769
|
+
deleteMirrorFromTaskGraph(this.taskGraph, skillId, targetId);
|
|
770
|
+
}
|
|
771
|
+
if (ok) {
|
|
772
|
+
this.ctx.markDirty();
|
|
773
|
+
const dir = this.skillsDir;
|
|
774
|
+
if (dir) {
|
|
775
|
+
const attrs = this._graph.getNodeAttributes(skillId);
|
|
776
|
+
const relations = listSkillRelations(this._graph, skillId, this.ext);
|
|
777
|
+
(0, file_mirror_1.mirrorSkillRelation)(dir, skillId, 'remove', kind, targetId, attrs, relations, targetGraph);
|
|
778
|
+
this.recordMirrorWrites(skillId);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return ok;
|
|
782
|
+
}
|
|
783
|
+
deleteSkillLink(fromId, toId) {
|
|
784
|
+
// Read edge kind before deleting
|
|
785
|
+
let kind = '';
|
|
786
|
+
try {
|
|
787
|
+
if (this._graph.hasEdge(fromId, toId)) {
|
|
788
|
+
kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, toId), 'kind') ?? '';
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch { /* ignore */ }
|
|
792
|
+
const ok = deleteSkillRelation(this._graph, fromId, toId);
|
|
793
|
+
if (ok) {
|
|
794
|
+
this.ctx.markDirty();
|
|
795
|
+
const dir = this.skillsDir;
|
|
796
|
+
if (dir) {
|
|
797
|
+
const fromAttrs = this._graph.getNodeAttributes(fromId);
|
|
798
|
+
const fromRels = listSkillRelations(this._graph, fromId, this.ext);
|
|
799
|
+
(0, file_mirror_1.mirrorSkillRelation)(dir, fromId, 'remove', kind, toId, fromAttrs, fromRels);
|
|
800
|
+
this.recordMirrorWrites(fromId);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return ok;
|
|
804
|
+
}
|
|
805
|
+
// -- Attachments --
|
|
806
|
+
addAttachment(skillId, filename, data) {
|
|
807
|
+
const dir = this.skillsDir;
|
|
808
|
+
if (!dir)
|
|
809
|
+
return null;
|
|
810
|
+
if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
|
|
811
|
+
return null;
|
|
812
|
+
const safe = (0, file_mirror_1.sanitizeFilename)(filename);
|
|
813
|
+
if (!safe)
|
|
814
|
+
return null;
|
|
815
|
+
const entityDir = path_1.default.join(dir, skillId);
|
|
816
|
+
(0, file_mirror_1.writeAttachment)(dir, skillId, safe, data);
|
|
817
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
|
|
818
|
+
(0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'add', safe);
|
|
819
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
|
|
820
|
+
const attachments = (0, attachment_types_1.scanAttachments)(entityDir);
|
|
821
|
+
this._graph.setNodeAttribute(skillId, 'attachments', attachments);
|
|
822
|
+
this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
|
|
823
|
+
this.ctx.markDirty();
|
|
824
|
+
this.ctx.emit('skill:attachment:added', { projectId: this.ctx.projectId, skillId, filename: safe });
|
|
825
|
+
return attachments.find(a => a.filename === safe) ?? null;
|
|
826
|
+
}
|
|
827
|
+
removeAttachment(skillId, filename) {
|
|
828
|
+
const dir = this.skillsDir;
|
|
829
|
+
if (!dir)
|
|
830
|
+
return false;
|
|
831
|
+
if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
|
|
832
|
+
return false;
|
|
833
|
+
const safe = (0, file_mirror_1.sanitizeFilename)(filename);
|
|
834
|
+
const entityDir = path_1.default.join(dir, skillId);
|
|
835
|
+
const deleted = (0, file_mirror_1.deleteAttachment)(dir, skillId, safe);
|
|
836
|
+
if (!deleted)
|
|
837
|
+
return false;
|
|
838
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
|
|
839
|
+
(0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'remove', safe);
|
|
840
|
+
this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
|
|
841
|
+
const attachments = (0, attachment_types_1.scanAttachments)(entityDir);
|
|
842
|
+
this._graph.setNodeAttribute(skillId, 'attachments', attachments);
|
|
843
|
+
this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
|
|
844
|
+
this.ctx.markDirty();
|
|
845
|
+
this.ctx.emit('skill:attachment:deleted', { projectId: this.ctx.projectId, skillId, filename: safe });
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
syncAttachments(skillId) {
|
|
849
|
+
const dir = this.skillsDir;
|
|
850
|
+
if (!dir)
|
|
851
|
+
return;
|
|
852
|
+
if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
|
|
853
|
+
return;
|
|
854
|
+
const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, skillId));
|
|
855
|
+
this._graph.setNodeAttribute(skillId, 'attachments', attachments);
|
|
856
|
+
this.ctx.markDirty();
|
|
857
|
+
}
|
|
858
|
+
listAttachments(skillId) {
|
|
859
|
+
if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
|
|
860
|
+
return [];
|
|
861
|
+
return this._graph.getNodeAttribute(skillId, 'attachments') ?? [];
|
|
862
|
+
}
|
|
863
|
+
getAttachmentPath(skillId, filename) {
|
|
864
|
+
const dir = this.skillsDir;
|
|
865
|
+
if (!dir)
|
|
866
|
+
return null;
|
|
867
|
+
return (0, file_mirror_1.getAttachmentPath)(dir, skillId, filename);
|
|
868
|
+
}
|
|
869
|
+
// -- Import from file (reverse mirror — does NOT write back to file) --
|
|
870
|
+
async importFromFile(parsed) {
|
|
871
|
+
const exists = this._graph.hasNode(parsed.id) && !isProxy(this._graph, parsed.id);
|
|
872
|
+
const embedding = await this.embedFns.document(`${parsed.title} ${parsed.description}`);
|
|
873
|
+
const now = Date.now();
|
|
874
|
+
if (exists) {
|
|
875
|
+
const existing = this._graph.getNodeAttributes(parsed.id);
|
|
876
|
+
this._graph.mergeNodeAttributes(parsed.id, {
|
|
877
|
+
title: parsed.title,
|
|
878
|
+
description: parsed.description,
|
|
879
|
+
steps: parsed.steps,
|
|
880
|
+
triggers: parsed.triggers,
|
|
881
|
+
inputHints: parsed.inputHints,
|
|
882
|
+
filePatterns: parsed.filePatterns,
|
|
883
|
+
tags: parsed.tags,
|
|
884
|
+
source: parsed.source,
|
|
885
|
+
confidence: parsed.confidence,
|
|
886
|
+
usageCount: parsed.usageCount ?? existing.usageCount,
|
|
887
|
+
lastUsedAt: parsed.lastUsedAt ?? existing.lastUsedAt,
|
|
888
|
+
embedding,
|
|
889
|
+
attachments: parsed.attachments,
|
|
890
|
+
updatedAt: now,
|
|
891
|
+
createdAt: existing.createdAt,
|
|
892
|
+
version: parsed.version ?? existing.version + 1,
|
|
893
|
+
...(parsed.createdBy != null ? { createdBy: parsed.createdBy } : {}),
|
|
894
|
+
...(parsed.updatedBy != null ? { updatedBy: parsed.updatedBy } : {}),
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
this._graph.addNode(parsed.id, {
|
|
899
|
+
title: parsed.title,
|
|
900
|
+
description: parsed.description,
|
|
901
|
+
steps: parsed.steps,
|
|
902
|
+
triggers: parsed.triggers,
|
|
903
|
+
inputHints: parsed.inputHints,
|
|
904
|
+
filePatterns: parsed.filePatterns,
|
|
905
|
+
tags: parsed.tags,
|
|
906
|
+
source: parsed.source,
|
|
907
|
+
confidence: parsed.confidence,
|
|
908
|
+
usageCount: parsed.usageCount ?? 0,
|
|
909
|
+
lastUsedAt: parsed.lastUsedAt ?? null,
|
|
910
|
+
embedding,
|
|
911
|
+
attachments: parsed.attachments ?? [],
|
|
912
|
+
createdAt: parsed.createdAt ?? now,
|
|
913
|
+
updatedAt: now,
|
|
914
|
+
version: parsed.version ?? 1,
|
|
915
|
+
createdBy: parsed.createdBy ?? undefined,
|
|
916
|
+
updatedBy: parsed.updatedBy ?? undefined,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
this._bm25Index.updateDocument(parsed.id, this._graph.getNodeAttributes(parsed.id));
|
|
920
|
+
this.syncRelationsFromFile(parsed.id, parsed.relations);
|
|
921
|
+
this.ctx.markDirty();
|
|
922
|
+
this.ctx.emit(exists ? 'skill:updated' : 'skill:created', { projectId: this.ctx.projectId, skillId: parsed.id });
|
|
923
|
+
}
|
|
924
|
+
updateDescriptionFromFile(skillId, description) {
|
|
925
|
+
if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
|
|
926
|
+
return;
|
|
927
|
+
this._graph.setNodeAttribute(skillId, 'description', description);
|
|
928
|
+
this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
|
|
929
|
+
this._graph.setNodeAttribute(skillId, 'version', (this._graph.getNodeAttribute(skillId, 'version') ?? 0) + 1);
|
|
930
|
+
this.ctx.markDirty();
|
|
931
|
+
this.ctx.emit('skill:updated', { projectId: this.ctx.projectId, skillId });
|
|
932
|
+
}
|
|
933
|
+
deleteFromFile(skillId) {
|
|
934
|
+
if (!this._graph.hasNode(skillId))
|
|
935
|
+
return;
|
|
936
|
+
if (isProxy(this._graph, skillId))
|
|
937
|
+
return;
|
|
938
|
+
this._bm25Index.removeDocument(skillId);
|
|
939
|
+
deleteSkill(this._graph, skillId);
|
|
940
|
+
if (this.knowledgeGraph) {
|
|
941
|
+
const pId = `@skills::${skillId}`;
|
|
942
|
+
if (this.knowledgeGraph.hasNode(pId))
|
|
943
|
+
this.knowledgeGraph.dropNode(pId);
|
|
944
|
+
}
|
|
945
|
+
if (this.taskGraph) {
|
|
946
|
+
const pId = `@skills::${skillId}`;
|
|
947
|
+
if (this.taskGraph.hasNode(pId))
|
|
948
|
+
this.taskGraph.dropNode(pId);
|
|
949
|
+
}
|
|
950
|
+
this.ctx.markDirty();
|
|
951
|
+
this.ctx.emit('skill:deleted', { projectId: this.ctx.projectId, skillId });
|
|
952
|
+
}
|
|
953
|
+
syncRelationsFromFile(skillId, desired) {
|
|
954
|
+
const current = [];
|
|
955
|
+
this._graph.forEachOutEdge(skillId, (_edge, attrs, _src, target) => {
|
|
956
|
+
const proxy = this._graph.hasNode(target) ? this._graph.getNodeAttribute(target, 'proxyFor') : undefined;
|
|
957
|
+
if (proxy) {
|
|
958
|
+
current.push({ to: proxy.nodeId, kind: attrs.kind, graph: proxy.graph });
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
current.push({ to: target, kind: attrs.kind });
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
const diff = (0, file_import_1.diffRelations)(current, desired);
|
|
965
|
+
for (const rel of diff.toRemove) {
|
|
966
|
+
if (rel.graph) {
|
|
967
|
+
deleteCrossRelation(this._graph, skillId, rel.graph, rel.to);
|
|
968
|
+
if (rel.graph === 'knowledge' && this.knowledgeGraph) {
|
|
969
|
+
deleteMirrorFromKnowledgeGraph(this.knowledgeGraph, skillId, rel.to);
|
|
970
|
+
}
|
|
971
|
+
if (rel.graph === 'tasks' && this.taskGraph) {
|
|
972
|
+
deleteMirrorFromTaskGraph(this.taskGraph, skillId, rel.to);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
deleteSkillRelation(this._graph, skillId, rel.to);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const rel of diff.toAdd) {
|
|
980
|
+
if (rel.graph) {
|
|
981
|
+
const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, rel.graph);
|
|
982
|
+
createCrossRelation(this._graph, skillId, rel.graph, rel.to, rel.kind, extGraph);
|
|
983
|
+
if (rel.graph === 'knowledge' && this.knowledgeGraph) {
|
|
984
|
+
createMirrorInKnowledgeGraph(this.knowledgeGraph, skillId, rel.to, rel.kind);
|
|
985
|
+
}
|
|
986
|
+
if (rel.graph === 'tasks' && this.taskGraph) {
|
|
987
|
+
createMirrorInTaskGraph(this.taskGraph, skillId, rel.to, rel.kind);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
createSkillRelation(this._graph, skillId, rel.to, rel.kind);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// -- Read --
|
|
996
|
+
getSkill(skillId) {
|
|
997
|
+
return getSkill(this._graph, skillId);
|
|
998
|
+
}
|
|
999
|
+
listSkills(opts) {
|
|
1000
|
+
return listSkills(this._graph, opts);
|
|
1001
|
+
}
|
|
1002
|
+
async searchSkills(query, opts) {
|
|
1003
|
+
const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
|
|
1004
|
+
return (0, skills_1.searchSkills)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
|
|
1005
|
+
}
|
|
1006
|
+
listRelations(skillId) {
|
|
1007
|
+
return listSkillRelations(this._graph, skillId, this.ext);
|
|
1008
|
+
}
|
|
1009
|
+
findLinkedSkills(targetGraph, targetNodeId, kind, projectId) {
|
|
1010
|
+
return findLinkedSkills(this._graph, targetGraph, targetNodeId, kind, projectId || this.ctx.projectId);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
exports.SkillGraphManager = SkillGraphManager;
|