clitrigger 0.1.12 → 0.1.13

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 (64) hide show
  1. package/README.md +6 -3
  2. package/README_KR.md +6 -3
  3. package/dist/client/assets/index-BWNQgE_E.js +649 -0
  4. package/dist/client/assets/index-Ck_mmrzu.css +32 -0
  5. package/dist/client/index.html +2 -2
  6. package/dist/server/db/queries.d.ts +39 -3
  7. package/dist/server/db/queries.d.ts.map +1 -1
  8. package/dist/server/db/queries.js +118 -3
  9. package/dist/server/db/queries.js.map +1 -1
  10. package/dist/server/db/schema.d.ts.map +1 -1
  11. package/dist/server/db/schema.js +65 -0
  12. package/dist/server/db/schema.js.map +1 -1
  13. package/dist/server/index.d.ts.map +1 -1
  14. package/dist/server/index.js +2 -0
  15. package/dist/server/index.js.map +1 -1
  16. package/dist/server/routes/favorites.d.ts +3 -0
  17. package/dist/server/routes/favorites.d.ts.map +1 -0
  18. package/dist/server/routes/favorites.js +201 -0
  19. package/dist/server/routes/favorites.js.map +1 -0
  20. package/dist/server/routes/memory.d.ts.map +1 -1
  21. package/dist/server/routes/memory.js +396 -4
  22. package/dist/server/routes/memory.js.map +1 -1
  23. package/dist/server/routes/projects.js +2 -2
  24. package/dist/server/routes/projects.js.map +1 -1
  25. package/dist/server/routes/sessions.d.ts.map +1 -1
  26. package/dist/server/routes/sessions.js +24 -2
  27. package/dist/server/routes/sessions.js.map +1 -1
  28. package/dist/server/services/claude-manager.d.ts +18 -1
  29. package/dist/server/services/claude-manager.d.ts.map +1 -1
  30. package/dist/server/services/claude-manager.js +99 -5
  31. package/dist/server/services/claude-manager.js.map +1 -1
  32. package/dist/server/services/discussion-orchestrator.d.ts.map +1 -1
  33. package/dist/server/services/discussion-orchestrator.js +22 -0
  34. package/dist/server/services/discussion-orchestrator.js.map +1 -1
  35. package/dist/server/services/memory-ingest.d.ts +16 -0
  36. package/dist/server/services/memory-ingest.d.ts.map +1 -0
  37. package/dist/server/services/memory-ingest.js +488 -0
  38. package/dist/server/services/memory-ingest.js.map +1 -0
  39. package/dist/server/services/memory-injector.d.ts.map +1 -1
  40. package/dist/server/services/memory-injector.js +32 -2
  41. package/dist/server/services/memory-injector.js.map +1 -1
  42. package/dist/server/services/memory-wikilinks.d.ts +34 -0
  43. package/dist/server/services/memory-wikilinks.d.ts.map +1 -0
  44. package/dist/server/services/memory-wikilinks.js +86 -0
  45. package/dist/server/services/memory-wikilinks.js.map +1 -0
  46. package/dist/server/services/orchestrator.d.ts.map +1 -1
  47. package/dist/server/services/orchestrator.js +10 -0
  48. package/dist/server/services/orchestrator.js.map +1 -1
  49. package/dist/server/services/session-manager.d.ts +22 -1
  50. package/dist/server/services/session-manager.d.ts.map +1 -1
  51. package/dist/server/services/session-manager.js +82 -5
  52. package/dist/server/services/session-manager.js.map +1 -1
  53. package/dist/server/websocket/broadcaster.d.ts +11 -0
  54. package/dist/server/websocket/broadcaster.d.ts.map +1 -1
  55. package/dist/server/websocket/broadcaster.js +67 -0
  56. package/dist/server/websocket/broadcaster.js.map +1 -1
  57. package/dist/server/websocket/events.d.ts +3 -0
  58. package/dist/server/websocket/events.d.ts.map +1 -1
  59. package/dist/server/websocket/index.d.ts.map +1 -1
  60. package/dist/server/websocket/index.js +45 -2
  61. package/dist/server/websocket/index.js.map +1 -1
  62. package/package.json +1 -1
  63. package/dist/client/assets/index-CcsvxPmx.css +0 -1
  64. package/dist/client/assets/index-qSpSlrcM.js +0 -606
@@ -0,0 +1,201 @@
1
+ import { Router } from 'express';
2
+ import nodePath from 'path';
3
+ import fs from 'fs';
4
+ import { exec, spawn } from 'child_process';
5
+ import * as queries from '../db/queries.js';
6
+ const router = Router();
7
+ const MAX_TARGET_LEN = 4096;
8
+ const MAX_ARGS_COUNT = 64;
9
+ const ALLOWED_TYPES = ['executable', 'command', 'url'];
10
+ function isValidType(t) {
11
+ return typeof t === 'string' && ALLOWED_TYPES.includes(t);
12
+ }
13
+ function normalizeArgs(input) {
14
+ if (input === null || input === undefined)
15
+ return null;
16
+ if (Array.isArray(input)) {
17
+ const cleaned = input
18
+ .map((s) => (typeof s === 'string' ? s : String(s)))
19
+ .filter((s) => s.length > 0);
20
+ if (cleaned.length === 0)
21
+ return null;
22
+ if (cleaned.length > MAX_ARGS_COUNT)
23
+ return cleaned.slice(0, MAX_ARGS_COUNT);
24
+ return cleaned;
25
+ }
26
+ if (typeof input === 'string' && input.trim().length === 0)
27
+ return null;
28
+ return null;
29
+ }
30
+ function validatePayload(body) {
31
+ const name = typeof body?.name === 'string' ? body.name.trim() : '';
32
+ if (!name)
33
+ return { ok: false, error: 'name is required' };
34
+ if (!isValidType(body?.type))
35
+ return { ok: false, error: 'invalid type' };
36
+ const type = body.type;
37
+ const target = typeof body?.target === 'string' ? body.target.trim() : '';
38
+ if (!target)
39
+ return { ok: false, error: 'target is required' };
40
+ if (target.length > MAX_TARGET_LEN)
41
+ return { ok: false, error: 'target too long' };
42
+ if (type === 'url') {
43
+ if (!/^https?:\/\//i.test(target)) {
44
+ return { ok: false, error: 'url must start with http:// or https://' };
45
+ }
46
+ }
47
+ const argsArr = normalizeArgs(body?.args);
48
+ const args = argsArr ? JSON.stringify(argsArr) : null;
49
+ const cwdInput = typeof body?.cwd === 'string' ? body.cwd.trim() : '';
50
+ const cwd = cwdInput ? cwdInput : null;
51
+ const iconInput = typeof body?.icon === 'string' ? body.icon.trim() : '';
52
+ const icon = iconInput ? iconInput : null;
53
+ return { ok: true, value: { name, type, target, args, cwd, icon } };
54
+ }
55
+ function parseStoredArgs(args) {
56
+ if (!args)
57
+ return [];
58
+ try {
59
+ const parsed = JSON.parse(args);
60
+ if (Array.isArray(parsed))
61
+ return parsed.filter((s) => typeof s === 'string');
62
+ }
63
+ catch { /* ignore */ }
64
+ return [];
65
+ }
66
+ // GET /api/favorites - list all
67
+ router.get('/favorites', (_req, res) => {
68
+ try {
69
+ const favorites = queries.getAllFavorites();
70
+ res.json(favorites);
71
+ }
72
+ catch (err) {
73
+ const message = err instanceof Error ? err.message : 'Unknown error';
74
+ res.status(500).json({ error: message });
75
+ }
76
+ });
77
+ // POST /api/favorites - create
78
+ router.post('/favorites', (req, res) => {
79
+ try {
80
+ const validation = validatePayload(req.body);
81
+ if (!validation.ok) {
82
+ res.status(400).json({ error: validation.error });
83
+ return;
84
+ }
85
+ const { name, type, target, args, cwd, icon } = validation.value;
86
+ const existing = queries.getAllFavorites();
87
+ const sortOrder = existing.length;
88
+ const favorite = queries.createFavorite(name, type, target, args, cwd, icon, sortOrder);
89
+ res.status(201).json(favorite);
90
+ }
91
+ catch (err) {
92
+ const message = err instanceof Error ? err.message : 'Unknown error';
93
+ res.status(500).json({ error: message });
94
+ }
95
+ });
96
+ // PUT /api/favorites/:id - update
97
+ router.put('/favorites/:id', (req, res) => {
98
+ try {
99
+ const existing = queries.getFavoriteById(req.params.id);
100
+ if (!existing) {
101
+ res.status(404).json({ error: 'Favorite not found' });
102
+ return;
103
+ }
104
+ const validation = validatePayload(req.body);
105
+ if (!validation.ok) {
106
+ res.status(400).json({ error: validation.error });
107
+ return;
108
+ }
109
+ const updated = queries.updateFavorite(req.params.id, validation.value);
110
+ res.json(updated);
111
+ }
112
+ catch (err) {
113
+ const message = err instanceof Error ? err.message : 'Unknown error';
114
+ res.status(500).json({ error: message });
115
+ }
116
+ });
117
+ // DELETE /api/favorites/:id - delete
118
+ router.delete('/favorites/:id', (req, res) => {
119
+ try {
120
+ const existing = queries.getFavoriteById(req.params.id);
121
+ if (!existing) {
122
+ res.status(404).json({ error: 'Favorite not found' });
123
+ return;
124
+ }
125
+ queries.deleteFavorite(req.params.id);
126
+ res.status(204).send();
127
+ }
128
+ catch (err) {
129
+ const message = err instanceof Error ? err.message : 'Unknown error';
130
+ res.status(500).json({ error: message });
131
+ }
132
+ });
133
+ // POST /api/favorites/:id/launch - fire-and-forget execution via OS
134
+ router.post('/favorites/:id/launch', (req, res) => {
135
+ const favorite = queries.getFavoriteById(req.params.id);
136
+ if (!favorite) {
137
+ res.status(404).json({ error: 'Favorite not found' });
138
+ return;
139
+ }
140
+ const cwd = favorite.cwd ? nodePath.normalize(favorite.cwd) : undefined;
141
+ if (cwd && !fs.existsSync(cwd)) {
142
+ res.status(400).json({ error: 'cwd does not exist' });
143
+ return;
144
+ }
145
+ try {
146
+ if (favorite.type === 'url') {
147
+ // Re-validate at launch time (DB content could have been hand-edited)
148
+ if (!/^https?:\/\//i.test(favorite.target)) {
149
+ res.status(400).json({ error: 'invalid url scheme' });
150
+ return;
151
+ }
152
+ const target = favorite.target;
153
+ if (process.platform === 'win32') {
154
+ // start "" "<url>" via cmd to launch default browser
155
+ const child = spawn('cmd.exe', ['/c', 'start', '""', target], { detached: true, stdio: 'ignore' });
156
+ child.on('error', () => { });
157
+ child.unref();
158
+ }
159
+ else if (process.platform === 'darwin') {
160
+ const child = spawn('open', [target], { detached: true, stdio: 'ignore' });
161
+ child.on('error', () => { });
162
+ child.unref();
163
+ }
164
+ else {
165
+ const child = spawn('xdg-open', [target], { detached: true, stdio: 'ignore' });
166
+ child.on('error', () => { });
167
+ child.unref();
168
+ }
169
+ res.json({ ok: true });
170
+ return;
171
+ }
172
+ if (favorite.type === 'executable') {
173
+ const target = nodePath.normalize(favorite.target);
174
+ const args = parseStoredArgs(favorite.args);
175
+ const ext = nodePath.extname(target).toLowerCase();
176
+ const useShell = process.platform === 'win32' && (ext === '.bat' || ext === '.cmd');
177
+ const child = spawn(target, args, {
178
+ cwd,
179
+ detached: true,
180
+ stdio: 'ignore',
181
+ shell: useShell,
182
+ windowsHide: false,
183
+ });
184
+ child.on('error', () => { });
185
+ child.unref();
186
+ res.json({ ok: true });
187
+ return;
188
+ }
189
+ // command
190
+ const child = exec(favorite.target, { cwd, windowsHide: false });
191
+ child.on('error', () => { });
192
+ child.unref?.();
193
+ res.json({ ok: true });
194
+ }
195
+ catch (err) {
196
+ const message = err instanceof Error ? err.message : 'launch failed';
197
+ res.status(500).json({ error: message });
198
+ }
199
+ });
200
+ export default router;
201
+ //# sourceMappingURL=favorites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"favorites.js","sourceRoot":"","sources":["../../../src/server/routes/favorites.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../src/server/routes/memory.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAoQxB,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../src/server/routes/memory.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6oBxB,eAAe,MAAM,CAAC"}
@@ -1,6 +1,11 @@
1
1
  import { Router } from 'express';
2
+ import { exec } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
2
5
  import * as queries from '../db/queries.js';
3
6
  import { buildMemoryBlock } from '../services/memory-injector.js';
7
+ import { ingestSource, lintWiki, buildSourceTextFromTodo, buildSourceTextFromDiscussion } from '../services/memory-ingest.js';
8
+ import { appendWikilinkToBody, findBacklinks, parseWikilinks, replaceTitleInBody, resolveWikilinks, } from '../services/memory-wikilinks.js';
4
9
  const router = Router();
5
10
  const VALID_RELATION_TYPES = new Set([
6
11
  'related',
@@ -78,8 +83,23 @@ router.post('/projects/:id/memory/nodes', (req, res) => {
78
83
  res.status(400).json({ error: 'title is required' });
79
84
  return;
80
85
  }
81
- const node = queries.createMemoryNode(req.params.id, title.trim(), typeof body === 'string' ? body : '', normalizeTags(tags), pinned ? 1 : 0);
82
- res.status(201).json(node);
86
+ const trimmedTitle = title.trim();
87
+ if (queries.getMemoryNodeByTitle(req.params.id, trimmedTitle)) {
88
+ res.status(409).json({ error: 'A memory node with this title already exists in this project' });
89
+ return;
90
+ }
91
+ try {
92
+ const node = queries.createMemoryNode(req.params.id, trimmedTitle, typeof body === 'string' ? body : '', normalizeTags(tags), pinned ? 1 : 0);
93
+ res.status(201).json(node);
94
+ }
95
+ catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ if (msg.includes('UNIQUE')) {
98
+ res.status(409).json({ error: 'A memory node with this title already exists in this project' });
99
+ return;
100
+ }
101
+ throw err;
102
+ }
83
103
  }
84
104
  catch (err) {
85
105
  res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
@@ -94,12 +114,24 @@ router.put('/memory/nodes/:nodeId', (req, res) => {
94
114
  }
95
115
  const { title, body, tags, pinned } = req.body ?? {};
96
116
  const updates = {};
117
+ let titleChange = null;
97
118
  if (title !== undefined) {
98
119
  if (typeof title !== 'string' || !title.trim()) {
99
120
  res.status(400).json({ error: 'title cannot be empty' });
100
121
  return;
101
122
  }
102
- updates.title = title.trim();
123
+ const trimmed = title.trim();
124
+ if (trimmed.toLowerCase() !== existing.title.toLowerCase()) {
125
+ const conflict = queries.getMemoryNodeByTitle(existing.project_id, trimmed);
126
+ if (conflict && conflict.id !== existing.id) {
127
+ res.status(409).json({ error: 'A memory node with this title already exists in this project' });
128
+ return;
129
+ }
130
+ }
131
+ if (trimmed !== existing.title) {
132
+ titleChange = { from: existing.title, to: trimmed };
133
+ }
134
+ updates.title = trimmed;
103
135
  }
104
136
  if (body !== undefined)
105
137
  updates.body = typeof body === 'string' ? body : '';
@@ -107,7 +139,32 @@ router.put('/memory/nodes/:nodeId', (req, res) => {
107
139
  updates.tags = normalizeTags(tags);
108
140
  if (pinned !== undefined)
109
141
  updates.pinned = pinned ? 1 : 0;
110
- const updated = queries.updateMemoryNode(req.params.nodeId, updates);
142
+ let updated;
143
+ try {
144
+ updated = queries.updateMemoryNode(req.params.nodeId, updates);
145
+ }
146
+ catch (err) {
147
+ const msg = err instanceof Error ? err.message : String(err);
148
+ if (msg.includes('UNIQUE')) {
149
+ res.status(409).json({ error: 'A memory node with this title already exists in this project' });
150
+ return;
151
+ }
152
+ throw err;
153
+ }
154
+ // Cascade rename: rewrite [[oldTitle]] → [[newTitle]] across other nodes' bodies
155
+ if (titleChange) {
156
+ const others = queries.getMemoryNodesByProjectId(existing.project_id);
157
+ for (const other of others) {
158
+ if (other.id === existing.id)
159
+ continue;
160
+ if (!other.body)
161
+ continue;
162
+ const rewritten = replaceTitleInBody(other.body, titleChange.from, titleChange.to);
163
+ if (rewritten !== other.body) {
164
+ queries.updateMemoryNode(other.id, { body: rewritten });
165
+ }
166
+ }
167
+ }
111
168
  res.json(updated);
112
169
  }
113
170
  catch (err) {
@@ -149,6 +206,83 @@ router.delete('/memory/nodes/:nodeId', (req, res) => {
149
206
  res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
150
207
  }
151
208
  });
209
+ // ── Wikilinks (body-level references) ──
210
+ router.get('/memory/nodes/:nodeId/backlinks', (req, res) => {
211
+ try {
212
+ const node = queries.getMemoryNodeById(req.params.nodeId);
213
+ if (!node) {
214
+ res.status(404).json({ error: 'Memory node not found' });
215
+ return;
216
+ }
217
+ const hits = findBacklinks(node.project_id, node.title, node.id);
218
+ res.json(hits.map(h => ({
219
+ id: h.source.id,
220
+ title: h.source.title,
221
+ snippet: h.snippet,
222
+ })));
223
+ }
224
+ catch (err) {
225
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
226
+ }
227
+ });
228
+ router.post('/memory/nodes/:nodeId/insert-link', (req, res) => {
229
+ try {
230
+ const source = queries.getMemoryNodeById(req.params.nodeId);
231
+ if (!source) {
232
+ res.status(404).json({ error: 'Source node not found' });
233
+ return;
234
+ }
235
+ const { targetTitle, targetNodeId } = req.body ?? {};
236
+ let title;
237
+ if (targetNodeId) {
238
+ const target = queries.getMemoryNodeById(String(targetNodeId));
239
+ if (!target || target.project_id !== source.project_id) {
240
+ res.status(404).json({ error: 'Target node not found in this project' });
241
+ return;
242
+ }
243
+ title = target.title;
244
+ }
245
+ else if (typeof targetTitle === 'string' && targetTitle.trim()) {
246
+ title = targetTitle.trim();
247
+ }
248
+ else {
249
+ res.status(400).json({ error: 'targetTitle or targetNodeId required' });
250
+ return;
251
+ }
252
+ const newBody = appendWikilinkToBody(source.body || '', title);
253
+ const updated = queries.updateMemoryNode(source.id, { body: newBody });
254
+ res.json(updated);
255
+ }
256
+ catch (err) {
257
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
258
+ }
259
+ });
260
+ router.post('/projects/:id/memory/wikilinks/resolve', (req, res) => {
261
+ try {
262
+ const project = queries.getProjectById(req.params.id);
263
+ if (!project) {
264
+ res.status(404).json({ error: 'Project not found' });
265
+ return;
266
+ }
267
+ const { body, titles } = req.body ?? {};
268
+ let titleList = [];
269
+ if (Array.isArray(titles)) {
270
+ titleList = titles.map(String).filter(Boolean);
271
+ }
272
+ else if (typeof body === 'string') {
273
+ titleList = parseWikilinks(body).map(r => r.title);
274
+ }
275
+ else {
276
+ res.status(400).json({ error: 'Provide body or titles[]' });
277
+ return;
278
+ }
279
+ const resolved = resolveWikilinks(req.params.id, titleList);
280
+ res.json(resolved);
281
+ }
282
+ catch (err) {
283
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
284
+ }
285
+ });
152
286
  // ── Edges ──
153
287
  router.post('/projects/:id/memory/edges', (req, res) => {
154
288
  try {
@@ -233,6 +367,264 @@ router.delete('/memory/edges/:edgeId', (req, res) => {
233
367
  res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
234
368
  }
235
369
  });
370
+ // ── Ingest (LLM-driven wiki update from raw source) ──
371
+ router.post('/projects/:id/memory/ingest', async (req, res) => {
372
+ try {
373
+ const project = queries.getProjectById(req.params.id);
374
+ if (!project) {
375
+ res.status(404).json({ error: 'Project not found' });
376
+ return;
377
+ }
378
+ const { source_text, source_type, source_id } = req.body ?? {};
379
+ const stype = typeof source_type === 'string' ? source_type : null;
380
+ const sid = typeof source_id === 'string' ? source_id : null;
381
+ let text;
382
+ let titleHint = null;
383
+ if (stype === 'todo' && sid) {
384
+ const todo = queries.getTodoById(sid);
385
+ if (!todo || todo.project_id !== project.id) {
386
+ res.status(404).json({ error: 'Todo not found' });
387
+ return;
388
+ }
389
+ const built = buildSourceTextFromTodo(sid);
390
+ if (!built) {
391
+ res.status(400).json({ error: 'Todo has no ingestable content yet' });
392
+ return;
393
+ }
394
+ text = built;
395
+ titleHint = todo.title;
396
+ }
397
+ else if (stype === 'discussion' && sid) {
398
+ const discussion = queries.getDiscussionById(sid);
399
+ if (!discussion || discussion.project_id !== project.id) {
400
+ res.status(404).json({ error: 'Discussion not found' });
401
+ return;
402
+ }
403
+ const built = buildSourceTextFromDiscussion(sid);
404
+ if (!built) {
405
+ res.status(400).json({ error: 'Discussion has no ingestable content yet' });
406
+ return;
407
+ }
408
+ text = built;
409
+ titleHint = discussion.title;
410
+ }
411
+ else {
412
+ // manual paste (or any direct source_text)
413
+ if (!source_text || typeof source_text !== 'string' || !source_text.trim()) {
414
+ res.status(400).json({ error: 'source_text is required for manual ingest' });
415
+ return;
416
+ }
417
+ text = source_text.trim();
418
+ }
419
+ const result = await ingestSource(req.params.id, text, stype === 'todo' || stype === 'discussion' ? stype : 'manual', sid, titleHint);
420
+ res.json(result);
421
+ }
422
+ catch (err) {
423
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
424
+ }
425
+ });
426
+ // ── Raw source viewer ──
427
+ const RAW_SOURCE_TYPES = ['todo', 'discussion', 'manual'];
428
+ const RAW_DIR = '.clitrigger/raw';
429
+ router.get('/projects/:id/memory/raw-files', (req, res) => {
430
+ try {
431
+ const project = queries.getProjectById(req.params.id);
432
+ if (!project) {
433
+ res.status(404).json({ error: 'Project not found' });
434
+ return;
435
+ }
436
+ if (!project.path) {
437
+ res.json({ files: [] });
438
+ return;
439
+ }
440
+ const allNodes = queries.getMemoryNodesByProjectId(req.params.id);
441
+ const derivedByPath = new Map();
442
+ for (const n of allNodes) {
443
+ if (!n.source_path)
444
+ continue;
445
+ const list = derivedByPath.get(n.source_path);
446
+ if (list)
447
+ list.push(n.id);
448
+ else
449
+ derivedByPath.set(n.source_path, [n.id]);
450
+ }
451
+ const projectRoot = path.resolve(project.path);
452
+ const files = [];
453
+ for (const sourceType of RAW_SOURCE_TYPES) {
454
+ const dir = path.join(projectRoot, RAW_DIR, sourceType);
455
+ if (!fs.existsSync(dir))
456
+ continue;
457
+ let entries;
458
+ try {
459
+ entries = fs.readdirSync(dir);
460
+ }
461
+ catch {
462
+ continue;
463
+ }
464
+ for (const filename of entries) {
465
+ if (filename.startsWith('.'))
466
+ continue;
467
+ const absPath = path.join(dir, filename);
468
+ const resolvedAbs = path.resolve(absPath);
469
+ const resolvedDir = path.resolve(dir);
470
+ if (!resolvedAbs.startsWith(resolvedDir + path.sep))
471
+ continue;
472
+ let stat;
473
+ try {
474
+ stat = fs.statSync(absPath);
475
+ }
476
+ catch {
477
+ continue;
478
+ }
479
+ if (!stat.isFile())
480
+ continue;
481
+ const relativePath = `${RAW_DIR}/${sourceType}/${filename}`;
482
+ files.push({
483
+ source_type: sourceType,
484
+ filename,
485
+ relative_path: relativePath,
486
+ size: stat.size,
487
+ mtime: stat.mtime.toISOString(),
488
+ derived_node_ids: derivedByPath.get(relativePath) ?? [],
489
+ });
490
+ }
491
+ }
492
+ files.sort((a, b) => b.mtime.localeCompare(a.mtime));
493
+ res.json({ files });
494
+ }
495
+ catch (err) {
496
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
497
+ }
498
+ });
499
+ router.get('/projects/:id/memory/raw-files/content', (req, res) => {
500
+ try {
501
+ const project = queries.getProjectById(req.params.id);
502
+ if (!project || !project.path) {
503
+ res.status(404).json({ error: 'Project not found' });
504
+ return;
505
+ }
506
+ const relPathRaw = req.query.path;
507
+ if (typeof relPathRaw !== 'string' || !relPathRaw.trim()) {
508
+ res.status(400).json({ error: 'path query param is required' });
509
+ return;
510
+ }
511
+ const projectRoot = path.resolve(project.path);
512
+ const rawRoot = path.resolve(projectRoot, RAW_DIR);
513
+ const absPath = path.resolve(projectRoot, relPathRaw);
514
+ if (!absPath.startsWith(rawRoot + path.sep)) {
515
+ res.status(400).json({ error: 'Path must be within the raw sources directory' });
516
+ return;
517
+ }
518
+ if (!fs.existsSync(absPath)) {
519
+ res.status(404).json({ error: 'Raw file not found', path: relPathRaw });
520
+ return;
521
+ }
522
+ const stat = fs.statSync(absPath);
523
+ if (!stat.isFile()) {
524
+ res.status(400).json({ error: 'Path is not a file' });
525
+ return;
526
+ }
527
+ const content = fs.readFileSync(absPath, 'utf-8');
528
+ res.type('text/markdown; charset=utf-8').send(content);
529
+ }
530
+ catch (err) {
531
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
532
+ }
533
+ });
534
+ router.post('/projects/:id/memory/raw-files/open', (req, res) => {
535
+ try {
536
+ const project = queries.getProjectById(req.params.id);
537
+ if (!project || !project.path) {
538
+ res.status(404).json({ error: 'Project not found' });
539
+ return;
540
+ }
541
+ const { path: relPathRaw, mode } = req.body ?? {};
542
+ if (typeof relPathRaw !== 'string' || !relPathRaw.trim()) {
543
+ res.status(400).json({ error: 'path is required' });
544
+ return;
545
+ }
546
+ const projectRoot = path.resolve(project.path);
547
+ const rawRoot = path.resolve(projectRoot, RAW_DIR);
548
+ const absPath = path.resolve(projectRoot, relPathRaw);
549
+ if (!absPath.startsWith(rawRoot + path.sep)) {
550
+ res.status(400).json({ error: 'Path must be within the raw sources directory' });
551
+ return;
552
+ }
553
+ if (!fs.existsSync(absPath)) {
554
+ res.status(404).json({ error: 'Raw file not found', path: relPathRaw });
555
+ return;
556
+ }
557
+ // mode === 'reveal' → open the containing folder, else open the file itself
558
+ const target = mode === 'reveal' ? path.dirname(absPath) : absPath;
559
+ if (process.platform === 'win32') {
560
+ if (mode === 'reveal') {
561
+ exec(`explorer.exe /select,"${absPath}"`);
562
+ }
563
+ else {
564
+ exec(`start "" "${target}"`, { windowsHide: true });
565
+ }
566
+ }
567
+ else if (process.platform === 'darwin') {
568
+ exec(mode === 'reveal' ? `open -R "${absPath}"` : `open "${target}"`);
569
+ }
570
+ else {
571
+ exec(`xdg-open "${target}"`);
572
+ }
573
+ res.json({ ok: true });
574
+ }
575
+ catch (err) {
576
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
577
+ }
578
+ });
579
+ router.get('/memory/nodes/:nodeId/raw', (req, res) => {
580
+ try {
581
+ const node = queries.getMemoryNodeById(req.params.nodeId);
582
+ if (!node) {
583
+ res.status(404).json({ error: 'Memory node not found' });
584
+ return;
585
+ }
586
+ if (!node.source_path) {
587
+ res.status(404).json({ error: 'No raw source associated with this node' });
588
+ return;
589
+ }
590
+ const project = queries.getProjectById(node.project_id);
591
+ if (!project || !project.path) {
592
+ res.status(404).json({ error: 'Project path unavailable' });
593
+ return;
594
+ }
595
+ // Path traversal guard: resolved path must be under project root
596
+ const absPath = path.resolve(project.path, node.source_path);
597
+ const projectRoot = path.resolve(project.path);
598
+ if (!absPath.startsWith(projectRoot + path.sep) && absPath !== projectRoot) {
599
+ res.status(400).json({ error: 'Invalid source path' });
600
+ return;
601
+ }
602
+ if (!fs.existsSync(absPath)) {
603
+ res.status(404).json({ error: 'Raw source file no longer exists', path: node.source_path });
604
+ return;
605
+ }
606
+ const content = fs.readFileSync(absPath, 'utf-8');
607
+ res.type('text/markdown; charset=utf-8').send(content);
608
+ }
609
+ catch (err) {
610
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
611
+ }
612
+ });
613
+ // ── Lint (LLM-driven wiki health check) ──
614
+ router.post('/projects/:id/memory/lint', async (req, res) => {
615
+ try {
616
+ const project = queries.getProjectById(req.params.id);
617
+ if (!project) {
618
+ res.status(404).json({ error: 'Project not found' });
619
+ return;
620
+ }
621
+ const issues = await lintWiki(req.params.id);
622
+ res.json({ issues });
623
+ }
624
+ catch (err) {
625
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
626
+ }
627
+ });
236
628
  // ── Preview (build prompt block) ──
237
629
  router.post('/projects/:id/memory/preview', (req, res) => {
238
630
  try {