@graphmemory/server 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +216 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/embed.js +47 -0
  7. package/dist/api/rest/files.js +64 -0
  8. package/dist/api/rest/graph.js +71 -0
  9. package/dist/api/rest/index.js +371 -0
  10. package/dist/api/rest/knowledge.js +239 -0
  11. package/dist/api/rest/skills.js +285 -0
  12. package/dist/api/rest/tasks.js +273 -0
  13. package/dist/api/rest/tools.js +157 -0
  14. package/dist/api/rest/validation.js +196 -0
  15. package/dist/api/rest/websocket.js +71 -0
  16. package/dist/api/tools/code/get-file-symbols.js +30 -0
  17. package/dist/api/tools/code/get-symbol.js +22 -0
  18. package/dist/api/tools/code/list-files.js +18 -0
  19. package/dist/api/tools/code/search-code.js +27 -0
  20. package/dist/api/tools/code/search-files.js +22 -0
  21. package/dist/api/tools/context/get-context.js +19 -0
  22. package/dist/api/tools/docs/cross-references.js +76 -0
  23. package/dist/api/tools/docs/explain-symbol.js +55 -0
  24. package/dist/api/tools/docs/find-examples.js +52 -0
  25. package/dist/api/tools/docs/get-node.js +24 -0
  26. package/dist/api/tools/docs/get-toc.js +22 -0
  27. package/dist/api/tools/docs/list-snippets.js +46 -0
  28. package/dist/api/tools/docs/list-topics.js +18 -0
  29. package/dist/api/tools/docs/search-files.js +22 -0
  30. package/dist/api/tools/docs/search-snippets.js +43 -0
  31. package/dist/api/tools/docs/search.js +27 -0
  32. package/dist/api/tools/file-index/get-file-info.js +21 -0
  33. package/dist/api/tools/file-index/list-all-files.js +28 -0
  34. package/dist/api/tools/file-index/search-all-files.js +24 -0
  35. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  36. package/dist/api/tools/knowledge/create-note.js +20 -0
  37. package/dist/api/tools/knowledge/create-relation.js +29 -0
  38. package/dist/api/tools/knowledge/delete-note.js +19 -0
  39. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  40. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  41. package/dist/api/tools/knowledge/get-note.js +20 -0
  42. package/dist/api/tools/knowledge/list-notes.js +18 -0
  43. package/dist/api/tools/knowledge/list-relations.js +17 -0
  44. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  45. package/dist/api/tools/knowledge/search-notes.js +25 -0
  46. package/dist/api/tools/knowledge/update-note.js +34 -0
  47. package/dist/api/tools/skills/add-attachment.js +31 -0
  48. package/dist/api/tools/skills/bump-usage.js +19 -0
  49. package/dist/api/tools/skills/create-skill-link.js +25 -0
  50. package/dist/api/tools/skills/create-skill.js +26 -0
  51. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  52. package/dist/api/tools/skills/delete-skill.js +20 -0
  53. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  54. package/dist/api/tools/skills/get-skill.js +21 -0
  55. package/dist/api/tools/skills/link-skill.js +23 -0
  56. package/dist/api/tools/skills/list-skills.js +20 -0
  57. package/dist/api/tools/skills/recall-skills.js +18 -0
  58. package/dist/api/tools/skills/remove-attachment.js +19 -0
  59. package/dist/api/tools/skills/search-skills.js +25 -0
  60. package/dist/api/tools/skills/update-skill.js +58 -0
  61. package/dist/api/tools/tasks/add-attachment.js +31 -0
  62. package/dist/api/tools/tasks/create-task-link.js +25 -0
  63. package/dist/api/tools/tasks/create-task.js +26 -0
  64. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  65. package/dist/api/tools/tasks/delete-task.js +20 -0
  66. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  67. package/dist/api/tools/tasks/get-task.js +20 -0
  68. package/dist/api/tools/tasks/link-task.js +23 -0
  69. package/dist/api/tools/tasks/list-tasks.js +25 -0
  70. package/dist/api/tools/tasks/move-task.js +38 -0
  71. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  72. package/dist/api/tools/tasks/search-tasks.js +25 -0
  73. package/dist/api/tools/tasks/update-task.js +58 -0
  74. package/dist/cli/index.js +617 -0
  75. package/dist/cli/indexer.js +275 -0
  76. package/dist/graphs/attachment-types.js +74 -0
  77. package/dist/graphs/code-types.js +10 -0
  78. package/dist/graphs/code.js +204 -0
  79. package/dist/graphs/docs.js +231 -0
  80. package/dist/graphs/file-index-types.js +10 -0
  81. package/dist/graphs/file-index.js +310 -0
  82. package/dist/graphs/file-lang.js +119 -0
  83. package/dist/graphs/knowledge-types.js +32 -0
  84. package/dist/graphs/knowledge.js +768 -0
  85. package/dist/graphs/manager-types.js +87 -0
  86. package/dist/graphs/skill-types.js +10 -0
  87. package/dist/graphs/skill.js +1016 -0
  88. package/dist/graphs/task-types.js +17 -0
  89. package/dist/graphs/task.js +972 -0
  90. package/dist/lib/access.js +67 -0
  91. package/dist/lib/embedder.js +235 -0
  92. package/dist/lib/events-log.js +401 -0
  93. package/dist/lib/file-import.js +328 -0
  94. package/dist/lib/file-mirror.js +461 -0
  95. package/dist/lib/frontmatter.js +17 -0
  96. package/dist/lib/jwt.js +146 -0
  97. package/dist/lib/mirror-watcher.js +637 -0
  98. package/dist/lib/multi-config.js +393 -0
  99. package/dist/lib/parsers/code.js +214 -0
  100. package/dist/lib/parsers/codeblock.js +33 -0
  101. package/dist/lib/parsers/docs.js +199 -0
  102. package/dist/lib/parsers/languages/index.js +15 -0
  103. package/dist/lib/parsers/languages/registry.js +68 -0
  104. package/dist/lib/parsers/languages/types.js +2 -0
  105. package/dist/lib/parsers/languages/typescript.js +306 -0
  106. package/dist/lib/project-manager.js +458 -0
  107. package/dist/lib/promise-queue.js +22 -0
  108. package/dist/lib/search/bm25.js +167 -0
  109. package/dist/lib/search/code.js +103 -0
  110. package/dist/lib/search/docs.js +106 -0
  111. package/dist/lib/search/file-index.js +31 -0
  112. package/dist/lib/search/files.js +61 -0
  113. package/dist/lib/search/knowledge.js +101 -0
  114. package/dist/lib/search/skills.js +104 -0
  115. package/dist/lib/search/tasks.js +103 -0
  116. package/dist/lib/team.js +89 -0
  117. package/dist/lib/watcher.js +67 -0
  118. package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
  119. package/dist/ui/assets/index-kKd4mVrh.css +1 -0
  120. package/dist/ui/favicon.svg +1 -0
  121. package/dist/ui/icons.svg +24 -0
  122. package/dist/ui/index.html +14 -0
  123. package/package.json +89 -0
@@ -0,0 +1,239 @@
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.createKnowledgeRouter = createKnowledgeRouter;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const mime_1 = __importDefault(require("mime"));
9
+ const express_1 = require("express");
10
+ const multer_1 = __importDefault(require("multer"));
11
+ const validation_1 = require("../../api/rest/validation");
12
+ const index_1 = require("../../api/rest/index");
13
+ const manager_types_1 = require("../../graphs/manager-types");
14
+ const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
15
+ function createKnowledgeRouter() {
16
+ const router = (0, express_1.Router)({ mergeParams: true });
17
+ function getProject(req) {
18
+ return req.project;
19
+ }
20
+ // List notes
21
+ router.get('/notes', (0, validation_1.validateQuery)(validation_1.noteListSchema), (req, res, next) => {
22
+ try {
23
+ const p = getProject(req);
24
+ const q = req.validatedQuery;
25
+ const notes = p.knowledgeManager.listNotes(q.filter, q.tag, q.limit);
26
+ res.json({ results: notes });
27
+ }
28
+ catch (err) {
29
+ next(err);
30
+ }
31
+ });
32
+ // Search notes
33
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.noteSearchSchema), async (req, res, next) => {
34
+ try {
35
+ const p = getProject(req);
36
+ const q = req.validatedQuery;
37
+ const results = await p.knowledgeManager.searchNotes(q.q, {
38
+ topK: q.topK,
39
+ minScore: q.minScore,
40
+ searchMode: q.searchMode,
41
+ });
42
+ res.json({ results });
43
+ }
44
+ catch (err) {
45
+ next(err);
46
+ }
47
+ });
48
+ // Get note
49
+ router.get('/notes/:noteId', (req, res, next) => {
50
+ try {
51
+ const p = getProject(req);
52
+ const note = p.knowledgeManager.getNote(req.params.noteId);
53
+ if (!note)
54
+ return res.status(404).json({ error: 'Note not found' });
55
+ res.json(note);
56
+ }
57
+ catch (err) {
58
+ next(err);
59
+ }
60
+ });
61
+ // Create note
62
+ router.post('/notes', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createNoteSchema), async (req, res, next) => {
63
+ try {
64
+ const p = getProject(req);
65
+ const { title, content, tags } = req.body;
66
+ const created = await p.mutationQueue.enqueue(async () => {
67
+ const noteId = await p.knowledgeManager.createNote(title, content, tags);
68
+ return p.knowledgeManager.getNote(noteId);
69
+ });
70
+ res.status(201).json(created);
71
+ }
72
+ catch (err) {
73
+ next(err);
74
+ }
75
+ });
76
+ // Update note
77
+ router.put('/notes/:noteId', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.updateNoteSchema), async (req, res, next) => {
78
+ try {
79
+ const p = getProject(req);
80
+ const noteId = req.params.noteId;
81
+ const { version, ...patch } = req.body;
82
+ const result = await p.mutationQueue.enqueue(async () => {
83
+ const ok = await p.knowledgeManager.updateNote(noteId, patch, version);
84
+ if (!ok)
85
+ return null;
86
+ return p.knowledgeManager.getNote(noteId);
87
+ });
88
+ if (!result)
89
+ return res.status(404).json({ error: 'Note not found' });
90
+ res.json(result);
91
+ }
92
+ catch (err) {
93
+ if (err instanceof manager_types_1.VersionConflictError) {
94
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
95
+ }
96
+ next(err);
97
+ }
98
+ });
99
+ // Delete note
100
+ router.delete('/notes/:noteId', index_1.requireWriteAccess, async (req, res, next) => {
101
+ try {
102
+ const p = getProject(req);
103
+ const noteId = req.params.noteId;
104
+ const ok = await p.mutationQueue.enqueue(async () => {
105
+ return p.knowledgeManager.deleteNote(noteId);
106
+ });
107
+ if (!ok)
108
+ return res.status(404).json({ error: 'Note not found' });
109
+ res.status(204).end();
110
+ }
111
+ catch (err) {
112
+ next(err);
113
+ }
114
+ });
115
+ // Create relation
116
+ router.post('/relations', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createRelationSchema), async (req, res, next) => {
117
+ try {
118
+ const p = getProject(req);
119
+ const { fromId, toId, kind, targetGraph, projectId } = req.body;
120
+ const ok = await p.mutationQueue.enqueue(async () => {
121
+ return p.knowledgeManager.createRelation(fromId, toId, kind, targetGraph, projectId);
122
+ });
123
+ if (!ok)
124
+ return res.status(400).json({ error: 'Failed to create relation' });
125
+ res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
126
+ }
127
+ catch (err) {
128
+ next(err);
129
+ }
130
+ });
131
+ // Delete relation
132
+ router.delete('/relations', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createRelationSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
133
+ try {
134
+ const p = getProject(req);
135
+ const { fromId, toId, targetGraph, projectId } = req.body;
136
+ const ok = await p.mutationQueue.enqueue(async () => {
137
+ return p.knowledgeManager.deleteRelation(fromId, toId, targetGraph, projectId);
138
+ });
139
+ if (!ok)
140
+ return res.status(404).json({ error: 'Relation not found' });
141
+ res.status(204).end();
142
+ }
143
+ catch (err) {
144
+ next(err);
145
+ }
146
+ });
147
+ // List relations for a note
148
+ router.get('/notes/:noteId/relations', (req, res, next) => {
149
+ try {
150
+ const p = getProject(req);
151
+ const relations = p.knowledgeManager.listRelations(req.params.noteId);
152
+ res.json({ results: relations });
153
+ }
154
+ catch (err) {
155
+ next(err);
156
+ }
157
+ });
158
+ // Find notes linked to an external entity
159
+ router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
160
+ try {
161
+ const p = getProject(req);
162
+ const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
163
+ const notes = p.knowledgeManager.findLinkedNotes(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
164
+ res.json({ results: notes });
165
+ }
166
+ catch (err) {
167
+ next(err);
168
+ }
169
+ });
170
+ // -- Attachments --
171
+ // Upload attachment
172
+ router.post('/notes/:noteId/attachments', index_1.requireWriteAccess, upload.single('file'), async (req, res, next) => {
173
+ try {
174
+ const p = getProject(req);
175
+ const noteId = req.params.noteId;
176
+ const file = req.file;
177
+ if (!file)
178
+ return res.status(400).json({ error: 'No file uploaded' });
179
+ const meta = await p.mutationQueue.enqueue(async () => {
180
+ return p.knowledgeManager.addAttachment(noteId, file.originalname, file.buffer);
181
+ });
182
+ if (!meta)
183
+ return res.status(404).json({ error: 'Note not found' });
184
+ res.status(201).json(meta);
185
+ }
186
+ catch (err) {
187
+ next(err);
188
+ }
189
+ });
190
+ // List attachments
191
+ router.get('/notes/:noteId/attachments', (req, res, next) => {
192
+ try {
193
+ const p = getProject(req);
194
+ const attachments = p.knowledgeManager.listAttachments(req.params.noteId);
195
+ res.json({ results: attachments });
196
+ }
197
+ catch (err) {
198
+ next(err);
199
+ }
200
+ });
201
+ // Download attachment
202
+ router.get('/notes/:noteId/attachments/:filename', (req, res, next) => {
203
+ try {
204
+ const p = getProject(req);
205
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
206
+ const filePath = p.knowledgeManager.getAttachmentPath(req.params.noteId, filename);
207
+ if (!filePath)
208
+ return res.status(404).json({ error: 'Attachment not found' });
209
+ const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
210
+ res.setHeader('Content-Type', mimeType);
211
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
212
+ res.setHeader('X-Content-Type-Options', 'nosniff');
213
+ const stream = fs_1.default.createReadStream(filePath);
214
+ stream.on('error', (err) => next(err));
215
+ stream.pipe(res);
216
+ }
217
+ catch (err) {
218
+ next(err);
219
+ }
220
+ });
221
+ // Delete attachment
222
+ router.delete('/notes/:noteId/attachments/:filename', index_1.requireWriteAccess, async (req, res, next) => {
223
+ try {
224
+ const p = getProject(req);
225
+ const noteId = req.params.noteId;
226
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
227
+ const ok = await p.mutationQueue.enqueue(async () => {
228
+ return p.knowledgeManager.removeAttachment(noteId, filename);
229
+ });
230
+ if (!ok)
231
+ return res.status(404).json({ error: 'Attachment not found' });
232
+ res.status(204).end();
233
+ }
234
+ catch (err) {
235
+ next(err);
236
+ }
237
+ });
238
+ return router;
239
+ }
@@ -0,0 +1,285 @@
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.createSkillsRouter = createSkillsRouter;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const mime_1 = __importDefault(require("mime"));
9
+ const express_1 = require("express");
10
+ const multer_1 = __importDefault(require("multer"));
11
+ const validation_1 = require("../../api/rest/validation");
12
+ const index_1 = require("../../api/rest/index");
13
+ const manager_types_1 = require("../../graphs/manager-types");
14
+ const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
15
+ function createSkillsRouter() {
16
+ const router = (0, express_1.Router)({ mergeParams: true });
17
+ function getProject(req) {
18
+ return req.project;
19
+ }
20
+ // List skills
21
+ router.get('/', (0, validation_1.validateQuery)(validation_1.skillListSchema), (req, res, next) => {
22
+ try {
23
+ const p = getProject(req);
24
+ const q = req.validatedQuery;
25
+ const skills = p.skillManager.listSkills(q);
26
+ res.json({ results: skills });
27
+ }
28
+ catch (err) {
29
+ next(err);
30
+ }
31
+ });
32
+ // Search skills
33
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.skillSearchSchema), async (req, res, next) => {
34
+ try {
35
+ const p = getProject(req);
36
+ const q = req.validatedQuery;
37
+ const results = await p.skillManager.searchSkills(q.q, {
38
+ topK: q.topK,
39
+ minScore: q.minScore,
40
+ searchMode: q.searchMode,
41
+ });
42
+ res.json({ results });
43
+ }
44
+ catch (err) {
45
+ next(err);
46
+ }
47
+ });
48
+ // Recall skills (higher recall search for task contexts)
49
+ router.get('/recall', (0, validation_1.validateQuery)(validation_1.skillSearchSchema), async (req, res, next) => {
50
+ try {
51
+ const p = getProject(req);
52
+ const q = req.validatedQuery;
53
+ const results = await p.skillManager.searchSkills(q.q, {
54
+ topK: q.topK,
55
+ minScore: q.minScore ?? 0.3,
56
+ searchMode: q.searchMode,
57
+ });
58
+ res.json({ results });
59
+ }
60
+ catch (err) {
61
+ next(err);
62
+ }
63
+ });
64
+ // Find skills linked to an external entity
65
+ router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
66
+ try {
67
+ const p = getProject(req);
68
+ const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
69
+ const skills = p.skillManager.findLinkedSkills(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
70
+ res.json({ results: skills });
71
+ }
72
+ catch (err) {
73
+ next(err);
74
+ }
75
+ });
76
+ // Get skill
77
+ router.get('/:skillId', (req, res, next) => {
78
+ try {
79
+ const p = getProject(req);
80
+ const skill = p.skillManager.getSkill(req.params.skillId);
81
+ if (!skill)
82
+ return res.status(404).json({ error: 'Skill not found' });
83
+ const relations = p.skillManager.listRelations(req.params.skillId);
84
+ res.json({ ...skill, relations });
85
+ }
86
+ catch (err) {
87
+ next(err);
88
+ }
89
+ });
90
+ // Create skill
91
+ router.post('/', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createSkillSchema), async (req, res, next) => {
92
+ try {
93
+ const p = getProject(req);
94
+ const { title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence } = req.body;
95
+ const created = await p.mutationQueue.enqueue(async () => {
96
+ const skillId = await p.skillManager.createSkill(title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence);
97
+ return p.skillManager.getSkill(skillId);
98
+ });
99
+ res.status(201).json(created);
100
+ }
101
+ catch (err) {
102
+ next(err);
103
+ }
104
+ });
105
+ // Update skill
106
+ router.put('/:skillId', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.updateSkillSchema), async (req, res, next) => {
107
+ try {
108
+ const p = getProject(req);
109
+ const skillId = req.params.skillId;
110
+ const { version, ...patch } = req.body;
111
+ const result = await p.mutationQueue.enqueue(async () => {
112
+ const ok = await p.skillManager.updateSkill(skillId, patch, version);
113
+ if (!ok)
114
+ return null;
115
+ return p.skillManager.getSkill(skillId);
116
+ });
117
+ if (!result)
118
+ return res.status(404).json({ error: 'Skill not found' });
119
+ res.json(result);
120
+ }
121
+ catch (err) {
122
+ if (err instanceof manager_types_1.VersionConflictError) {
123
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
124
+ }
125
+ next(err);
126
+ }
127
+ });
128
+ // Bump usage
129
+ router.post('/:skillId/bump', index_1.requireWriteAccess, async (req, res, next) => {
130
+ try {
131
+ const p = getProject(req);
132
+ const skillId = req.params.skillId;
133
+ const result = await p.mutationQueue.enqueue(async () => {
134
+ const ok = p.skillManager.bumpUsage(skillId);
135
+ if (!ok)
136
+ return null;
137
+ return p.skillManager.getSkill(skillId);
138
+ });
139
+ if (!result)
140
+ return res.status(404).json({ error: 'Skill not found' });
141
+ res.json(result);
142
+ }
143
+ catch (err) {
144
+ next(err);
145
+ }
146
+ });
147
+ // Delete skill
148
+ router.delete('/:skillId', index_1.requireWriteAccess, async (req, res, next) => {
149
+ try {
150
+ const p = getProject(req);
151
+ const skillId = req.params.skillId;
152
+ const ok = await p.mutationQueue.enqueue(async () => {
153
+ return p.skillManager.deleteSkill(skillId);
154
+ });
155
+ if (!ok)
156
+ return res.status(404).json({ error: 'Skill not found' });
157
+ res.status(204).end();
158
+ }
159
+ catch (err) {
160
+ next(err);
161
+ }
162
+ });
163
+ // Create skill link (skill-to-skill or cross-graph)
164
+ router.post('/links', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createSkillLinkSchema), async (req, res, next) => {
165
+ try {
166
+ const p = getProject(req);
167
+ const { fromId, toId, kind, targetGraph, projectId } = req.body;
168
+ const ok = await p.mutationQueue.enqueue(async () => {
169
+ if (targetGraph) {
170
+ return p.skillManager.createCrossLink(fromId, toId, targetGraph, kind, projectId);
171
+ }
172
+ else {
173
+ return p.skillManager.linkSkills(fromId, toId, kind);
174
+ }
175
+ });
176
+ if (!ok)
177
+ return res.status(400).json({ error: 'Failed to create link' });
178
+ res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
179
+ }
180
+ catch (err) {
181
+ next(err);
182
+ }
183
+ });
184
+ // Delete skill link
185
+ router.delete('/links', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createSkillLinkSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
186
+ try {
187
+ const p = getProject(req);
188
+ const { fromId, toId, targetGraph, projectId } = req.body;
189
+ const ok = await p.mutationQueue.enqueue(async () => {
190
+ if (targetGraph) {
191
+ return p.skillManager.deleteCrossLink(fromId, toId, targetGraph, projectId);
192
+ }
193
+ else {
194
+ return p.skillManager.deleteSkillLink(fromId, toId);
195
+ }
196
+ });
197
+ if (!ok)
198
+ return res.status(404).json({ error: 'Link not found' });
199
+ res.status(204).end();
200
+ }
201
+ catch (err) {
202
+ next(err);
203
+ }
204
+ });
205
+ // List relations for a skill
206
+ router.get('/:skillId/relations', (req, res, next) => {
207
+ try {
208
+ const p = getProject(req);
209
+ const relations = p.skillManager.listRelations(req.params.skillId);
210
+ res.json({ results: relations });
211
+ }
212
+ catch (err) {
213
+ next(err);
214
+ }
215
+ });
216
+ // -- Attachments --
217
+ // Upload attachment
218
+ router.post('/:skillId/attachments', index_1.requireWriteAccess, upload.single('file'), async (req, res, next) => {
219
+ try {
220
+ const p = getProject(req);
221
+ const skillId = req.params.skillId;
222
+ const file = req.file;
223
+ if (!file)
224
+ return res.status(400).json({ error: 'No file uploaded' });
225
+ const meta = await p.mutationQueue.enqueue(async () => {
226
+ return p.skillManager.addAttachment(skillId, file.originalname, file.buffer);
227
+ });
228
+ if (!meta)
229
+ return res.status(404).json({ error: 'Skill not found' });
230
+ res.status(201).json(meta);
231
+ }
232
+ catch (err) {
233
+ next(err);
234
+ }
235
+ });
236
+ // List attachments
237
+ router.get('/:skillId/attachments', (req, res, next) => {
238
+ try {
239
+ const p = getProject(req);
240
+ const attachments = p.skillManager.listAttachments(req.params.skillId);
241
+ res.json({ results: attachments });
242
+ }
243
+ catch (err) {
244
+ next(err);
245
+ }
246
+ });
247
+ // Download attachment
248
+ router.get('/:skillId/attachments/:filename', (req, res, next) => {
249
+ try {
250
+ const p = getProject(req);
251
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
252
+ const filePath = p.skillManager.getAttachmentPath(req.params.skillId, filename);
253
+ if (!filePath)
254
+ return res.status(404).json({ error: 'Attachment not found' });
255
+ const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
256
+ res.setHeader('Content-Type', mimeType);
257
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
258
+ res.setHeader('X-Content-Type-Options', 'nosniff');
259
+ const stream = fs_1.default.createReadStream(filePath);
260
+ stream.on('error', (err) => next(err));
261
+ stream.pipe(res);
262
+ }
263
+ catch (err) {
264
+ next(err);
265
+ }
266
+ });
267
+ // Delete attachment
268
+ router.delete('/:skillId/attachments/:filename', index_1.requireWriteAccess, async (req, res, next) => {
269
+ try {
270
+ const p = getProject(req);
271
+ const skillId = req.params.skillId;
272
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
273
+ const ok = await p.mutationQueue.enqueue(async () => {
274
+ return p.skillManager.removeAttachment(skillId, filename);
275
+ });
276
+ if (!ok)
277
+ return res.status(404).json({ error: 'Attachment not found' });
278
+ res.status(204).end();
279
+ }
280
+ catch (err) {
281
+ next(err);
282
+ }
283
+ });
284
+ return router;
285
+ }