@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,273 @@
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 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 createTasksRouter() {
16
+ const router = (0, express_1.Router)({ mergeParams: true });
17
+ function getProject(req) {
18
+ return req.project;
19
+ }
20
+ // List tasks
21
+ router.get('/', (0, validation_1.validateQuery)(validation_1.taskListSchema), (req, res, next) => {
22
+ try {
23
+ const p = getProject(req);
24
+ const q = req.validatedQuery;
25
+ const tasks = p.taskManager.listTasks(q);
26
+ res.json({ results: tasks });
27
+ }
28
+ catch (err) {
29
+ next(err);
30
+ }
31
+ });
32
+ // Search tasks
33
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.taskSearchSchema), async (req, res, next) => {
34
+ try {
35
+ const p = getProject(req);
36
+ const q = req.validatedQuery;
37
+ const results = await p.taskManager.searchTasks(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
+ // Find tasks linked to an external entity
49
+ router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
50
+ try {
51
+ const p = getProject(req);
52
+ const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
53
+ const tasks = p.taskManager.findLinkedTasks(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
54
+ res.json({ results: tasks });
55
+ }
56
+ catch (err) {
57
+ next(err);
58
+ }
59
+ });
60
+ // Get task
61
+ router.get('/:taskId', (req, res, next) => {
62
+ try {
63
+ const p = getProject(req);
64
+ const task = p.taskManager.getTask(req.params.taskId);
65
+ if (!task)
66
+ return res.status(404).json({ error: 'Task not found' });
67
+ const relations = p.taskManager.listRelations(req.params.taskId);
68
+ res.json({ ...task, relations });
69
+ }
70
+ catch (err) {
71
+ next(err);
72
+ }
73
+ });
74
+ // Create task
75
+ router.post('/', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createTaskSchema), async (req, res, next) => {
76
+ try {
77
+ const p = getProject(req);
78
+ const { title, description, status, priority, tags, dueDate, estimate, assignee } = req.body;
79
+ const created = await p.mutationQueue.enqueue(async () => {
80
+ const taskId = await p.taskManager.createTask(title, description, status, priority, tags, dueDate, estimate, assignee ?? null);
81
+ return p.taskManager.getTask(taskId);
82
+ });
83
+ res.status(201).json(created);
84
+ }
85
+ catch (err) {
86
+ next(err);
87
+ }
88
+ });
89
+ // Update task
90
+ router.put('/:taskId', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.updateTaskSchema), async (req, res, next) => {
91
+ try {
92
+ const p = getProject(req);
93
+ const taskId = req.params.taskId;
94
+ const { version, ...patch } = req.body;
95
+ const result = await p.mutationQueue.enqueue(async () => {
96
+ const ok = await p.taskManager.updateTask(taskId, patch, version);
97
+ if (!ok)
98
+ return null;
99
+ return p.taskManager.getTask(taskId);
100
+ });
101
+ if (!result)
102
+ return res.status(404).json({ error: 'Task not found' });
103
+ res.json(result);
104
+ }
105
+ catch (err) {
106
+ if (err instanceof manager_types_1.VersionConflictError) {
107
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
108
+ }
109
+ next(err);
110
+ }
111
+ });
112
+ // Move task (change status) — action, so POST
113
+ router.post('/:taskId/move', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.moveTaskSchema), async (req, res, next) => {
114
+ try {
115
+ const p = getProject(req);
116
+ const taskId = req.params.taskId;
117
+ const { status, version } = req.body;
118
+ const result = await p.mutationQueue.enqueue(async () => {
119
+ const ok = p.taskManager.moveTask(taskId, status, version);
120
+ if (!ok)
121
+ return null;
122
+ return p.taskManager.getTask(taskId);
123
+ });
124
+ if (!result)
125
+ return res.status(404).json({ error: 'Task not found' });
126
+ res.json(result);
127
+ }
128
+ catch (err) {
129
+ if (err instanceof manager_types_1.VersionConflictError) {
130
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
131
+ }
132
+ next(err);
133
+ }
134
+ });
135
+ // Delete task
136
+ router.delete('/:taskId', index_1.requireWriteAccess, async (req, res, next) => {
137
+ try {
138
+ const p = getProject(req);
139
+ const taskId = req.params.taskId;
140
+ const ok = await p.mutationQueue.enqueue(async () => {
141
+ return p.taskManager.deleteTask(taskId);
142
+ });
143
+ if (!ok)
144
+ return res.status(404).json({ error: 'Task not found' });
145
+ res.status(204).end();
146
+ }
147
+ catch (err) {
148
+ next(err);
149
+ }
150
+ });
151
+ // Create task link (task-to-task or cross-graph)
152
+ router.post('/links', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createTaskLinkSchema), async (req, res, next) => {
153
+ try {
154
+ const p = getProject(req);
155
+ const { fromId, toId, kind, targetGraph, projectId } = req.body;
156
+ const ok = await p.mutationQueue.enqueue(async () => {
157
+ if (targetGraph) {
158
+ return p.taskManager.createCrossLink(fromId, toId, targetGraph, kind, projectId);
159
+ }
160
+ else {
161
+ return p.taskManager.linkTasks(fromId, toId, kind);
162
+ }
163
+ });
164
+ if (!ok)
165
+ return res.status(400).json({ error: 'Failed to create link' });
166
+ res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
167
+ }
168
+ catch (err) {
169
+ next(err);
170
+ }
171
+ });
172
+ // Delete task link
173
+ router.delete('/links', index_1.requireWriteAccess, (0, validation_1.validateBody)(validation_1.createTaskLinkSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
174
+ try {
175
+ const p = getProject(req);
176
+ const { fromId, toId, targetGraph, projectId } = req.body;
177
+ const ok = await p.mutationQueue.enqueue(async () => {
178
+ if (targetGraph) {
179
+ return p.taskManager.deleteCrossLink(fromId, toId, targetGraph, projectId);
180
+ }
181
+ else {
182
+ return p.taskManager.deleteTaskLink(fromId, toId);
183
+ }
184
+ });
185
+ if (!ok)
186
+ return res.status(404).json({ error: 'Link not found' });
187
+ res.status(204).end();
188
+ }
189
+ catch (err) {
190
+ next(err);
191
+ }
192
+ });
193
+ // List relations for a task
194
+ router.get('/:taskId/relations', (req, res, next) => {
195
+ try {
196
+ const p = getProject(req);
197
+ const relations = p.taskManager.listRelations(req.params.taskId);
198
+ res.json({ results: relations });
199
+ }
200
+ catch (err) {
201
+ next(err);
202
+ }
203
+ });
204
+ // -- Attachments --
205
+ // Upload attachment
206
+ router.post('/:taskId/attachments', index_1.requireWriteAccess, upload.single('file'), async (req, res, next) => {
207
+ try {
208
+ const p = getProject(req);
209
+ const taskId = req.params.taskId;
210
+ const file = req.file;
211
+ if (!file)
212
+ return res.status(400).json({ error: 'No file uploaded' });
213
+ const meta = await p.mutationQueue.enqueue(async () => {
214
+ return p.taskManager.addAttachment(taskId, file.originalname, file.buffer);
215
+ });
216
+ if (!meta)
217
+ return res.status(404).json({ error: 'Task not found' });
218
+ res.status(201).json(meta);
219
+ }
220
+ catch (err) {
221
+ next(err);
222
+ }
223
+ });
224
+ // List attachments
225
+ router.get('/:taskId/attachments', (req, res, next) => {
226
+ try {
227
+ const p = getProject(req);
228
+ const attachments = p.taskManager.listAttachments(req.params.taskId);
229
+ res.json({ results: attachments });
230
+ }
231
+ catch (err) {
232
+ next(err);
233
+ }
234
+ });
235
+ // Download attachment
236
+ router.get('/:taskId/attachments/:filename', (req, res, next) => {
237
+ try {
238
+ const p = getProject(req);
239
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
240
+ const filePath = p.taskManager.getAttachmentPath(req.params.taskId, filename);
241
+ if (!filePath)
242
+ return res.status(404).json({ error: 'Attachment not found' });
243
+ const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
244
+ res.setHeader('Content-Type', mimeType);
245
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
246
+ res.setHeader('X-Content-Type-Options', 'nosniff');
247
+ const stream = fs_1.default.createReadStream(filePath);
248
+ stream.on('error', (err) => next(err));
249
+ stream.pipe(res);
250
+ }
251
+ catch (err) {
252
+ next(err);
253
+ }
254
+ });
255
+ // Delete attachment
256
+ router.delete('/:taskId/attachments/:filename', index_1.requireWriteAccess, async (req, res, next) => {
257
+ try {
258
+ const p = getProject(req);
259
+ const taskId = req.params.taskId;
260
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
261
+ const ok = await p.mutationQueue.enqueue(async () => {
262
+ return p.taskManager.removeAttachment(taskId, filename);
263
+ });
264
+ if (!ok)
265
+ return res.status(404).json({ error: 'Attachment not found' });
266
+ res.status(204).end();
267
+ }
268
+ catch (err) {
269
+ next(err);
270
+ }
271
+ });
272
+ return router;
273
+ }
@@ -0,0 +1,157 @@
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
+ const MUTATION_PREFIXES = ['create_', 'update_', 'delete_', 'move_', 'link_', 'add_', 'remove_', 'bump_'];
35
+ function isMutationTool(toolName) {
36
+ return MUTATION_PREFIXES.some(p => toolName.startsWith(p));
37
+ }
38
+ /**
39
+ * Get or create a lazy MCP client for a project instance.
40
+ * The client is cached on the instance for reuse.
41
+ */
42
+ async function getClient(p, pm) {
43
+ if (p.mcpClient)
44
+ return p.mcpClient;
45
+ // Build session context for get_context tool
46
+ const ws = p.workspaceId ? pm.getWorkspace(p.workspaceId) : undefined;
47
+ const sessionCtx = {
48
+ projectId: p.id,
49
+ workspaceId: ws?.id,
50
+ workspaceProjects: ws?.config.projects,
51
+ };
52
+ const [serverTransport, clientTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
53
+ 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);
54
+ await server.connect(serverTransport);
55
+ const client = new index_js_1.Client({ name: 'tools-explorer', version: '1.0.0' });
56
+ await client.connect(clientTransport);
57
+ p.mcpClient = client;
58
+ p.mcpClientCleanup = async () => {
59
+ await client.close();
60
+ p.mcpClient = undefined;
61
+ p.mcpClientCleanup = undefined;
62
+ };
63
+ return client;
64
+ }
65
+ function createToolsRouter(projectManager, checkAccess) {
66
+ const router = (0, express_1.Router)({ mergeParams: true });
67
+ function getProject(req) {
68
+ return req.project;
69
+ }
70
+ // List all available tools (filtered by access)
71
+ router.get('/', async (req, res, next) => {
72
+ try {
73
+ const p = getProject(req);
74
+ const client = await getClient(p, projectManager);
75
+ const { tools } = await client.listTools();
76
+ const results = tools
77
+ .filter(t => {
78
+ if (!checkAccess)
79
+ return true;
80
+ const cat = TOOL_CATEGORIES[t.name];
81
+ if (!cat || cat === 'context' || cat === 'cross-graph')
82
+ return true;
83
+ return checkAccess(req, cat, 'r');
84
+ })
85
+ .map(t => ({
86
+ name: t.name,
87
+ description: t.description || '',
88
+ category: TOOL_CATEGORIES[t.name] || 'other',
89
+ inputSchema: t.inputSchema,
90
+ }));
91
+ res.json({ results });
92
+ }
93
+ catch (err) {
94
+ next(err);
95
+ }
96
+ });
97
+ // Get single tool info
98
+ router.get('/:toolName', async (req, res, next) => {
99
+ try {
100
+ const p = getProject(req);
101
+ const client = await getClient(p, projectManager);
102
+ const { tools } = await client.listTools();
103
+ const tool = tools.find(t => t.name === req.params.toolName);
104
+ if (!tool)
105
+ return res.status(404).json({ error: `Tool "${req.params.toolName}" not found` });
106
+ // Check read access for the tool's graph
107
+ if (checkAccess) {
108
+ const cat = TOOL_CATEGORIES[tool.name];
109
+ if (cat && cat !== 'context' && cat !== 'cross-graph' && !checkAccess(req, cat, 'r')) {
110
+ return res.status(403).json({ error: 'Access denied' });
111
+ }
112
+ }
113
+ res.json({
114
+ name: tool.name,
115
+ description: tool.description || '',
116
+ category: TOOL_CATEGORIES[tool.name] || 'other',
117
+ inputSchema: tool.inputSchema,
118
+ });
119
+ }
120
+ catch (err) {
121
+ next(err);
122
+ }
123
+ });
124
+ // Call a tool
125
+ router.post('/:toolName/call', async (req, res, next) => {
126
+ try {
127
+ const p = getProject(req);
128
+ const toolName = req.params.toolName;
129
+ // Check access: read for queries, write for mutations
130
+ if (checkAccess) {
131
+ const cat = TOOL_CATEGORIES[toolName];
132
+ if (cat && cat !== 'context' && cat !== 'cross-graph') {
133
+ const level = isMutationTool(toolName) ? 'rw' : 'r';
134
+ if (!checkAccess(req, cat, level)) {
135
+ return res.status(403).json({ error: level === 'rw' ? 'Read-only access' : 'Access denied' });
136
+ }
137
+ }
138
+ }
139
+ const client = await getClient(p, projectManager);
140
+ const start = Date.now();
141
+ const result = await client.callTool({
142
+ name: toolName,
143
+ arguments: req.body.arguments || {},
144
+ });
145
+ const duration = Date.now() - start;
146
+ res.json({
147
+ result: result.content,
148
+ isError: result.isError || false,
149
+ duration,
150
+ });
151
+ }
152
+ catch (err) {
153
+ next(err);
154
+ }
155
+ });
156
+ return router;
157
+ }
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachmentFilenameSchema = exports.linkedQuerySchema = exports.graphExportSchema = exports.skillListSchema = exports.skillSearchSchema = exports.createSkillLinkSchema = exports.updateSkillSchema = exports.createSkillSchema = exports.fileListSchema = exports.listQuerySchema = exports.searchQuerySchema = exports.taskListSchema = exports.taskSearchSchema = exports.createTaskLinkSchema = exports.moveTaskSchema = exports.updateTaskSchema = exports.createTaskSchema = exports.noteListSchema = exports.noteSearchSchema = exports.createRelationSchema = exports.updateNoteSchema = exports.createNoteSchema = void 0;
4
+ exports.validateBody = validateBody;
5
+ exports.validateQuery = validateQuery;
6
+ const zod_1 = require("zod");
7
+ function validateBody(schema) {
8
+ return (req, _res, next) => {
9
+ req.body = schema.parse(req.body);
10
+ next();
11
+ };
12
+ }
13
+ function validateQuery(schema) {
14
+ return (req, _res, next) => {
15
+ req.validatedQuery = schema.parse(req.query);
16
+ next();
17
+ };
18
+ }
19
+ // ---------------------------------------------------------------------------
20
+ // Knowledge schemas
21
+ // ---------------------------------------------------------------------------
22
+ exports.createNoteSchema = zod_1.z.object({
23
+ title: zod_1.z.string().min(1).max(500),
24
+ content: zod_1.z.string().max(1_000_000),
25
+ tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional().default([]),
26
+ });
27
+ exports.updateNoteSchema = zod_1.z.object({
28
+ title: zod_1.z.string().min(1).max(500).optional(),
29
+ content: zod_1.z.string().max(1_000_000).optional(),
30
+ tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional(),
31
+ version: zod_1.z.number().int().positive().optional(),
32
+ });
33
+ exports.createRelationSchema = zod_1.z.object({
34
+ fromId: zod_1.z.string().min(1),
35
+ toId: zod_1.z.string().min(1),
36
+ kind: zod_1.z.string().min(1),
37
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks', 'skills']).optional(),
38
+ projectId: zod_1.z.string().min(1).optional(),
39
+ });
40
+ exports.noteSearchSchema = zod_1.z.object({
41
+ q: zod_1.z.string().min(1).max(2000),
42
+ topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
43
+ minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
44
+ searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
45
+ });
46
+ exports.noteListSchema = zod_1.z.object({
47
+ filter: zod_1.z.string().optional(),
48
+ tag: zod_1.z.string().optional(),
49
+ limit: zod_1.z.coerce.number().int().positive().optional(),
50
+ });
51
+ // ---------------------------------------------------------------------------
52
+ // Task schemas
53
+ // ---------------------------------------------------------------------------
54
+ exports.createTaskSchema = zod_1.z.object({
55
+ title: zod_1.z.string().min(1).max(500),
56
+ description: zod_1.z.string().max(500_000).default(''),
57
+ status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).default('todo'),
58
+ priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).default('medium'),
59
+ tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional().default([]),
60
+ dueDate: zod_1.z.number().nullable().optional(),
61
+ estimate: zod_1.z.number().nullable().optional(),
62
+ assignee: zod_1.z.string().max(100).nullable().optional(),
63
+ });
64
+ exports.updateTaskSchema = zod_1.z.object({
65
+ title: zod_1.z.string().min(1).max(500).optional(),
66
+ description: zod_1.z.string().max(500_000).optional(),
67
+ status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).optional(),
68
+ priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).optional(),
69
+ tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional(),
70
+ dueDate: zod_1.z.number().nullable().optional(),
71
+ estimate: zod_1.z.number().nullable().optional(),
72
+ assignee: zod_1.z.string().max(100).nullable().optional(),
73
+ version: zod_1.z.number().int().positive().optional(),
74
+ });
75
+ exports.moveTaskSchema = zod_1.z.object({
76
+ status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']),
77
+ version: zod_1.z.number().int().positive().optional(),
78
+ });
79
+ exports.createTaskLinkSchema = zod_1.z.object({
80
+ fromId: zod_1.z.string().min(1),
81
+ toId: zod_1.z.string().min(1),
82
+ kind: zod_1.z.string().min(1),
83
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'skills']).optional(),
84
+ projectId: zod_1.z.string().min(1).optional(),
85
+ });
86
+ exports.taskSearchSchema = zod_1.z.object({
87
+ q: zod_1.z.string().min(1).max(2000),
88
+ topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
89
+ minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
90
+ searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
91
+ });
92
+ exports.taskListSchema = zod_1.z.object({
93
+ status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).optional(),
94
+ priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).optional(),
95
+ tag: zod_1.z.string().optional(),
96
+ filter: zod_1.z.string().optional(),
97
+ assignee: zod_1.z.string().optional(),
98
+ limit: zod_1.z.coerce.number().int().positive().optional(),
99
+ });
100
+ // ---------------------------------------------------------------------------
101
+ // Search schemas (docs, code, files)
102
+ // ---------------------------------------------------------------------------
103
+ exports.searchQuerySchema = zod_1.z.object({
104
+ q: zod_1.z.string().min(1).max(2000),
105
+ topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
106
+ minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
107
+ searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
108
+ });
109
+ exports.listQuerySchema = zod_1.z.object({
110
+ filter: zod_1.z.string().optional(),
111
+ limit: zod_1.z.coerce.number().int().positive().optional(),
112
+ });
113
+ // ---------------------------------------------------------------------------
114
+ // File index schemas
115
+ // ---------------------------------------------------------------------------
116
+ exports.fileListSchema = zod_1.z.object({
117
+ directory: zod_1.z.string().optional(),
118
+ extension: zod_1.z.string().optional(),
119
+ language: zod_1.z.string().optional(),
120
+ filter: zod_1.z.string().optional(),
121
+ limit: zod_1.z.coerce.number().int().positive().optional(),
122
+ });
123
+ // ---------------------------------------------------------------------------
124
+ // Graph export schema
125
+ // ---------------------------------------------------------------------------
126
+ // ---------------------------------------------------------------------------
127
+ // Skill schemas
128
+ // ---------------------------------------------------------------------------
129
+ exports.createSkillSchema = zod_1.z.object({
130
+ title: zod_1.z.string().min(1).max(500),
131
+ description: zod_1.z.string().max(500_000).default(''),
132
+ steps: zod_1.z.array(zod_1.z.string().max(10_000)).max(100).optional().default([]),
133
+ triggers: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional().default([]),
134
+ inputHints: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional().default([]),
135
+ filePatterns: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional().default([]),
136
+ tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional().default([]),
137
+ source: zod_1.z.enum(['user', 'learned']).default('user'),
138
+ confidence: zod_1.z.number().min(0).max(1).default(1),
139
+ });
140
+ exports.updateSkillSchema = zod_1.z.object({
141
+ title: zod_1.z.string().min(1).max(500).optional(),
142
+ description: zod_1.z.string().max(500_000).optional(),
143
+ steps: zod_1.z.array(zod_1.z.string().max(10_000)).max(100).optional(),
144
+ triggers: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional(),
145
+ inputHints: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional(),
146
+ filePatterns: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional(),
147
+ tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional(),
148
+ source: zod_1.z.enum(['user', 'learned']).optional(),
149
+ confidence: zod_1.z.number().min(0).max(1).optional(),
150
+ version: zod_1.z.number().int().positive().optional(),
151
+ });
152
+ exports.createSkillLinkSchema = zod_1.z.object({
153
+ fromId: zod_1.z.string().min(1),
154
+ toId: zod_1.z.string().min(1),
155
+ kind: zod_1.z.string().min(1),
156
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'tasks']).optional(),
157
+ projectId: zod_1.z.string().min(1).optional(),
158
+ });
159
+ exports.skillSearchSchema = zod_1.z.object({
160
+ q: zod_1.z.string().min(1).max(2000),
161
+ topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
162
+ minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
163
+ searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
164
+ });
165
+ exports.skillListSchema = zod_1.z.object({
166
+ source: zod_1.z.enum(['user', 'learned']).optional(),
167
+ tag: zod_1.z.string().optional(),
168
+ filter: zod_1.z.string().optional(),
169
+ limit: zod_1.z.coerce.number().int().positive().optional(),
170
+ });
171
+ exports.graphExportSchema = zod_1.z.object({
172
+ scope: zod_1.z.enum(['all', 'docs', 'code', 'knowledge', 'tasks', 'files', 'skills']).default('all'),
173
+ });
174
+ // ---------------------------------------------------------------------------
175
+ // Attachment schemas
176
+ // ---------------------------------------------------------------------------
177
+ // ---------------------------------------------------------------------------
178
+ // Linked query schema (cross-graph reverse lookup)
179
+ // ---------------------------------------------------------------------------
180
+ exports.linkedQuerySchema = zod_1.z.object({
181
+ targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'tasks', 'skills']),
182
+ targetNodeId: zod_1.z.string().min(1).max(500),
183
+ kind: zod_1.z.string().max(100).optional(),
184
+ projectId: zod_1.z.string().max(200).optional(),
185
+ });
186
+ // ---------------------------------------------------------------------------
187
+ // Attachment schemas
188
+ // ---------------------------------------------------------------------------
189
+ /** Validates an attachment filename (path param). No path separators, no .., no dangerous chars. */
190
+ exports.attachmentFilenameSchema = zod_1.z.string()
191
+ .min(1)
192
+ .max(255)
193
+ .refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
194
+ .refine(s => !s.includes('..'), 'Filename must not contain ..')
195
+ .refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
196
+ .refine(s => !/[\x00-\x1f\x7f"<>|?*]/.test(s), 'Filename contains invalid characters');