@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.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +512 -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/files.js +64 -0
  7. package/dist/api/rest/graph.js +56 -0
  8. package/dist/api/rest/index.js +117 -0
  9. package/dist/api/rest/knowledge.js +238 -0
  10. package/dist/api/rest/skills.js +284 -0
  11. package/dist/api/rest/tasks.js +272 -0
  12. package/dist/api/rest/tools.js +126 -0
  13. package/dist/api/rest/validation.js +191 -0
  14. package/dist/api/rest/websocket.js +65 -0
  15. package/dist/api/tools/code/get-file-symbols.js +30 -0
  16. package/dist/api/tools/code/get-symbol.js +22 -0
  17. package/dist/api/tools/code/list-files.js +18 -0
  18. package/dist/api/tools/code/search-code.js +27 -0
  19. package/dist/api/tools/code/search-files.js +22 -0
  20. package/dist/api/tools/context/get-context.js +19 -0
  21. package/dist/api/tools/docs/cross-references.js +76 -0
  22. package/dist/api/tools/docs/explain-symbol.js +55 -0
  23. package/dist/api/tools/docs/find-examples.js +52 -0
  24. package/dist/api/tools/docs/get-node.js +24 -0
  25. package/dist/api/tools/docs/get-toc.js +22 -0
  26. package/dist/api/tools/docs/list-snippets.js +46 -0
  27. package/dist/api/tools/docs/list-topics.js +18 -0
  28. package/dist/api/tools/docs/search-files.js +22 -0
  29. package/dist/api/tools/docs/search-snippets.js +43 -0
  30. package/dist/api/tools/docs/search.js +27 -0
  31. package/dist/api/tools/file-index/get-file-info.js +21 -0
  32. package/dist/api/tools/file-index/list-all-files.js +28 -0
  33. package/dist/api/tools/file-index/search-all-files.js +24 -0
  34. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  35. package/dist/api/tools/knowledge/create-note.js +20 -0
  36. package/dist/api/tools/knowledge/create-relation.js +29 -0
  37. package/dist/api/tools/knowledge/delete-note.js +19 -0
  38. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  39. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  40. package/dist/api/tools/knowledge/get-note.js +20 -0
  41. package/dist/api/tools/knowledge/list-notes.js +18 -0
  42. package/dist/api/tools/knowledge/list-relations.js +17 -0
  43. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  44. package/dist/api/tools/knowledge/search-notes.js +25 -0
  45. package/dist/api/tools/knowledge/update-note.js +34 -0
  46. package/dist/api/tools/skills/add-attachment.js +31 -0
  47. package/dist/api/tools/skills/bump-usage.js +19 -0
  48. package/dist/api/tools/skills/create-skill-link.js +25 -0
  49. package/dist/api/tools/skills/create-skill.js +26 -0
  50. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  51. package/dist/api/tools/skills/delete-skill.js +20 -0
  52. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  53. package/dist/api/tools/skills/get-skill.js +21 -0
  54. package/dist/api/tools/skills/link-skill.js +23 -0
  55. package/dist/api/tools/skills/list-skills.js +20 -0
  56. package/dist/api/tools/skills/recall-skills.js +18 -0
  57. package/dist/api/tools/skills/remove-attachment.js +19 -0
  58. package/dist/api/tools/skills/search-skills.js +25 -0
  59. package/dist/api/tools/skills/update-skill.js +58 -0
  60. package/dist/api/tools/tasks/add-attachment.js +31 -0
  61. package/dist/api/tools/tasks/create-task-link.js +25 -0
  62. package/dist/api/tools/tasks/create-task.js +25 -0
  63. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  64. package/dist/api/tools/tasks/delete-task.js +20 -0
  65. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  66. package/dist/api/tools/tasks/get-task.js +20 -0
  67. package/dist/api/tools/tasks/link-task.js +23 -0
  68. package/dist/api/tools/tasks/list-tasks.js +24 -0
  69. package/dist/api/tools/tasks/move-task.js +38 -0
  70. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  71. package/dist/api/tools/tasks/search-tasks.js +25 -0
  72. package/dist/api/tools/tasks/update-task.js +55 -0
  73. package/dist/cli/index.js +451 -0
  74. package/dist/cli/indexer.js +277 -0
  75. package/dist/graphs/attachment-types.js +74 -0
  76. package/dist/graphs/code-types.js +10 -0
  77. package/dist/graphs/code.js +172 -0
  78. package/dist/graphs/docs.js +198 -0
  79. package/dist/graphs/file-index-types.js +10 -0
  80. package/dist/graphs/file-index.js +310 -0
  81. package/dist/graphs/file-lang.js +119 -0
  82. package/dist/graphs/knowledge-types.js +32 -0
  83. package/dist/graphs/knowledge.js +764 -0
  84. package/dist/graphs/manager-types.js +87 -0
  85. package/dist/graphs/skill-types.js +10 -0
  86. package/dist/graphs/skill.js +1013 -0
  87. package/dist/graphs/task-types.js +17 -0
  88. package/dist/graphs/task.js +960 -0
  89. package/dist/lib/embedder.js +101 -0
  90. package/dist/lib/events-log.js +400 -0
  91. package/dist/lib/file-import.js +327 -0
  92. package/dist/lib/file-mirror.js +446 -0
  93. package/dist/lib/frontmatter.js +17 -0
  94. package/dist/lib/mirror-watcher.js +637 -0
  95. package/dist/lib/multi-config.js +254 -0
  96. package/dist/lib/parsers/code.js +246 -0
  97. package/dist/lib/parsers/codeblock.js +66 -0
  98. package/dist/lib/parsers/docs.js +196 -0
  99. package/dist/lib/project-manager.js +418 -0
  100. package/dist/lib/promise-queue.js +22 -0
  101. package/dist/lib/search/bm25.js +167 -0
  102. package/dist/lib/search/code.js +103 -0
  103. package/dist/lib/search/docs.js +108 -0
  104. package/dist/lib/search/file-index.js +31 -0
  105. package/dist/lib/search/files.js +61 -0
  106. package/dist/lib/search/knowledge.js +101 -0
  107. package/dist/lib/search/skills.js +104 -0
  108. package/dist/lib/search/tasks.js +103 -0
  109. package/dist/lib/watcher.js +67 -0
  110. package/package.json +83 -0
  111. package/ui/README.md +54 -0
@@ -0,0 +1,64 @@
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.createFilesRouter = createFilesRouter;
7
+ const path_1 = __importDefault(require("path"));
8
+ const express_1 = require("express");
9
+ const validation_1 = require("../../api/rest/validation");
10
+ function createFilesRouter() {
11
+ const router = (0, express_1.Router)({ mergeParams: true });
12
+ function getProject(req) {
13
+ return req.project;
14
+ }
15
+ // List all files
16
+ router.get('/', (0, validation_1.validateQuery)(validation_1.fileListSchema), (req, res, next) => {
17
+ try {
18
+ const p = getProject(req);
19
+ const q = req.validatedQuery;
20
+ const files = p.fileIndexManager.listAllFiles(q);
21
+ res.json({ results: files });
22
+ }
23
+ catch (err) {
24
+ next(err);
25
+ }
26
+ });
27
+ // Search files
28
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.searchQuerySchema), async (req, res, next) => {
29
+ try {
30
+ const p = getProject(req);
31
+ const q = req.validatedQuery;
32
+ const results = await p.fileIndexManager.search(q.q, {
33
+ topK: q.topK,
34
+ minScore: q.minScore,
35
+ });
36
+ res.json({ results });
37
+ }
38
+ catch (err) {
39
+ next(err);
40
+ }
41
+ });
42
+ // Get file info
43
+ router.get('/info', (req, res, next) => {
44
+ try {
45
+ const p = getProject(req);
46
+ const filePath = req.query.path;
47
+ if (!filePath)
48
+ return res.status(400).json({ error: 'path query parameter required' });
49
+ // Prevent path traversal
50
+ const normalized = path_1.default.normalize(filePath);
51
+ if (normalized.startsWith('..') || path_1.default.isAbsolute(normalized)) {
52
+ return res.status(400).json({ error: 'Invalid path' });
53
+ }
54
+ const info = p.fileIndexManager.getFileInfo(normalized);
55
+ if (!info)
56
+ return res.status(404).json({ error: 'File not found' });
57
+ res.json(info);
58
+ }
59
+ catch (err) {
60
+ next(err);
61
+ }
62
+ });
63
+ return router;
64
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGraphRouter = createGraphRouter;
4
+ const express_1 = require("express");
5
+ const validation_1 = require("../../api/rest/validation");
6
+ function exportGraph(graph, graphName) {
7
+ const nodes = [];
8
+ const edges = [];
9
+ graph.forEachNode((id, attrs) => {
10
+ // Skip proxy nodes and embeddings for transfer size
11
+ const { embedding, fileEmbedding, ...rest } = attrs;
12
+ nodes.push({ id, graph: graphName, ...rest });
13
+ });
14
+ graph.forEachEdge((_edge, attrs, source, target) => {
15
+ edges.push({ source, target, graph: graphName, ...attrs });
16
+ });
17
+ return { nodes, edges };
18
+ }
19
+ function createGraphRouter() {
20
+ const router = (0, express_1.Router)({ mergeParams: true });
21
+ function getProject(req) {
22
+ return req.project;
23
+ }
24
+ router.get('/', (0, validation_1.validateQuery)(validation_1.graphExportSchema), (req, res, next) => {
25
+ try {
26
+ const p = getProject(req);
27
+ const scope = req.validatedQuery.scope;
28
+ const allNodes = [];
29
+ const allEdges = [];
30
+ const add = (g, name) => {
31
+ if (!g)
32
+ return;
33
+ const exp = exportGraph(g, name);
34
+ allNodes.push(...exp.nodes);
35
+ allEdges.push(...exp.edges);
36
+ };
37
+ if (scope === 'all' || scope === 'docs')
38
+ add(p.docGraph, 'docs');
39
+ if (scope === 'all' || scope === 'code')
40
+ add(p.codeGraph, 'code');
41
+ if (scope === 'all' || scope === 'knowledge')
42
+ add(p.knowledgeGraph, 'knowledge');
43
+ if (scope === 'all' || scope === 'tasks')
44
+ add(p.taskGraph, 'tasks');
45
+ if (scope === 'all' || scope === 'files')
46
+ add(p.fileIndexGraph, 'files');
47
+ if (scope === 'all' || scope === 'skills')
48
+ add(p.skillGraph, 'skills');
49
+ res.json({ nodes: allNodes, edges: allEdges });
50
+ }
51
+ catch (err) {
52
+ next(err);
53
+ }
54
+ });
55
+ return router;
56
+ }
@@ -0,0 +1,117 @@
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.createRestApp = createRestApp;
7
+ const path_1 = __importDefault(require("path"));
8
+ const express_1 = __importDefault(require("express"));
9
+ const cors_1 = __importDefault(require("cors"));
10
+ const knowledge_1 = require("../../api/rest/knowledge");
11
+ const tasks_1 = require("../../api/rest/tasks");
12
+ const skills_1 = require("../../api/rest/skills");
13
+ const docs_1 = require("../../api/rest/docs");
14
+ const code_1 = require("../../api/rest/code");
15
+ const files_1 = require("../../api/rest/files");
16
+ const graph_1 = require("../../api/rest/graph");
17
+ const tools_1 = require("../../api/rest/tools");
18
+ /**
19
+ * Create an Express app with all REST routes mounted.
20
+ * Each route uses the ProjectManager to look up project-specific graphs.
21
+ */
22
+ function createRestApp(projectManager) {
23
+ const app = (0, express_1.default)();
24
+ app.use((0, cors_1.default)());
25
+ app.use(express_1.default.json({ limit: '10mb' }));
26
+ // Security headers
27
+ app.use((_req, res, next) => {
28
+ res.setHeader('X-Content-Type-Options', 'nosniff');
29
+ res.setHeader('X-Frame-Options', 'DENY');
30
+ next();
31
+ });
32
+ // Project resolution middleware — injects project instance into req
33
+ app.param('projectId', (req, _res, next, projectId) => {
34
+ const project = projectManager.getProject(projectId);
35
+ if (!project) {
36
+ return _res.status(404).json({ error: `Project "${projectId}" not found` });
37
+ }
38
+ req.project = project;
39
+ next();
40
+ });
41
+ // List projects
42
+ app.get('/api/projects', (_req, res) => {
43
+ const projects = projectManager.listProjects().map(id => {
44
+ const p = projectManager.getProject(id);
45
+ return {
46
+ id,
47
+ projectDir: p.config.projectDir,
48
+ workspaceId: p.workspaceId ?? null,
49
+ stats: {
50
+ docs: p.docGraph ? p.docGraph.order : 0,
51
+ code: p.codeGraph ? p.codeGraph.order : 0,
52
+ knowledge: p.knowledgeGraph.order,
53
+ files: p.fileIndexGraph.order,
54
+ tasks: p.taskGraph.order,
55
+ skills: p.skillGraph.order,
56
+ },
57
+ };
58
+ });
59
+ res.json({ results: projects });
60
+ });
61
+ // List workspaces
62
+ app.get('/api/workspaces', (_req, res) => {
63
+ const workspaces = projectManager.listWorkspaces().map(id => {
64
+ const ws = projectManager.getWorkspace(id);
65
+ return {
66
+ id,
67
+ projects: ws.config.projects,
68
+ };
69
+ });
70
+ res.json({ results: workspaces });
71
+ });
72
+ // Project stats
73
+ app.get('/api/projects/:projectId/stats', (req, res) => {
74
+ const p = req.project;
75
+ res.json({
76
+ docs: p.docGraph ? { nodes: p.docGraph.order, edges: p.docGraph.size } : null,
77
+ code: p.codeGraph ? { nodes: p.codeGraph.order, edges: p.codeGraph.size } : null,
78
+ knowledge: { nodes: p.knowledgeGraph.order, edges: p.knowledgeGraph.size },
79
+ fileIndex: { nodes: p.fileIndexGraph.order, edges: p.fileIndexGraph.size },
80
+ tasks: { nodes: p.taskGraph.order, edges: p.taskGraph.size },
81
+ skills: { nodes: p.skillGraph.order, edges: p.skillGraph.size },
82
+ });
83
+ });
84
+ // Mount domain routers
85
+ app.use('/api/projects/:projectId/knowledge', (0, knowledge_1.createKnowledgeRouter)());
86
+ app.use('/api/projects/:projectId/tasks', (0, tasks_1.createTasksRouter)());
87
+ app.use('/api/projects/:projectId/skills', (0, skills_1.createSkillsRouter)());
88
+ app.use('/api/projects/:projectId/docs', (0, docs_1.createDocsRouter)());
89
+ app.use('/api/projects/:projectId/code', (0, code_1.createCodeRouter)());
90
+ app.use('/api/projects/:projectId/files', (0, files_1.createFilesRouter)());
91
+ app.use('/api/projects/:projectId/graph', (0, graph_1.createGraphRouter)());
92
+ app.use('/api/projects/:projectId/tools', (0, tools_1.createToolsRouter)(projectManager));
93
+ // Serve UI static files (ui/dist)
94
+ const uiDist = path_1.default.resolve(__dirname, '../../../ui/dist');
95
+ app.use(express_1.default.static(uiDist));
96
+ // SPA fallback: serve index.html for non-API routes
97
+ app.get('/{*splat}', (_req, res, next) => {
98
+ if (_req.path.startsWith('/api/'))
99
+ return next();
100
+ res.sendFile(path_1.default.join(uiDist, 'index.html'), (err) => {
101
+ if (err)
102
+ next();
103
+ });
104
+ });
105
+ // Error handler
106
+ app.use((err, _req, res, _next) => {
107
+ if (err.name === 'ZodError') {
108
+ return res.status(400).json({ error: 'Validation error', details: err.issues });
109
+ }
110
+ if (err.type === 'entity.parse.failed' || (err instanceof SyntaxError && 'body' in err)) {
111
+ return res.status(400).json({ error: 'Invalid JSON' });
112
+ }
113
+ process.stderr.write(`[rest] Error: ${err.stack || err}\n`);
114
+ res.status(500).json({ error: 'Internal server error' });
115
+ });
116
+ return app;
117
+ }
@@ -0,0 +1,238 @@
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 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 createKnowledgeRouter() {
15
+ const router = (0, express_1.Router)({ mergeParams: true });
16
+ function getProject(req) {
17
+ return req.project;
18
+ }
19
+ // List notes
20
+ router.get('/notes', (0, validation_1.validateQuery)(validation_1.noteListSchema), (req, res, next) => {
21
+ try {
22
+ const p = getProject(req);
23
+ const q = req.validatedQuery;
24
+ const notes = p.knowledgeManager.listNotes(q.filter, q.tag, q.limit);
25
+ res.json({ results: notes });
26
+ }
27
+ catch (err) {
28
+ next(err);
29
+ }
30
+ });
31
+ // Search notes
32
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.noteSearchSchema), async (req, res, next) => {
33
+ try {
34
+ const p = getProject(req);
35
+ const q = req.validatedQuery;
36
+ const results = await p.knowledgeManager.searchNotes(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
+ // Get note
48
+ router.get('/notes/:noteId', (req, res, next) => {
49
+ try {
50
+ const p = getProject(req);
51
+ const note = p.knowledgeManager.getNote(req.params.noteId);
52
+ if (!note)
53
+ return res.status(404).json({ error: 'Note not found' });
54
+ res.json(note);
55
+ }
56
+ catch (err) {
57
+ next(err);
58
+ }
59
+ });
60
+ // Create note
61
+ router.post('/notes', (0, validation_1.validateBody)(validation_1.createNoteSchema), async (req, res, next) => {
62
+ try {
63
+ const p = getProject(req);
64
+ const { title, content, tags } = req.body;
65
+ const created = await p.mutationQueue.enqueue(async () => {
66
+ const noteId = await p.knowledgeManager.createNote(title, content, tags);
67
+ return p.knowledgeManager.getNote(noteId);
68
+ });
69
+ res.status(201).json(created);
70
+ }
71
+ catch (err) {
72
+ next(err);
73
+ }
74
+ });
75
+ // Update note
76
+ router.put('/notes/:noteId', (0, validation_1.validateBody)(validation_1.updateNoteSchema), async (req, res, next) => {
77
+ try {
78
+ const p = getProject(req);
79
+ const noteId = req.params.noteId;
80
+ const { version, ...patch } = req.body;
81
+ const result = await p.mutationQueue.enqueue(async () => {
82
+ const ok = await p.knowledgeManager.updateNote(noteId, patch, version);
83
+ if (!ok)
84
+ return null;
85
+ return p.knowledgeManager.getNote(noteId);
86
+ });
87
+ if (!result)
88
+ return res.status(404).json({ error: 'Note not found' });
89
+ res.json(result);
90
+ }
91
+ catch (err) {
92
+ if (err instanceof manager_types_1.VersionConflictError) {
93
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
94
+ }
95
+ next(err);
96
+ }
97
+ });
98
+ // Delete note
99
+ router.delete('/notes/:noteId', async (req, res, next) => {
100
+ try {
101
+ const p = getProject(req);
102
+ const noteId = req.params.noteId;
103
+ const ok = await p.mutationQueue.enqueue(async () => {
104
+ return p.knowledgeManager.deleteNote(noteId);
105
+ });
106
+ if (!ok)
107
+ return res.status(404).json({ error: 'Note not found' });
108
+ res.status(204).end();
109
+ }
110
+ catch (err) {
111
+ next(err);
112
+ }
113
+ });
114
+ // Create relation
115
+ router.post('/relations', (0, validation_1.validateBody)(validation_1.createRelationSchema), async (req, res, next) => {
116
+ try {
117
+ const p = getProject(req);
118
+ const { fromId, toId, kind, targetGraph, projectId } = req.body;
119
+ const ok = await p.mutationQueue.enqueue(async () => {
120
+ return p.knowledgeManager.createRelation(fromId, toId, kind, targetGraph, projectId);
121
+ });
122
+ if (!ok)
123
+ return res.status(400).json({ error: 'Failed to create relation' });
124
+ res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
125
+ }
126
+ catch (err) {
127
+ next(err);
128
+ }
129
+ });
130
+ // Delete relation
131
+ router.delete('/relations', (0, validation_1.validateBody)(validation_1.createRelationSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
132
+ try {
133
+ const p = getProject(req);
134
+ const { fromId, toId, targetGraph, projectId } = req.body;
135
+ const ok = await p.mutationQueue.enqueue(async () => {
136
+ return p.knowledgeManager.deleteRelation(fromId, toId, targetGraph, projectId);
137
+ });
138
+ if (!ok)
139
+ return res.status(404).json({ error: 'Relation not found' });
140
+ res.status(204).end();
141
+ }
142
+ catch (err) {
143
+ next(err);
144
+ }
145
+ });
146
+ // List relations for a note
147
+ router.get('/notes/:noteId/relations', (req, res, next) => {
148
+ try {
149
+ const p = getProject(req);
150
+ const relations = p.knowledgeManager.listRelations(req.params.noteId);
151
+ res.json({ results: relations });
152
+ }
153
+ catch (err) {
154
+ next(err);
155
+ }
156
+ });
157
+ // Find notes linked to an external entity
158
+ router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
159
+ try {
160
+ const p = getProject(req);
161
+ const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
162
+ const notes = p.knowledgeManager.findLinkedNotes(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
163
+ res.json({ results: notes });
164
+ }
165
+ catch (err) {
166
+ next(err);
167
+ }
168
+ });
169
+ // -- Attachments --
170
+ // Upload attachment
171
+ router.post('/notes/:noteId/attachments', upload.single('file'), async (req, res, next) => {
172
+ try {
173
+ const p = getProject(req);
174
+ const noteId = req.params.noteId;
175
+ const file = req.file;
176
+ if (!file)
177
+ return res.status(400).json({ error: 'No file uploaded' });
178
+ const meta = await p.mutationQueue.enqueue(async () => {
179
+ return p.knowledgeManager.addAttachment(noteId, file.originalname, file.buffer);
180
+ });
181
+ if (!meta)
182
+ return res.status(404).json({ error: 'Note not found' });
183
+ res.status(201).json(meta);
184
+ }
185
+ catch (err) {
186
+ next(err);
187
+ }
188
+ });
189
+ // List attachments
190
+ router.get('/notes/:noteId/attachments', (req, res, next) => {
191
+ try {
192
+ const p = getProject(req);
193
+ const attachments = p.knowledgeManager.listAttachments(req.params.noteId);
194
+ res.json({ results: attachments });
195
+ }
196
+ catch (err) {
197
+ next(err);
198
+ }
199
+ });
200
+ // Download attachment
201
+ router.get('/notes/:noteId/attachments/:filename', (req, res, next) => {
202
+ try {
203
+ const p = getProject(req);
204
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
205
+ const filePath = p.knowledgeManager.getAttachmentPath(req.params.noteId, filename);
206
+ if (!filePath)
207
+ return res.status(404).json({ error: 'Attachment not found' });
208
+ const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
209
+ res.setHeader('Content-Type', mimeType);
210
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
211
+ res.setHeader('X-Content-Type-Options', 'nosniff');
212
+ const stream = fs_1.default.createReadStream(filePath);
213
+ stream.on('error', (err) => next(err));
214
+ stream.pipe(res);
215
+ }
216
+ catch (err) {
217
+ next(err);
218
+ }
219
+ });
220
+ // Delete attachment
221
+ router.delete('/notes/:noteId/attachments/:filename', async (req, res, next) => {
222
+ try {
223
+ const p = getProject(req);
224
+ const noteId = req.params.noteId;
225
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
226
+ const ok = await p.mutationQueue.enqueue(async () => {
227
+ return p.knowledgeManager.removeAttachment(noteId, filename);
228
+ });
229
+ if (!ok)
230
+ return res.status(404).json({ error: 'Attachment not found' });
231
+ res.status(204).end();
232
+ }
233
+ catch (err) {
234
+ next(err);
235
+ }
236
+ });
237
+ return router;
238
+ }