@romandp/context-memory-mcp 1.0.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 (91) hide show
  1. package/README.md +454 -0
  2. package/bin/mcp-memory.js +306 -0
  3. package/dist/config/paths.d.ts +10 -0
  4. package/dist/config/paths.d.ts.map +1 -0
  5. package/dist/config/paths.js +41 -0
  6. package/dist/config/paths.js.map +1 -0
  7. package/dist/db/init.d.ts +6 -0
  8. package/dist/db/init.d.ts.map +1 -0
  9. package/dist/db/init.js +37 -0
  10. package/dist/db/init.js.map +1 -0
  11. package/dist/db/migrations/runner.d.ts +9 -0
  12. package/dist/db/migrations/runner.d.ts.map +1 -0
  13. package/dist/db/migrations/runner.js +285 -0
  14. package/dist/db/migrations/runner.js.map +1 -0
  15. package/dist/db/schema.d.ts +51 -0
  16. package/dist/db/schema.d.ts.map +1 -0
  17. package/dist/db/schema.js +496 -0
  18. package/dist/db/schema.js.map +1 -0
  19. package/dist/http/routes.d.ts +3 -0
  20. package/dist/http/routes.d.ts.map +1 -0
  21. package/dist/http/routes.js +968 -0
  22. package/dist/http/routes.js.map +1 -0
  23. package/dist/http/server.d.ts +5 -0
  24. package/dist/http/server.d.ts.map +1 -0
  25. package/dist/http/server.js +45 -0
  26. package/dist/http/server.js.map +1 -0
  27. package/dist/http/swagger.d.ts +2 -0
  28. package/dist/http/swagger.d.ts.map +1 -0
  29. package/dist/http/swagger.js +101 -0
  30. package/dist/http/swagger.js.map +1 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +139 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/memory/budget.d.ts +22 -0
  36. package/dist/memory/budget.d.ts.map +1 -0
  37. package/dist/memory/budget.js +51 -0
  38. package/dist/memory/budget.js.map +1 -0
  39. package/dist/memory/facts.d.ts +3 -0
  40. package/dist/memory/facts.d.ts.map +1 -0
  41. package/dist/memory/facts.js +63 -0
  42. package/dist/memory/facts.js.map +1 -0
  43. package/dist/memory/rebuild.d.ts +4 -0
  44. package/dist/memory/rebuild.d.ts.map +1 -0
  45. package/dist/memory/rebuild.js +31 -0
  46. package/dist/memory/rebuild.js.map +1 -0
  47. package/dist/memory/response.d.ts +11 -0
  48. package/dist/memory/response.d.ts.map +1 -0
  49. package/dist/memory/response.js +20 -0
  50. package/dist/memory/response.js.map +1 -0
  51. package/dist/memory/summary.d.ts +3 -0
  52. package/dist/memory/summary.d.ts.map +1 -0
  53. package/dist/memory/summary.js +67 -0
  54. package/dist/memory/summary.js.map +1 -0
  55. package/dist/memory/toon.d.ts +20 -0
  56. package/dist/memory/toon.d.ts.map +1 -0
  57. package/dist/memory/toon.js +102 -0
  58. package/dist/memory/toon.js.map +1 -0
  59. package/dist/memory/view.d.ts +4 -0
  60. package/dist/memory/view.d.ts.map +1 -0
  61. package/dist/memory/view.js +7 -0
  62. package/dist/memory/view.js.map +1 -0
  63. package/dist/server/setup.d.ts +9 -0
  64. package/dist/server/setup.d.ts.map +1 -0
  65. package/dist/server/setup.js +291 -0
  66. package/dist/server/setup.js.map +1 -0
  67. package/dist/tools/audit.d.ts +2 -0
  68. package/dist/tools/audit.d.ts.map +1 -0
  69. package/dist/tools/audit.js +22 -0
  70. package/dist/tools/audit.js.map +1 -0
  71. package/dist/tools/context.d.ts +5 -0
  72. package/dist/tools/context.d.ts.map +1 -0
  73. package/dist/tools/context.js +104 -0
  74. package/dist/tools/context.js.map +1 -0
  75. package/dist/tools/entry.d.ts +9 -0
  76. package/dist/tools/entry.d.ts.map +1 -0
  77. package/dist/tools/entry.js +214 -0
  78. package/dist/tools/entry.js.map +1 -0
  79. package/dist/tools/project.d.ts +4 -0
  80. package/dist/tools/project.d.ts.map +1 -0
  81. package/dist/tools/project.js +54 -0
  82. package/dist/tools/project.js.map +1 -0
  83. package/dist/tools/task.d.ts +4 -0
  84. package/dist/tools/task.d.ts.map +1 -0
  85. package/dist/tools/task.js +49 -0
  86. package/dist/tools/task.js.map +1 -0
  87. package/dist/types/context.d.ts +139 -0
  88. package/dist/types/context.d.ts.map +1 -0
  89. package/dist/types/context.js +2 -0
  90. package/dist/types/context.js.map +1 -0
  91. package/package.json +68 -0
@@ -0,0 +1,968 @@
1
+ import { Router } from 'express';
2
+ import { randomUUID } from 'crypto';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { createProject, getProject, getAllProjects, updateProject, deleteProject, createEntry, getEntry, getProjectEntries, updateEntry, deleteEntry, searchEntries, searchAllEntries, createTask, getTask, getProjectTasks, updateTask, deleteTask, addClassification, getClassifications, getAuditLog, addDesignDecision, addEntryRelationship, getCompactEntry, getCompactEntryContext, getEntryContext, getProjectCompact, listMemoryFacts, searchCompactEntries, } from '../db/schema.js';
6
+ import { getDbPath } from '../db/init.js';
7
+ import { rebuildEntryMemory } from '../memory/rebuild.js';
8
+ import { applyCompactEntryCharBudget, applyCursor, applyFactCharBudget, applyItemBudget, buildNextCursor } from '../memory/budget.js';
9
+ import { withMetrics } from '../memory/response.js';
10
+ import { parseFormat, parseView } from '../memory/view.js';
11
+ import { setToonMeta, toToonEntryContext, toToonEntrySummary, toToonFacts, toToonProjectCompact, toToonSearchResults } from '../memory/toon.js';
12
+ const router = Router();
13
+ /**
14
+ * @swagger
15
+ * /health:
16
+ * get:
17
+ * summary: Health check
18
+ * tags: [Utility]
19
+ * responses:
20
+ * 200:
21
+ * description: Server is up and running
22
+ * content:
23
+ * application/json:
24
+ * schema:
25
+ * type: object
26
+ * properties:
27
+ * status: { type: string, example: ok }
28
+ * timestamp: { type: string, format: date-time }
29
+ */
30
+ router.get('/health', (_req, res) => {
31
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
32
+ });
33
+ // ---- PROJECTS ----
34
+ /**
35
+ * @swagger
36
+ * /api/projects:
37
+ * get:
38
+ * summary: List all projects
39
+ * tags: [Projects]
40
+ * parameters:
41
+ * - in: query
42
+ * name: page
43
+ * schema: { type: integer, minimum: 1 }
44
+ * description: Page number
45
+ * - in: query
46
+ * name: limit
47
+ * schema: { type: integer, minimum: 1, maximum: 200 }
48
+ * description: Items per page
49
+ * responses:
50
+ * 200:
51
+ * description: List of projects
52
+ * content:
53
+ * application/json:
54
+ * schema:
55
+ * type: object
56
+ * properties:
57
+ * success: { type: boolean }
58
+ * projects: { type: array, items: { $ref: '#/components/schemas/Project' } }
59
+ * pagination: { $ref: '#/components/schemas/Pagination' }
60
+ */
61
+ router.get('/api/projects', (req, res) => {
62
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
63
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
64
+ const { data, total } = getAllProjects(page || limit ? { page, limit } : undefined);
65
+ const result = { success: true, projects: data };
66
+ if (page || limit) {
67
+ const pageNum = page || 1;
68
+ const limitNum = limit || total;
69
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
70
+ }
71
+ res.json(result);
72
+ });
73
+ /**
74
+ * @swagger
75
+ * /api/projects:
76
+ * post:
77
+ * summary: Create a new project
78
+ * tags: [Projects]
79
+ * requestBody:
80
+ * required: true
81
+ * content:
82
+ * application/json:
83
+ * schema:
84
+ * type: object
85
+ * required: [name]
86
+ * properties:
87
+ * name: { type: string }
88
+ * description: { type: string }
89
+ * responses:
90
+ * 201:
91
+ * description: Project created
92
+ * 400:
93
+ * description: Invalid input
94
+ */
95
+ router.post('/api/projects', (req, res) => {
96
+ const { name, description } = req.body;
97
+ if (!name) {
98
+ res.status(400).json({ success: false, message: 'name is required' });
99
+ return;
100
+ }
101
+ const id = randomUUID();
102
+ const now = new Date().toISOString();
103
+ const project = { id, name, description, status: 'active', created_at: now, updated_at: now };
104
+ createProject(project);
105
+ res.status(201).json({ success: true, id, message: `Project "${name}" created` });
106
+ });
107
+ /**
108
+ * @swagger
109
+ * /api/projects/{id}:
110
+ * get:
111
+ * summary: Get project details
112
+ * tags: [Projects]
113
+ * parameters:
114
+ * - in: path
115
+ * name: id
116
+ * required: true
117
+ * schema: { type: string, format: uuid }
118
+ * responses:
119
+ * 200:
120
+ * description: Project details with entries and tasks
121
+ * 404:
122
+ * description: Project not found
123
+ */
124
+ router.get('/api/projects/:id', (req, res) => {
125
+ const id = req.params.id;
126
+ const project = getProject(id);
127
+ if (!project) {
128
+ res.status(404).json({ success: false, message: 'Project not found' });
129
+ return;
130
+ }
131
+ const { data: entries } = getProjectEntries(id);
132
+ const { data: tasks } = getProjectTasks(id);
133
+ const classifications = getClassifications('project', id);
134
+ res.json({ success: true, project, entries, tasks, classifications });
135
+ });
136
+ /**
137
+ * @swagger
138
+ * /api/projects/{id}:
139
+ * put:
140
+ * summary: Update project
141
+ * tags: [Projects]
142
+ * parameters:
143
+ * - in: path
144
+ * name: id
145
+ * required: true
146
+ * schema: { type: string, format: uuid }
147
+ * requestBody:
148
+ * content:
149
+ * application/json:
150
+ * schema:
151
+ * type: object
152
+ * properties:
153
+ * name: { type: string }
154
+ * description: { type: string }
155
+ * status: { type: string, enum: [active, archived, completed] }
156
+ * responses:
157
+ * 200:
158
+ * description: Project updated
159
+ * 404:
160
+ * description: Project not found
161
+ */
162
+ router.put('/api/projects/:id', (req, res) => {
163
+ const id = req.params.id;
164
+ if (!getProject(id)) {
165
+ res.status(404).json({ success: false, message: 'Project not found' });
166
+ return;
167
+ }
168
+ updateProject(id, req.body);
169
+ res.json({ success: true, message: 'Project updated' });
170
+ });
171
+ /**
172
+ * @swagger
173
+ * /api/projects/{id}:
174
+ * delete:
175
+ * summary: Delete project
176
+ * tags: [Projects]
177
+ * parameters:
178
+ * - in: path
179
+ * name: id
180
+ * required: true
181
+ * schema: { type: string, format: uuid }
182
+ * responses:
183
+ * 200:
184
+ * description: Project deleted
185
+ * 404:
186
+ * description: Project not found
187
+ */
188
+ router.delete('/api/projects/:id', (req, res) => {
189
+ const id = req.params.id;
190
+ if (!getProject(id)) {
191
+ res.status(404).json({ success: false, message: 'Project not found' });
192
+ return;
193
+ }
194
+ deleteProject(id);
195
+ res.json({ success: true, message: 'Project deleted' });
196
+ });
197
+ // ---- SDD ENTRIES ----
198
+ /**
199
+ * @swagger
200
+ * /api/projects/{pid}/entries:
201
+ * get:
202
+ * summary: List entries in a project
203
+ * tags: [Entries]
204
+ * parameters:
205
+ * - in: path
206
+ * name: pid
207
+ * required: true
208
+ * schema: { type: string, format: uuid }
209
+ * - in: query
210
+ * name: section
211
+ * schema: { type: string, enum: [plan, design, tasks, general] }
212
+ * - in: query
213
+ * name: page
214
+ * schema: { type: integer }
215
+ * - in: query
216
+ * name: limit
217
+ * schema: { type: integer }
218
+ * responses:
219
+ * 200:
220
+ * description: List of entries
221
+ */
222
+ router.get('/api/projects/:pid/entries', (req, res) => {
223
+ const pid = req.params.pid;
224
+ const section = req.query.section;
225
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
226
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
227
+ const { data, total } = getProjectEntries(pid, section, page || limit ? { page, limit } : undefined);
228
+ const result = { success: true, count: data.length, entries: data };
229
+ if (page || limit) {
230
+ const pageNum = page || 1;
231
+ const limitNum = limit || total;
232
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
233
+ }
234
+ res.json(result);
235
+ });
236
+ /**
237
+ * @swagger
238
+ * /api/projects/{pid}/entries:
239
+ * post:
240
+ * summary: Create entry in project
241
+ * tags: [Entries]
242
+ * parameters:
243
+ * - in: path
244
+ * name: pid
245
+ * required: true
246
+ * schema: { type: string, format: uuid }
247
+ * requestBody:
248
+ * required: true
249
+ * content:
250
+ * application/json:
251
+ * schema:
252
+ * type: object
253
+ * required: [section, title]
254
+ * properties:
255
+ * section: { type: string, enum: [plan, design, tasks, general] }
256
+ * title: { type: string }
257
+ * content: { type: string }
258
+ * status: { type: string, enum: [draft, review, done] }
259
+ * parent_id: { type: string, format: uuid }
260
+ * responses:
261
+ * 201:
262
+ * description: Entry created
263
+ */
264
+ router.post('/api/projects/:pid/entries', (req, res) => {
265
+ const pid = req.params.pid;
266
+ if (!getProject(pid)) {
267
+ res.status(404).json({ success: false, message: 'Project not found' });
268
+ return;
269
+ }
270
+ const { section, title, content, status, parent_id } = req.body;
271
+ if (!section || !title) {
272
+ res.status(400).json({ success: false, message: 'section and title are required' });
273
+ return;
274
+ }
275
+ const id = randomUUID();
276
+ const now = new Date().toISOString();
277
+ const entry = { id, project_id: pid, section, title, content: content || '', status: status || 'draft', parent_id, created_at: now, updated_at: now };
278
+ createEntry(entry);
279
+ rebuildEntryMemory(id);
280
+ res.status(201).json({ success: true, id, message: `Entry created in ${section}` });
281
+ });
282
+ /**
283
+ * @swagger
284
+ * /api/entries/search:
285
+ * get:
286
+ * summary: Global search of entries
287
+ * tags: [Search]
288
+ * parameters:
289
+ * - in: query
290
+ * name: q
291
+ * required: true
292
+ * schema: { type: string }
293
+ * description: Search query
294
+ * - in: query
295
+ * name: page
296
+ * schema: { type: integer }
297
+ * - in: query
298
+ * name: limit
299
+ * schema: { type: integer }
300
+ * responses:
301
+ * 200:
302
+ * description: Search results
303
+ */
304
+ router.get('/api/entries/search', (req, res) => {
305
+ const query = req.query.q;
306
+ if (!query) {
307
+ res.status(400).json({ success: false, message: 'query param q is required' });
308
+ return;
309
+ }
310
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
311
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
312
+ const maxItems = req.query.max_items ? parseInt(req.query.max_items, 10) : undefined;
313
+ const maxChars = req.query.max_chars ? parseInt(req.query.max_chars, 10) : undefined;
314
+ const cursor = req.query.cursor;
315
+ const view = parseView(req.query.view);
316
+ const format = parseFormat(req.query.format);
317
+ if (view !== 'full') {
318
+ const { data, total } = searchCompactEntries(query, undefined, page || limit ? { page, limit } : undefined);
319
+ const cursorData = applyCursor(data, cursor);
320
+ const itemBudget = applyItemBudget(cursorData, maxItems);
321
+ const charBudget = applyCompactEntryCharBudget(itemBudget.items, maxChars);
322
+ const truncated = itemBudget.truncated || charBudget.truncated;
323
+ const nextCursor = buildNextCursor(cursorData, charBudget.items, truncated);
324
+ const result = format === 'json' ? withMetrics({ success: true, count: charBudget.items.length, results: charBudget.items, truncated, next_cursor: nextCursor }) : setToonMeta(toToonSearchResults(charBudget.items, format, { truncated, next: nextCursor }), { truncated, next: nextCursor, count: charBudget.items.length });
325
+ if (format === 'json' && (page || limit)) {
326
+ const pageNum = page || 1;
327
+ const limitNum = limit || total;
328
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
329
+ }
330
+ res.json(result);
331
+ return;
332
+ }
333
+ const { data, total } = searchAllEntries(query, page || limit ? { page, limit } : undefined);
334
+ const result = { success: true, count: data.length, results: data };
335
+ if (page || limit) {
336
+ const pageNum = page || 1;
337
+ const limitNum = limit || total;
338
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
339
+ }
340
+ res.json(result);
341
+ });
342
+ /**
343
+ * @swagger
344
+ * /api/projects/{pid}/entries/search:
345
+ * get:
346
+ * summary: Search entries within a project
347
+ * tags: [Search]
348
+ * parameters:
349
+ * - in: path
350
+ * name: pid
351
+ * required: true
352
+ * schema: { type: string, format: uuid }
353
+ * - in: query
354
+ * name: q
355
+ * required: true
356
+ * schema: { type: string }
357
+ * - in: query
358
+ * name: page
359
+ * schema: { type: integer }
360
+ * - in: query
361
+ * name: limit
362
+ * schema: { type: integer }
363
+ * responses:
364
+ * 200:
365
+ * description: Search results
366
+ */
367
+ router.get('/api/projects/:pid/entries/search', (req, res) => {
368
+ const pid = req.params.pid;
369
+ const query = req.query.q;
370
+ if (!query) {
371
+ res.status(400).json({ success: false, message: 'query param q is required' });
372
+ return;
373
+ }
374
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
375
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
376
+ const maxItems = req.query.max_items ? parseInt(req.query.max_items, 10) : undefined;
377
+ const maxChars = req.query.max_chars ? parseInt(req.query.max_chars, 10) : undefined;
378
+ const cursor = req.query.cursor;
379
+ const view = parseView(req.query.view);
380
+ const format = parseFormat(req.query.format);
381
+ if (view !== 'full') {
382
+ const { data, total } = searchCompactEntries(query, pid, page || limit ? { page, limit } : undefined);
383
+ const cursorData = applyCursor(data, cursor);
384
+ const itemBudget = applyItemBudget(cursorData, maxItems);
385
+ const charBudget = applyCompactEntryCharBudget(itemBudget.items, maxChars);
386
+ const truncated = itemBudget.truncated || charBudget.truncated;
387
+ const nextCursor = buildNextCursor(cursorData, charBudget.items, truncated);
388
+ const result = format === 'json' ? withMetrics({ success: true, count: charBudget.items.length, results: charBudget.items, truncated, next_cursor: nextCursor }) : setToonMeta(toToonSearchResults(charBudget.items, format, { truncated, next: nextCursor }), { truncated, next: nextCursor, count: charBudget.items.length });
389
+ if (format === 'json' && (page || limit)) {
390
+ const pageNum = page || 1;
391
+ const limitNum = limit || total;
392
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
393
+ }
394
+ res.json(result);
395
+ return;
396
+ }
397
+ const { data, total } = searchEntries(pid, query, page || limit ? { page, limit } : undefined);
398
+ const result = { success: true, count: data.length, results: data };
399
+ if (page || limit) {
400
+ const pageNum = page || 1;
401
+ const limitNum = limit || total;
402
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
403
+ }
404
+ res.json(result);
405
+ });
406
+ /**
407
+ * @swagger
408
+ * /api/entries/{eid}/context:
409
+ * get:
410
+ * summary: Get full context for an entry
411
+ * tags: [Context]
412
+ * parameters:
413
+ * - in: path
414
+ * name: eid
415
+ * required: true
416
+ * schema: { type: string, format: uuid }
417
+ * responses:
418
+ * 200:
419
+ * description: Entry context (decisions, relationships)
420
+ */
421
+ router.get('/api/entries/:eid/context', (req, res) => {
422
+ const eid = req.params.eid;
423
+ const view = parseView(req.query.view);
424
+ const format = parseFormat(req.query.format);
425
+ if (view !== 'full') {
426
+ const ctx = getCompactEntryContext(eid);
427
+ if (!ctx) {
428
+ res.status(404).json({ success: false, message: 'Entry not found' });
429
+ return;
430
+ }
431
+ res.json(format === 'json' ? withMetrics({ success: true, context: ctx }) : setToonMeta(toToonEntryContext(ctx, format), { count: 1 }));
432
+ return;
433
+ }
434
+ const ctx = getEntryContext(eid);
435
+ if (!ctx) {
436
+ res.status(404).json({ success: false, message: 'Entry not found' });
437
+ return;
438
+ }
439
+ res.json({ success: true, context: ctx });
440
+ });
441
+ router.get('/api/entries/:eid/summary', (req, res) => {
442
+ const eid = req.params.eid;
443
+ const format = parseFormat(req.query.format);
444
+ const entry = getCompactEntry(eid);
445
+ if (!entry) {
446
+ res.status(404).json({ success: false, message: 'Entry not found' });
447
+ return;
448
+ }
449
+ res.json(format === 'json' ? withMetrics({ success: true, entry }) : setToonMeta(toToonEntrySummary(entry, format), { count: 1 }));
450
+ });
451
+ router.get('/api/entries/batch', (req, res) => {
452
+ const idsParam = req.query.ids;
453
+ if (!idsParam) {
454
+ res.status(400).json({ success: false, message: 'query param ids is required' });
455
+ return;
456
+ }
457
+ const format = parseFormat(req.query.format);
458
+ const maxItems = req.query.max_items ? parseInt(req.query.max_items, 10) : undefined;
459
+ const maxChars = req.query.max_chars ? parseInt(req.query.max_chars, 10) : undefined;
460
+ const cursor = req.query.cursor;
461
+ const entries = applyCursor(idsParam.split(',').map(id => id.trim()).filter(Boolean).map(id => getCompactEntry(id)).filter((entry) => entry !== null), cursor);
462
+ const itemBudget = applyItemBudget(entries, maxItems);
463
+ const charBudget = applyCompactEntryCharBudget(itemBudget.items, maxChars);
464
+ const truncated = itemBudget.truncated || charBudget.truncated;
465
+ const nextCursor = buildNextCursor(entries, charBudget.items, truncated);
466
+ res.json(format === 'json'
467
+ ? withMetrics({ success: true, count: charBudget.items.length, entries: charBudget.items, truncated, next_cursor: nextCursor })
468
+ : setToonMeta(toToonSearchResults(charBudget.items, format, { truncated, next: nextCursor }), { truncated, next: nextCursor, count: charBudget.items.length }));
469
+ });
470
+ router.get('/api/entries/:eid/compact', (req, res) => {
471
+ const eid = req.params.eid;
472
+ const format = parseFormat(req.query.format);
473
+ const ctx = getCompactEntryContext(eid);
474
+ if (!ctx) {
475
+ res.status(404).json({ success: false, message: 'Entry not found' });
476
+ return;
477
+ }
478
+ res.json(format === 'json' ? withMetrics({ success: true, context: ctx }) : setToonMeta(toToonEntryContext(ctx, format), { count: 1 }));
479
+ });
480
+ router.get('/api/projects/:id/compact', (req, res) => {
481
+ const id = req.params.id;
482
+ const format = parseFormat(req.query.format);
483
+ const entryLimit = req.query.entry_limit ? parseInt(req.query.entry_limit, 10) : undefined;
484
+ const taskLimit = req.query.task_limit ? parseInt(req.query.task_limit, 10) : undefined;
485
+ const compact = getProjectCompact(id, { entryLimit, taskLimit });
486
+ if (!compact) {
487
+ res.status(404).json({ success: false, message: 'Project not found' });
488
+ return;
489
+ }
490
+ res.json(format === 'json' ? withMetrics({ success: true, ...compact }) : setToonMeta(toToonProjectCompact(compact, format), { count: compact.entries.length + compact.tasks.length }));
491
+ });
492
+ router.get('/api/facts', (req, res) => {
493
+ const format = parseFormat(req.query.format);
494
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
495
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
496
+ const maxItems = req.query.max_items ? parseInt(req.query.max_items, 10) : undefined;
497
+ const maxChars = req.query.max_chars ? parseInt(req.query.max_chars, 10) : undefined;
498
+ const cursor = req.query.cursor;
499
+ const { data, total } = listMemoryFacts({
500
+ project_id: req.query.project_id,
501
+ entry_id: req.query.entry_id,
502
+ kind: req.query.kind,
503
+ }, page || limit ? { page, limit } : undefined);
504
+ const cursorData = applyCursor(data, cursor);
505
+ const itemBudget = applyItemBudget(cursorData, maxItems);
506
+ const charBudget = applyFactCharBudget(itemBudget.items, maxChars);
507
+ const truncated = itemBudget.truncated || charBudget.truncated;
508
+ const nextCursor = buildNextCursor(cursorData, charBudget.items, truncated);
509
+ const result = format === 'json' ? withMetrics({ success: true, count: charBudget.items.length, facts: charBudget.items, truncated, next_cursor: nextCursor }) : setToonMeta(toToonFacts(charBudget.items, format), { truncated, next: nextCursor, count: charBudget.items.length });
510
+ if (format === 'json' && (page || limit)) {
511
+ const pageNum = page || 1;
512
+ const limitNum = limit || total;
513
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
514
+ }
515
+ res.json(result);
516
+ });
517
+ /**
518
+ * @swagger
519
+ * /api/entries/{eid}/decisions:
520
+ * post:
521
+ * summary: Record a design decision
522
+ * tags: [Context]
523
+ * parameters:
524
+ * - in: path
525
+ * name: eid
526
+ * required: true
527
+ * schema: { type: string, format: uuid }
528
+ * requestBody:
529
+ * required: true
530
+ * content:
531
+ * application/json:
532
+ * schema:
533
+ * type: object
534
+ * required: [decision, rationale]
535
+ * properties:
536
+ * decision: { type: string }
537
+ * rationale: { type: string }
538
+ * alternatives_considered: { type: string }
539
+ * responses:
540
+ * 201:
541
+ * description: Decision recorded
542
+ */
543
+ router.post('/api/entries/:eid/decisions', (req, res) => {
544
+ const eid = req.params.eid;
545
+ if (!getEntry(eid)) {
546
+ res.status(404).json({ success: false, message: 'Entry not found' });
547
+ return;
548
+ }
549
+ const { decision, rationale, alternatives_considered } = req.body;
550
+ if (!decision || !rationale) {
551
+ res.status(400).json({ success: false, message: 'decision and rationale required' });
552
+ return;
553
+ }
554
+ const dd = {
555
+ id: randomUUID(), entry_id: eid, decision, rationale,
556
+ alternatives_considered, created_at: new Date().toISOString(),
557
+ };
558
+ addDesignDecision(dd);
559
+ rebuildEntryMemory(eid);
560
+ res.status(201).json({ success: true, id: dd.id, message: `Decision "${decision}" recorded` });
561
+ });
562
+ /**
563
+ * @swagger
564
+ * /api/entries/{eid}/relationships:
565
+ * post:
566
+ * summary: Relate two entries
567
+ * tags: [Context]
568
+ * parameters:
569
+ * - in: path
570
+ * name: eid
571
+ * required: true
572
+ * schema: { type: string, format: uuid }
573
+ * requestBody:
574
+ * required: true
575
+ * content:
576
+ * application/json:
577
+ * schema:
578
+ * type: object
579
+ * required: [target_entry_id, relationship_type]
580
+ * properties:
581
+ * target_entry_id: { type: string, format: uuid }
582
+ * relationship_type: { type: string, enum: [depends_on, implements, related_to, supersedes] }
583
+ * responses:
584
+ * 201:
585
+ * description: Relationship created
586
+ */
587
+ router.post('/api/entries/:eid/relationships', (req, res) => {
588
+ const eid = req.params.eid;
589
+ if (!getEntry(eid)) {
590
+ res.status(404).json({ success: false, message: 'Source entry not found' });
591
+ return;
592
+ }
593
+ const { target_entry_id, relationship_type } = req.body;
594
+ if (!target_entry_id || !relationship_type) {
595
+ res.status(400).json({ success: false, message: 'target_entry_id and relationship_type required' });
596
+ return;
597
+ }
598
+ if (!getEntry(target_entry_id)) {
599
+ res.status(404).json({ success: false, message: 'Target entry not found' });
600
+ return;
601
+ }
602
+ const rel = {
603
+ id: randomUUID(), source_entry_id: eid, target_entry_id,
604
+ relationship_type, created_at: new Date().toISOString(),
605
+ };
606
+ addEntryRelationship(rel);
607
+ rebuildEntryMemory(eid);
608
+ rebuildEntryMemory(target_entry_id);
609
+ res.status(201).json({ success: true, id: rel.id, message: `Relationship "${relationship_type}" created` });
610
+ });
611
+ /**
612
+ * @swagger
613
+ * /api/projects/{pid}/entries/{eid}:
614
+ * get:
615
+ * summary: Get entry details
616
+ * tags: [Entries]
617
+ * parameters:
618
+ * - in: path
619
+ * name: pid
620
+ * required: true
621
+ * schema: { type: string, format: uuid }
622
+ * - in: path
623
+ * name: eid
624
+ * required: true
625
+ * schema: { type: string, format: uuid }
626
+ * responses:
627
+ * 200:
628
+ * description: Entry details
629
+ */
630
+ router.get('/api/projects/:pid/entries/:eid', (req, res) => {
631
+ const eid = req.params.eid;
632
+ const view = parseView(req.query.view);
633
+ const format = parseFormat(req.query.format);
634
+ if (view !== 'full') {
635
+ const compactEntry = getCompactEntry(eid);
636
+ if (!compactEntry) {
637
+ res.status(404).json({ success: false, message: 'Entry not found' });
638
+ return;
639
+ }
640
+ res.json(format === 'json' ? { success: true, entry: compactEntry } : toToonEntrySummary(compactEntry, format));
641
+ return;
642
+ }
643
+ const entry = getEntry(eid);
644
+ if (!entry) {
645
+ res.status(404).json({ success: false, message: 'Entry not found' });
646
+ return;
647
+ }
648
+ const classifications = getClassifications('entry', eid);
649
+ res.json({ success: true, entry, classifications });
650
+ });
651
+ /**
652
+ * @swagger
653
+ * /api/projects/{pid}/entries/{eid}:
654
+ * put:
655
+ * summary: Update entry
656
+ * tags: [Entries]
657
+ * parameters:
658
+ * - in: path
659
+ * name: pid
660
+ * required: true
661
+ * schema: { type: string, format: uuid }
662
+ * - in: path
663
+ * name: eid
664
+ * required: true
665
+ * schema: { type: string, format: uuid }
666
+ * requestBody:
667
+ * content:
668
+ * application/json:
669
+ * schema:
670
+ * type: object
671
+ * properties:
672
+ * title: { type: string }
673
+ * content: { type: string }
674
+ * status: { type: string, enum: [draft, review, done] }
675
+ * section: { type: string, enum: [plan, design, tasks, general] }
676
+ * responses:
677
+ * 200:
678
+ * description: Entry updated
679
+ */
680
+ router.put('/api/projects/:pid/entries/:eid', (req, res) => {
681
+ const eid = req.params.eid;
682
+ if (!getEntry(eid)) {
683
+ res.status(404).json({ success: false, message: 'Entry not found' });
684
+ return;
685
+ }
686
+ updateEntry(eid, req.body);
687
+ rebuildEntryMemory(eid);
688
+ res.json({ success: true, message: 'Entry updated' });
689
+ });
690
+ /**
691
+ * @swagger
692
+ * /api/projects/{pid}/entries/{eid}:
693
+ * delete:
694
+ * summary: Delete entry
695
+ * tags: [Entries]
696
+ * parameters:
697
+ * - in: path
698
+ * name: pid
699
+ * required: true
700
+ * schema: { type: string, format: uuid }
701
+ * - in: path
702
+ * name: eid
703
+ * required: true
704
+ * schema: { type: string, format: uuid }
705
+ * responses:
706
+ * 200:
707
+ * description: Entry deleted
708
+ */
709
+ router.delete('/api/projects/:pid/entries/:eid', (req, res) => {
710
+ const eid = req.params.eid;
711
+ if (!getEntry(eid)) {
712
+ res.status(404).json({ success: false, message: 'Entry not found' });
713
+ return;
714
+ }
715
+ deleteEntry(eid);
716
+ res.json({ success: true, message: 'Entry deleted' });
717
+ });
718
+ // ---- TASKS ----
719
+ /**
720
+ * @swagger
721
+ * /api/projects/{pid}/tasks:
722
+ * get:
723
+ * summary: List tasks in a project
724
+ * tags: [Tasks]
725
+ * parameters:
726
+ * - in: path
727
+ * name: pid
728
+ * required: true
729
+ * schema: { type: string, format: uuid }
730
+ * - in: query
731
+ * name: entry_id
732
+ * schema: { type: string, format: uuid }
733
+ * - in: query
734
+ * name: page
735
+ * schema: { type: integer }
736
+ * - in: query
737
+ * name: limit
738
+ * schema: { type: integer }
739
+ * responses:
740
+ * 200:
741
+ * description: List of tasks
742
+ */
743
+ router.get('/api/projects/:pid/tasks', (req, res) => {
744
+ const pid = req.params.pid;
745
+ const entryId = req.query.entry_id;
746
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
747
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
748
+ const { data, total } = getProjectTasks(pid, entryId, page || limit ? { page, limit } : undefined);
749
+ const result = { success: true, count: data.length, tasks: data };
750
+ if (page || limit) {
751
+ const pageNum = page || 1;
752
+ const limitNum = limit || total;
753
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
754
+ }
755
+ res.json(result);
756
+ });
757
+ /**
758
+ * @swagger
759
+ * /api/projects/{pid}/tasks:
760
+ * post:
761
+ * summary: Create task in project
762
+ * tags: [Tasks]
763
+ * parameters:
764
+ * - in: path
765
+ * name: pid
766
+ * required: true
767
+ * schema: { type: string, format: uuid }
768
+ * requestBody:
769
+ * required: true
770
+ * content:
771
+ * application/json:
772
+ * schema:
773
+ * type: object
774
+ * required: [title]
775
+ * properties:
776
+ * title: { type: string }
777
+ * description: { type: string }
778
+ * priority: { type: string, enum: [low, medium, high, critical] }
779
+ * sdd_entry_id: { type: string, format: uuid }
780
+ * responses:
781
+ * 201:
782
+ * description: Task created
783
+ */
784
+ router.post('/api/projects/:pid/tasks', (req, res) => {
785
+ const pid = req.params.pid;
786
+ if (!getProject(pid)) {
787
+ res.status(404).json({ success: false, message: 'Project not found' });
788
+ return;
789
+ }
790
+ const { sdd_entry_id, title, description, priority } = req.body;
791
+ if (!title) {
792
+ res.status(400).json({ success: false, message: 'title is required' });
793
+ return;
794
+ }
795
+ const id = randomUUID();
796
+ const now = new Date().toISOString();
797
+ const task = { id, project_id: pid, sdd_entry_id, title, description, status: 'pending', priority: priority || 'medium', created_at: now, updated_at: now };
798
+ createTask(task);
799
+ res.status(201).json({ success: true, id, message: `Task "${title}" created` });
800
+ });
801
+ /**
802
+ * @swagger
803
+ * /api/projects/{pid}/tasks/{tid}:
804
+ * put:
805
+ * summary: Update task
806
+ * tags: [Tasks]
807
+ * parameters:
808
+ * - in: path
809
+ * name: pid
810
+ * required: true
811
+ * schema: { type: string, format: uuid }
812
+ * - in: path
813
+ * name: tid
814
+ * required: true
815
+ * schema: { type: string, format: uuid }
816
+ * requestBody:
817
+ * content:
818
+ * application/json:
819
+ * schema:
820
+ * type: object
821
+ * properties:
822
+ * title: { type: string }
823
+ * description: { type: string }
824
+ * status: { type: string, enum: [pending, in_progress, completed, cancelled] }
825
+ * priority: { type: string, enum: [low, medium, high, critical] }
826
+ * responses:
827
+ * 200:
828
+ * description: Task updated
829
+ */
830
+ router.put('/api/projects/:pid/tasks/:tid', (req, res) => {
831
+ const tid = req.params.tid;
832
+ if (!getTask(tid)) {
833
+ res.status(404).json({ success: false, message: 'Task not found' });
834
+ return;
835
+ }
836
+ updateTask(tid, req.body);
837
+ res.json({ success: true, message: 'Task updated' });
838
+ });
839
+ /**
840
+ * @swagger
841
+ * /api/projects/{pid}/tasks/{tid}:
842
+ * delete:
843
+ * summary: Delete task
844
+ * tags: [Tasks]
845
+ * parameters:
846
+ * - in: path
847
+ * name: pid
848
+ * required: true
849
+ * schema: { type: string, format: uuid }
850
+ * - in: path
851
+ * name: tid
852
+ * required: true
853
+ * schema: { type: string, format: uuid }
854
+ * responses:
855
+ * 200:
856
+ * description: Task deleted
857
+ */
858
+ router.delete('/api/projects/:pid/tasks/:tid', (req, res) => {
859
+ const tid = req.params.tid;
860
+ if (!getTask(tid)) {
861
+ res.status(404).json({ success: false, message: 'Task not found' });
862
+ return;
863
+ }
864
+ deleteTask(tid);
865
+ res.json({ success: true, message: 'Task deleted' });
866
+ });
867
+ // ---- DATABASE DOWNLOAD ----
868
+ /**
869
+ * @swagger
870
+ * /api/db/download:
871
+ * get:
872
+ * summary: Download the SQLite database file
873
+ * tags: [Utility]
874
+ * responses:
875
+ * 200:
876
+ * description: SQLite file
877
+ * content:
878
+ * application/x-sqlite3:
879
+ * schema: { type: string, format: binary }
880
+ */
881
+ router.get('/api/db/download', (_req, res) => {
882
+ const dbPath = getDbPath();
883
+ const filename = path.basename(dbPath);
884
+ res.setHeader('Content-Type', 'application/x-sqlite3');
885
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
886
+ const stream = fs.createReadStream(dbPath);
887
+ stream.pipe(res);
888
+ });
889
+ // ---- AUDIT LOG ----
890
+ /**
891
+ * @swagger
892
+ * /api/audit:
893
+ * get:
894
+ * summary: Get audit logs
895
+ * tags: [Audit]
896
+ * parameters:
897
+ * - in: query
898
+ * name: entity_type
899
+ * schema: { type: string, enum: [entry, task] }
900
+ * - in: query
901
+ * name: entity_id
902
+ * schema: { type: string, format: uuid }
903
+ * - in: query
904
+ * name: project_id
905
+ * schema: { type: string, format: uuid }
906
+ * - in: query
907
+ * name: page
908
+ * schema: { type: integer }
909
+ * - in: query
910
+ * name: limit
911
+ * schema: { type: integer }
912
+ * responses:
913
+ * 200:
914
+ * description: Audit logs
915
+ */
916
+ router.get('/api/audit', (req, res) => {
917
+ const entity_type = req.query.entity_type;
918
+ const entity_id = req.query.entity_id;
919
+ const project_id = req.query.project_id;
920
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
921
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
922
+ const { data, total } = getAuditLog({ entity_type, entity_id, project_id }, page || limit ? { page, limit } : undefined);
923
+ const result = { success: true, count: data.length, entries: data };
924
+ if (page || limit) {
925
+ const pageNum = page || 1;
926
+ const limitNum = limit || total;
927
+ result.pagination = { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) || 1 };
928
+ }
929
+ res.json(result);
930
+ });
931
+ // ---- CLASSIFICATIONS ----
932
+ /**
933
+ * @swagger
934
+ * /api/classify:
935
+ * post:
936
+ * summary: Add classification to an entity
937
+ * tags: [Utility]
938
+ * requestBody:
939
+ * required: true
940
+ * content:
941
+ * application/json:
942
+ * schema:
943
+ * type: object
944
+ * required: [classifiable_type, classifiable_id, tag, confidence]
945
+ * properties:
946
+ * classifiable_type: { type: string, enum: [project, entry, task] }
947
+ * classifiable_id: { type: string, format: uuid }
948
+ * tag: { type: string }
949
+ * confidence: { type: number, minimum: 0, maximum: 1 }
950
+ * responses:
951
+ * 201:
952
+ * description: Classification added
953
+ */
954
+ router.post('/api/classify', (req, res) => {
955
+ const { classifiable_type, classifiable_id, tag, confidence } = req.body;
956
+ if (!classifiable_type || !classifiable_id || !tag || confidence === undefined) {
957
+ res.status(400).json({ success: false, message: 'classifiable_type, classifiable_id, tag, confidence required' });
958
+ return;
959
+ }
960
+ const c = {
961
+ id: randomUUID(), classifiable_type, classifiable_id, tag, confidence,
962
+ created_at: new Date().toISOString(),
963
+ };
964
+ addClassification(c);
965
+ res.status(201).json({ success: true, classification: c });
966
+ });
967
+ export default router;
968
+ //# sourceMappingURL=routes.js.map