@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,284 @@
|
|
|
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 manager_types_1 = require("../../graphs/manager-types");
|
|
13
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
|
14
|
+
function createSkillsRouter() {
|
|
15
|
+
const router = (0, express_1.Router)({ mergeParams: true });
|
|
16
|
+
function getProject(req) {
|
|
17
|
+
return req.project;
|
|
18
|
+
}
|
|
19
|
+
// List skills
|
|
20
|
+
router.get('/', (0, validation_1.validateQuery)(validation_1.skillListSchema), (req, res, next) => {
|
|
21
|
+
try {
|
|
22
|
+
const p = getProject(req);
|
|
23
|
+
const q = req.validatedQuery;
|
|
24
|
+
const skills = p.skillManager.listSkills(q);
|
|
25
|
+
res.json({ results: skills });
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
next(err);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// Search skills
|
|
32
|
+
router.get('/search', (0, validation_1.validateQuery)(validation_1.skillSearchSchema), async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const p = getProject(req);
|
|
35
|
+
const q = req.validatedQuery;
|
|
36
|
+
const results = await p.skillManager.searchSkills(q.q, {
|
|
37
|
+
topK: q.topK,
|
|
38
|
+
minScore: q.minScore,
|
|
39
|
+
searchMode: q.searchMode,
|
|
40
|
+
});
|
|
41
|
+
res.json({ results });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
next(err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// Recall skills (higher recall search for task contexts)
|
|
48
|
+
router.get('/recall', (0, validation_1.validateQuery)(validation_1.skillSearchSchema), async (req, res, next) => {
|
|
49
|
+
try {
|
|
50
|
+
const p = getProject(req);
|
|
51
|
+
const q = req.validatedQuery;
|
|
52
|
+
const results = await p.skillManager.searchSkills(q.q, {
|
|
53
|
+
topK: q.topK,
|
|
54
|
+
minScore: q.minScore ?? 0.3,
|
|
55
|
+
searchMode: q.searchMode,
|
|
56
|
+
});
|
|
57
|
+
res.json({ results });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
next(err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// Find skills linked to an external entity
|
|
64
|
+
router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
|
|
65
|
+
try {
|
|
66
|
+
const p = getProject(req);
|
|
67
|
+
const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
|
|
68
|
+
const skills = p.skillManager.findLinkedSkills(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
|
|
69
|
+
res.json({ results: skills });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
next(err);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// Get skill
|
|
76
|
+
router.get('/:skillId', (req, res, next) => {
|
|
77
|
+
try {
|
|
78
|
+
const p = getProject(req);
|
|
79
|
+
const skill = p.skillManager.getSkill(req.params.skillId);
|
|
80
|
+
if (!skill)
|
|
81
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
82
|
+
const relations = p.skillManager.listRelations(req.params.skillId);
|
|
83
|
+
res.json({ ...skill, relations });
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
next(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// Create skill
|
|
90
|
+
router.post('/', (0, validation_1.validateBody)(validation_1.createSkillSchema), async (req, res, next) => {
|
|
91
|
+
try {
|
|
92
|
+
const p = getProject(req);
|
|
93
|
+
const { title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence } = req.body;
|
|
94
|
+
const created = await p.mutationQueue.enqueue(async () => {
|
|
95
|
+
const skillId = await p.skillManager.createSkill(title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence);
|
|
96
|
+
return p.skillManager.getSkill(skillId);
|
|
97
|
+
});
|
|
98
|
+
res.status(201).json(created);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
next(err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Update skill
|
|
105
|
+
router.put('/:skillId', (0, validation_1.validateBody)(validation_1.updateSkillSchema), async (req, res, next) => {
|
|
106
|
+
try {
|
|
107
|
+
const p = getProject(req);
|
|
108
|
+
const skillId = req.params.skillId;
|
|
109
|
+
const { version, ...patch } = req.body;
|
|
110
|
+
const result = await p.mutationQueue.enqueue(async () => {
|
|
111
|
+
const ok = await p.skillManager.updateSkill(skillId, patch, version);
|
|
112
|
+
if (!ok)
|
|
113
|
+
return null;
|
|
114
|
+
return p.skillManager.getSkill(skillId);
|
|
115
|
+
});
|
|
116
|
+
if (!result)
|
|
117
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
118
|
+
res.json(result);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err instanceof manager_types_1.VersionConflictError) {
|
|
122
|
+
return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
|
|
123
|
+
}
|
|
124
|
+
next(err);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Bump usage
|
|
128
|
+
router.post('/:skillId/bump', async (req, res, next) => {
|
|
129
|
+
try {
|
|
130
|
+
const p = getProject(req);
|
|
131
|
+
const skillId = req.params.skillId;
|
|
132
|
+
const result = await p.mutationQueue.enqueue(async () => {
|
|
133
|
+
const ok = p.skillManager.bumpUsage(skillId);
|
|
134
|
+
if (!ok)
|
|
135
|
+
return null;
|
|
136
|
+
return p.skillManager.getSkill(skillId);
|
|
137
|
+
});
|
|
138
|
+
if (!result)
|
|
139
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
140
|
+
res.json(result);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
next(err);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// Delete skill
|
|
147
|
+
router.delete('/:skillId', async (req, res, next) => {
|
|
148
|
+
try {
|
|
149
|
+
const p = getProject(req);
|
|
150
|
+
const skillId = req.params.skillId;
|
|
151
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
152
|
+
return p.skillManager.deleteSkill(skillId);
|
|
153
|
+
});
|
|
154
|
+
if (!ok)
|
|
155
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
156
|
+
res.status(204).end();
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
next(err);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Create skill link (skill-to-skill or cross-graph)
|
|
163
|
+
router.post('/links', (0, validation_1.validateBody)(validation_1.createSkillLinkSchema), async (req, res, next) => {
|
|
164
|
+
try {
|
|
165
|
+
const p = getProject(req);
|
|
166
|
+
const { fromId, toId, kind, targetGraph, projectId } = req.body;
|
|
167
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
168
|
+
if (targetGraph) {
|
|
169
|
+
return p.skillManager.createCrossLink(fromId, toId, targetGraph, kind, projectId);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
return p.skillManager.linkSkills(fromId, toId, kind);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (!ok)
|
|
176
|
+
return res.status(400).json({ error: 'Failed to create link' });
|
|
177
|
+
res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
next(err);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// Delete skill link
|
|
184
|
+
router.delete('/links', (0, validation_1.validateBody)(validation_1.createSkillLinkSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
|
|
185
|
+
try {
|
|
186
|
+
const p = getProject(req);
|
|
187
|
+
const { fromId, toId, targetGraph, projectId } = req.body;
|
|
188
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
189
|
+
if (targetGraph) {
|
|
190
|
+
return p.skillManager.deleteCrossLink(fromId, toId, targetGraph, projectId);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
return p.skillManager.deleteSkillLink(fromId, toId);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
if (!ok)
|
|
197
|
+
return res.status(404).json({ error: 'Link not found' });
|
|
198
|
+
res.status(204).end();
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
next(err);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// List relations for a skill
|
|
205
|
+
router.get('/:skillId/relations', (req, res, next) => {
|
|
206
|
+
try {
|
|
207
|
+
const p = getProject(req);
|
|
208
|
+
const relations = p.skillManager.listRelations(req.params.skillId);
|
|
209
|
+
res.json({ results: relations });
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
next(err);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// -- Attachments --
|
|
216
|
+
// Upload attachment
|
|
217
|
+
router.post('/:skillId/attachments', upload.single('file'), async (req, res, next) => {
|
|
218
|
+
try {
|
|
219
|
+
const p = getProject(req);
|
|
220
|
+
const skillId = req.params.skillId;
|
|
221
|
+
const file = req.file;
|
|
222
|
+
if (!file)
|
|
223
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
224
|
+
const meta = await p.mutationQueue.enqueue(async () => {
|
|
225
|
+
return p.skillManager.addAttachment(skillId, file.originalname, file.buffer);
|
|
226
|
+
});
|
|
227
|
+
if (!meta)
|
|
228
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
229
|
+
res.status(201).json(meta);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
next(err);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// List attachments
|
|
236
|
+
router.get('/:skillId/attachments', (req, res, next) => {
|
|
237
|
+
try {
|
|
238
|
+
const p = getProject(req);
|
|
239
|
+
const attachments = p.skillManager.listAttachments(req.params.skillId);
|
|
240
|
+
res.json({ results: attachments });
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
next(err);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
// Download attachment
|
|
247
|
+
router.get('/:skillId/attachments/:filename', (req, res, next) => {
|
|
248
|
+
try {
|
|
249
|
+
const p = getProject(req);
|
|
250
|
+
const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
|
|
251
|
+
const filePath = p.skillManager.getAttachmentPath(req.params.skillId, filename);
|
|
252
|
+
if (!filePath)
|
|
253
|
+
return res.status(404).json({ error: 'Attachment not found' });
|
|
254
|
+
const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
|
|
255
|
+
res.setHeader('Content-Type', mimeType);
|
|
256
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
257
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
258
|
+
const stream = fs_1.default.createReadStream(filePath);
|
|
259
|
+
stream.on('error', (err) => next(err));
|
|
260
|
+
stream.pipe(res);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
next(err);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// Delete attachment
|
|
267
|
+
router.delete('/:skillId/attachments/:filename', async (req, res, next) => {
|
|
268
|
+
try {
|
|
269
|
+
const p = getProject(req);
|
|
270
|
+
const skillId = req.params.skillId;
|
|
271
|
+
const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
|
|
272
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
273
|
+
return p.skillManager.removeAttachment(skillId, filename);
|
|
274
|
+
});
|
|
275
|
+
if (!ok)
|
|
276
|
+
return res.status(404).json({ error: 'Attachment not found' });
|
|
277
|
+
res.status(204).end();
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
next(err);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return router;
|
|
284
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
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.createTasksRouter = createTasksRouter;
|
|
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 manager_types_1 = require("../../graphs/manager-types");
|
|
13
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
|
14
|
+
function createTasksRouter() {
|
|
15
|
+
const router = (0, express_1.Router)({ mergeParams: true });
|
|
16
|
+
function getProject(req) {
|
|
17
|
+
return req.project;
|
|
18
|
+
}
|
|
19
|
+
// List tasks
|
|
20
|
+
router.get('/', (0, validation_1.validateQuery)(validation_1.taskListSchema), (req, res, next) => {
|
|
21
|
+
try {
|
|
22
|
+
const p = getProject(req);
|
|
23
|
+
const q = req.validatedQuery;
|
|
24
|
+
const tasks = p.taskManager.listTasks(q);
|
|
25
|
+
res.json({ results: tasks });
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
next(err);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// Search tasks
|
|
32
|
+
router.get('/search', (0, validation_1.validateQuery)(validation_1.taskSearchSchema), async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const p = getProject(req);
|
|
35
|
+
const q = req.validatedQuery;
|
|
36
|
+
const results = await p.taskManager.searchTasks(q.q, {
|
|
37
|
+
topK: q.topK,
|
|
38
|
+
minScore: q.minScore,
|
|
39
|
+
searchMode: q.searchMode,
|
|
40
|
+
});
|
|
41
|
+
res.json({ results });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
next(err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// Find tasks linked to an external entity
|
|
48
|
+
router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
|
|
49
|
+
try {
|
|
50
|
+
const p = getProject(req);
|
|
51
|
+
const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
|
|
52
|
+
const tasks = p.taskManager.findLinkedTasks(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
|
|
53
|
+
res.json({ results: tasks });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
next(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Get task
|
|
60
|
+
router.get('/:taskId', (req, res, next) => {
|
|
61
|
+
try {
|
|
62
|
+
const p = getProject(req);
|
|
63
|
+
const task = p.taskManager.getTask(req.params.taskId);
|
|
64
|
+
if (!task)
|
|
65
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
66
|
+
const relations = p.taskManager.listRelations(req.params.taskId);
|
|
67
|
+
res.json({ ...task, relations });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
next(err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Create task
|
|
74
|
+
router.post('/', (0, validation_1.validateBody)(validation_1.createTaskSchema), async (req, res, next) => {
|
|
75
|
+
try {
|
|
76
|
+
const p = getProject(req);
|
|
77
|
+
const { title, description, status, priority, tags, dueDate, estimate } = req.body;
|
|
78
|
+
const created = await p.mutationQueue.enqueue(async () => {
|
|
79
|
+
const taskId = await p.taskManager.createTask(title, description, status, priority, tags, dueDate, estimate);
|
|
80
|
+
return p.taskManager.getTask(taskId);
|
|
81
|
+
});
|
|
82
|
+
res.status(201).json(created);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
next(err);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// Update task
|
|
89
|
+
router.put('/:taskId', (0, validation_1.validateBody)(validation_1.updateTaskSchema), async (req, res, next) => {
|
|
90
|
+
try {
|
|
91
|
+
const p = getProject(req);
|
|
92
|
+
const taskId = req.params.taskId;
|
|
93
|
+
const { version, ...patch } = req.body;
|
|
94
|
+
const result = await p.mutationQueue.enqueue(async () => {
|
|
95
|
+
const ok = await p.taskManager.updateTask(taskId, patch, version);
|
|
96
|
+
if (!ok)
|
|
97
|
+
return null;
|
|
98
|
+
return p.taskManager.getTask(taskId);
|
|
99
|
+
});
|
|
100
|
+
if (!result)
|
|
101
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
102
|
+
res.json(result);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err instanceof manager_types_1.VersionConflictError) {
|
|
106
|
+
return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
|
|
107
|
+
}
|
|
108
|
+
next(err);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// Move task (change status) — action, so POST
|
|
112
|
+
router.post('/:taskId/move', (0, validation_1.validateBody)(validation_1.moveTaskSchema), async (req, res, next) => {
|
|
113
|
+
try {
|
|
114
|
+
const p = getProject(req);
|
|
115
|
+
const taskId = req.params.taskId;
|
|
116
|
+
const { status, version } = req.body;
|
|
117
|
+
const result = await p.mutationQueue.enqueue(async () => {
|
|
118
|
+
const ok = p.taskManager.moveTask(taskId, status, version);
|
|
119
|
+
if (!ok)
|
|
120
|
+
return null;
|
|
121
|
+
return p.taskManager.getTask(taskId);
|
|
122
|
+
});
|
|
123
|
+
if (!result)
|
|
124
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
125
|
+
res.json(result);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof manager_types_1.VersionConflictError) {
|
|
129
|
+
return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
|
|
130
|
+
}
|
|
131
|
+
next(err);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Delete task
|
|
135
|
+
router.delete('/:taskId', async (req, res, next) => {
|
|
136
|
+
try {
|
|
137
|
+
const p = getProject(req);
|
|
138
|
+
const taskId = req.params.taskId;
|
|
139
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
140
|
+
return p.taskManager.deleteTask(taskId);
|
|
141
|
+
});
|
|
142
|
+
if (!ok)
|
|
143
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
144
|
+
res.status(204).end();
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
next(err);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// Create task link (task-to-task or cross-graph)
|
|
151
|
+
router.post('/links', (0, validation_1.validateBody)(validation_1.createTaskLinkSchema), async (req, res, next) => {
|
|
152
|
+
try {
|
|
153
|
+
const p = getProject(req);
|
|
154
|
+
const { fromId, toId, kind, targetGraph, projectId } = req.body;
|
|
155
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
156
|
+
if (targetGraph) {
|
|
157
|
+
return p.taskManager.createCrossLink(fromId, toId, targetGraph, kind, projectId);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
return p.taskManager.linkTasks(fromId, toId, kind);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (!ok)
|
|
164
|
+
return res.status(400).json({ error: 'Failed to create link' });
|
|
165
|
+
res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
next(err);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Delete task link
|
|
172
|
+
router.delete('/links', (0, validation_1.validateBody)(validation_1.createTaskLinkSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
|
|
173
|
+
try {
|
|
174
|
+
const p = getProject(req);
|
|
175
|
+
const { fromId, toId, targetGraph, projectId } = req.body;
|
|
176
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
177
|
+
if (targetGraph) {
|
|
178
|
+
return p.taskManager.deleteCrossLink(fromId, toId, targetGraph, projectId);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
return p.taskManager.deleteTaskLink(fromId, toId);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
if (!ok)
|
|
185
|
+
return res.status(404).json({ error: 'Link not found' });
|
|
186
|
+
res.status(204).end();
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
next(err);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// List relations for a task
|
|
193
|
+
router.get('/:taskId/relations', (req, res, next) => {
|
|
194
|
+
try {
|
|
195
|
+
const p = getProject(req);
|
|
196
|
+
const relations = p.taskManager.listRelations(req.params.taskId);
|
|
197
|
+
res.json({ results: relations });
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
next(err);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// -- Attachments --
|
|
204
|
+
// Upload attachment
|
|
205
|
+
router.post('/:taskId/attachments', upload.single('file'), async (req, res, next) => {
|
|
206
|
+
try {
|
|
207
|
+
const p = getProject(req);
|
|
208
|
+
const taskId = req.params.taskId;
|
|
209
|
+
const file = req.file;
|
|
210
|
+
if (!file)
|
|
211
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
212
|
+
const meta = await p.mutationQueue.enqueue(async () => {
|
|
213
|
+
return p.taskManager.addAttachment(taskId, file.originalname, file.buffer);
|
|
214
|
+
});
|
|
215
|
+
if (!meta)
|
|
216
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
217
|
+
res.status(201).json(meta);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
next(err);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
// List attachments
|
|
224
|
+
router.get('/:taskId/attachments', (req, res, next) => {
|
|
225
|
+
try {
|
|
226
|
+
const p = getProject(req);
|
|
227
|
+
const attachments = p.taskManager.listAttachments(req.params.taskId);
|
|
228
|
+
res.json({ results: attachments });
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
next(err);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// Download attachment
|
|
235
|
+
router.get('/:taskId/attachments/:filename', (req, res, next) => {
|
|
236
|
+
try {
|
|
237
|
+
const p = getProject(req);
|
|
238
|
+
const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
|
|
239
|
+
const filePath = p.taskManager.getAttachmentPath(req.params.taskId, filename);
|
|
240
|
+
if (!filePath)
|
|
241
|
+
return res.status(404).json({ error: 'Attachment not found' });
|
|
242
|
+
const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
|
|
243
|
+
res.setHeader('Content-Type', mimeType);
|
|
244
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
245
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
246
|
+
const stream = fs_1.default.createReadStream(filePath);
|
|
247
|
+
stream.on('error', (err) => next(err));
|
|
248
|
+
stream.pipe(res);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
next(err);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// Delete attachment
|
|
255
|
+
router.delete('/:taskId/attachments/:filename', async (req, res, next) => {
|
|
256
|
+
try {
|
|
257
|
+
const p = getProject(req);
|
|
258
|
+
const taskId = req.params.taskId;
|
|
259
|
+
const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
|
|
260
|
+
const ok = await p.mutationQueue.enqueue(async () => {
|
|
261
|
+
return p.taskManager.removeAttachment(taskId, filename);
|
|
262
|
+
});
|
|
263
|
+
if (!ok)
|
|
264
|
+
return res.status(404).json({ error: 'Attachment not found' });
|
|
265
|
+
res.status(204).end();
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
next(err);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return router;
|
|
272
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createToolsRouter = createToolsRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
6
|
+
const inMemory_js_1 = require("@modelcontextprotocol/sdk/inMemory.js");
|
|
7
|
+
const index_1 = require("../../api/index");
|
|
8
|
+
// Tool category detection based on tool name
|
|
9
|
+
const TOOL_CATEGORIES = {
|
|
10
|
+
get_context: 'context',
|
|
11
|
+
list_topics: 'docs', get_toc: 'docs', search: 'docs', get_node: 'docs',
|
|
12
|
+
search_topic_files: 'docs', find_examples: 'docs', search_snippets: 'docs',
|
|
13
|
+
list_snippets: 'docs', explain_symbol: 'docs', cross_references: 'cross-graph',
|
|
14
|
+
list_files: 'code', get_file_symbols: 'code', search_code: 'code',
|
|
15
|
+
get_symbol: 'code', search_files: 'code',
|
|
16
|
+
list_all_files: 'files', search_all_files: 'files', get_file_info: 'files',
|
|
17
|
+
create_note: 'knowledge', update_note: 'knowledge', delete_note: 'knowledge',
|
|
18
|
+
get_note: 'knowledge', list_notes: 'knowledge', search_notes: 'knowledge',
|
|
19
|
+
create_relation: 'knowledge', delete_relation: 'knowledge',
|
|
20
|
+
list_relations: 'knowledge', find_linked_notes: 'knowledge',
|
|
21
|
+
add_note_attachment: 'knowledge', remove_note_attachment: 'knowledge',
|
|
22
|
+
create_task: 'tasks', update_task: 'tasks', delete_task: 'tasks',
|
|
23
|
+
get_task: 'tasks', list_tasks: 'tasks', search_tasks: 'tasks',
|
|
24
|
+
move_task: 'tasks', link_task: 'tasks', create_task_link: 'tasks',
|
|
25
|
+
delete_task_link: 'tasks', find_linked_tasks: 'tasks',
|
|
26
|
+
add_task_attachment: 'tasks', remove_task_attachment: 'tasks',
|
|
27
|
+
create_skill: 'skills', update_skill: 'skills', delete_skill: 'skills',
|
|
28
|
+
get_skill: 'skills', list_skills: 'skills', search_skills: 'skills',
|
|
29
|
+
recall_skills: 'skills', bump_skill_usage: 'skills',
|
|
30
|
+
link_skill: 'skills', create_skill_link: 'skills', delete_skill_link: 'skills',
|
|
31
|
+
find_linked_skills: 'skills',
|
|
32
|
+
add_skill_attachment: 'skills', remove_skill_attachment: 'skills',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Get or create a lazy MCP client for a project instance.
|
|
36
|
+
* The client is cached on the instance for reuse.
|
|
37
|
+
*/
|
|
38
|
+
async function getClient(p, pm) {
|
|
39
|
+
if (p.mcpClient)
|
|
40
|
+
return p.mcpClient;
|
|
41
|
+
// Build session context for get_context tool
|
|
42
|
+
const ws = p.workspaceId ? pm.getWorkspace(p.workspaceId) : undefined;
|
|
43
|
+
const sessionCtx = {
|
|
44
|
+
projectId: p.id,
|
|
45
|
+
workspaceId: ws?.id,
|
|
46
|
+
workspaceProjects: ws?.config.projects,
|
|
47
|
+
};
|
|
48
|
+
const [serverTransport, clientTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
|
|
49
|
+
const server = (0, index_1.createMcpServer)(p.docGraph, p.codeGraph, p.knowledgeGraph, p.fileIndexGraph, p.taskGraph, p.embedFns, p.mutationQueue, p.config.projectDir, p.skillGraph, sessionCtx);
|
|
50
|
+
await server.connect(serverTransport);
|
|
51
|
+
const client = new index_js_1.Client({ name: 'tools-explorer', version: '1.0.0' });
|
|
52
|
+
await client.connect(clientTransport);
|
|
53
|
+
p.mcpClient = client;
|
|
54
|
+
p.mcpClientCleanup = async () => {
|
|
55
|
+
await client.close();
|
|
56
|
+
p.mcpClient = undefined;
|
|
57
|
+
p.mcpClientCleanup = undefined;
|
|
58
|
+
};
|
|
59
|
+
return client;
|
|
60
|
+
}
|
|
61
|
+
function createToolsRouter(projectManager) {
|
|
62
|
+
const router = (0, express_1.Router)({ mergeParams: true });
|
|
63
|
+
function getProject(req) {
|
|
64
|
+
return req.project;
|
|
65
|
+
}
|
|
66
|
+
// List all available tools
|
|
67
|
+
router.get('/', async (req, res, next) => {
|
|
68
|
+
try {
|
|
69
|
+
const p = getProject(req);
|
|
70
|
+
const client = await getClient(p, projectManager);
|
|
71
|
+
const { tools } = await client.listTools();
|
|
72
|
+
const results = tools.map(t => ({
|
|
73
|
+
name: t.name,
|
|
74
|
+
description: t.description || '',
|
|
75
|
+
category: TOOL_CATEGORIES[t.name] || 'other',
|
|
76
|
+
inputSchema: t.inputSchema,
|
|
77
|
+
}));
|
|
78
|
+
res.json({ results });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
next(err);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// Get single tool info
|
|
85
|
+
router.get('/:toolName', async (req, res, next) => {
|
|
86
|
+
try {
|
|
87
|
+
const p = getProject(req);
|
|
88
|
+
const client = await getClient(p, projectManager);
|
|
89
|
+
const { tools } = await client.listTools();
|
|
90
|
+
const tool = tools.find(t => t.name === req.params.toolName);
|
|
91
|
+
if (!tool)
|
|
92
|
+
return res.status(404).json({ error: `Tool "${req.params.toolName}" not found` });
|
|
93
|
+
res.json({
|
|
94
|
+
name: tool.name,
|
|
95
|
+
description: tool.description || '',
|
|
96
|
+
category: TOOL_CATEGORIES[tool.name] || 'other',
|
|
97
|
+
inputSchema: tool.inputSchema,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
next(err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Call a tool
|
|
105
|
+
router.post('/:toolName/call', async (req, res, next) => {
|
|
106
|
+
try {
|
|
107
|
+
const p = getProject(req);
|
|
108
|
+
const client = await getClient(p, projectManager);
|
|
109
|
+
const start = Date.now();
|
|
110
|
+
const result = await client.callTool({
|
|
111
|
+
name: req.params.toolName,
|
|
112
|
+
arguments: req.body.arguments || {},
|
|
113
|
+
});
|
|
114
|
+
const duration = Date.now() - start;
|
|
115
|
+
res.json({
|
|
116
|
+
result: result.content,
|
|
117
|
+
isError: result.isError || false,
|
|
118
|
+
duration,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
next(err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return router;
|
|
126
|
+
}
|