clitrigger 0.1.11 → 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.
- package/README.md +28 -22
- package/README_KR.md +29 -23
- package/dist/client/assets/index-BWNQgE_E.js +649 -0
- package/dist/client/assets/index-Ck_mmrzu.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/data/cli-models-registry.json +12 -1
- package/dist/server/db/queries.d.ts +83 -6
- package/dist/server/db/queries.d.ts.map +1 -1
- package/dist/server/db/queries.js +253 -9
- package/dist/server/db/queries.js.map +1 -1
- package/dist/server/db/schema.d.ts.map +1 -1
- package/dist/server/db/schema.js +100 -0
- package/dist/server/db/schema.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +6 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/git.d.ts +1 -0
- package/dist/server/lib/git.d.ts.map +1 -1
- package/dist/server/lib/git.js +11 -0
- package/dist/server/lib/git.js.map +1 -1
- package/dist/server/plugins/harness/adapters/claude.d.ts +3 -0
- package/dist/server/plugins/harness/adapters/claude.d.ts.map +1 -0
- package/dist/server/plugins/harness/adapters/claude.js +98 -0
- package/dist/server/plugins/harness/adapters/claude.js.map +1 -0
- package/dist/server/plugins/harness/adapters/codex.d.ts +3 -0
- package/dist/server/plugins/harness/adapters/codex.d.ts.map +1 -0
- package/dist/server/plugins/harness/adapters/codex.js +136 -0
- package/dist/server/plugins/harness/adapters/codex.js.map +1 -0
- package/dist/server/plugins/harness/adapters/gemini.d.ts +3 -0
- package/dist/server/plugins/harness/adapters/gemini.d.ts.map +1 -0
- package/dist/server/plugins/harness/adapters/gemini.js +118 -0
- package/dist/server/plugins/harness/adapters/gemini.js.map +1 -0
- package/dist/server/plugins/harness/index.d.ts +3 -0
- package/dist/server/plugins/harness/index.d.ts.map +1 -0
- package/dist/server/plugins/harness/index.js +13 -0
- package/dist/server/plugins/harness/index.js.map +1 -0
- package/dist/server/plugins/harness/io.d.ts +12 -0
- package/dist/server/plugins/harness/io.d.ts.map +1 -0
- package/dist/server/plugins/harness/io.js +94 -0
- package/dist/server/plugins/harness/io.js.map +1 -0
- package/dist/server/plugins/harness/router.d.ts +4 -0
- package/dist/server/plugins/harness/router.d.ts.map +1 -0
- package/dist/server/plugins/harness/router.js +206 -0
- package/dist/server/plugins/harness/router.js.map +1 -0
- package/dist/server/plugins/harness/types.d.ts +40 -0
- package/dist/server/plugins/harness/types.d.ts.map +1 -0
- package/dist/server/plugins/harness/types.js +2 -0
- package/dist/server/plugins/harness/types.js.map +1 -0
- package/dist/server/routes/discussions.d.ts.map +1 -1
- package/dist/server/routes/discussions.js +116 -6
- package/dist/server/routes/discussions.js.map +1 -1
- package/dist/server/routes/favorites.d.ts +3 -0
- package/dist/server/routes/favorites.d.ts.map +1 -0
- package/dist/server/routes/favorites.js +201 -0
- package/dist/server/routes/favorites.js.map +1 -0
- package/dist/server/routes/logs.d.ts.map +1 -1
- package/dist/server/routes/logs.js +16 -5
- package/dist/server/routes/logs.js.map +1 -1
- package/dist/server/routes/memory.d.ts +3 -0
- package/dist/server/routes/memory.d.ts.map +1 -0
- package/dist/server/routes/memory.js +651 -0
- package/dist/server/routes/memory.js.map +1 -0
- package/dist/server/routes/projects.d.ts.map +1 -1
- package/dist/server/routes/projects.js +13 -4
- package/dist/server/routes/projects.js.map +1 -1
- package/dist/server/routes/review.d.ts.map +1 -1
- package/dist/server/routes/review.js +152 -1
- package/dist/server/routes/review.js.map +1 -1
- package/dist/server/routes/sessions.d.ts.map +1 -1
- package/dist/server/routes/sessions.js +24 -2
- package/dist/server/routes/sessions.js.map +1 -1
- package/dist/server/routes/todos.d.ts.map +1 -1
- package/dist/server/routes/todos.js +17 -3
- package/dist/server/routes/todos.js.map +1 -1
- package/dist/server/services/claude-manager.d.ts +18 -1
- package/dist/server/services/claude-manager.d.ts.map +1 -1
- package/dist/server/services/claude-manager.js +99 -5
- package/dist/server/services/claude-manager.js.map +1 -1
- package/dist/server/services/discussion-extractor.d.ts +7 -0
- package/dist/server/services/discussion-extractor.d.ts.map +1 -0
- package/dist/server/services/discussion-extractor.js +167 -0
- package/dist/server/services/discussion-extractor.js.map +1 -0
- package/dist/server/services/discussion-orchestrator.d.ts.map +1 -1
- package/dist/server/services/discussion-orchestrator.js +38 -1
- package/dist/server/services/discussion-orchestrator.js.map +1 -1
- package/dist/server/services/log-streamer.d.ts +20 -0
- package/dist/server/services/log-streamer.d.ts.map +1 -1
- package/dist/server/services/log-streamer.js +81 -3
- package/dist/server/services/log-streamer.js.map +1 -1
- package/dist/server/services/memory-ingest.d.ts +16 -0
- package/dist/server/services/memory-ingest.d.ts.map +1 -0
- package/dist/server/services/memory-ingest.js +488 -0
- package/dist/server/services/memory-ingest.js.map +1 -0
- package/dist/server/services/memory-inject-hook.d.ts +9 -0
- package/dist/server/services/memory-inject-hook.d.ts.map +1 -0
- package/dist/server/services/memory-inject-hook.js +17 -0
- package/dist/server/services/memory-inject-hook.js.map +1 -0
- package/dist/server/services/memory-injector.d.ts +15 -0
- package/dist/server/services/memory-injector.d.ts.map +1 -0
- package/dist/server/services/memory-injector.js +107 -0
- package/dist/server/services/memory-injector.js.map +1 -0
- package/dist/server/services/memory-wikilinks.d.ts +34 -0
- package/dist/server/services/memory-wikilinks.d.ts.map +1 -0
- package/dist/server/services/memory-wikilinks.js +86 -0
- package/dist/server/services/memory-wikilinks.js.map +1 -0
- package/dist/server/services/model-sync.js +1 -1
- package/dist/server/services/orchestrator.d.ts.map +1 -1
- package/dist/server/services/orchestrator.js +37 -0
- package/dist/server/services/orchestrator.js.map +1 -1
- package/dist/server/services/pty-output-filter.d.ts +12 -0
- package/dist/server/services/pty-output-filter.d.ts.map +1 -1
- package/dist/server/services/pty-output-filter.js +94 -1
- package/dist/server/services/pty-output-filter.js.map +1 -1
- package/dist/server/services/review-capture.d.ts +5 -3
- package/dist/server/services/review-capture.d.ts.map +1 -1
- package/dist/server/services/review-capture.js +29 -7
- package/dist/server/services/review-capture.js.map +1 -1
- package/dist/server/services/session-manager.d.ts +22 -1
- package/dist/server/services/session-manager.d.ts.map +1 -1
- package/dist/server/services/session-manager.js +82 -5
- package/dist/server/services/session-manager.js.map +1 -1
- package/dist/server/websocket/broadcaster.d.ts +11 -0
- package/dist/server/websocket/broadcaster.d.ts.map +1 -1
- package/dist/server/websocket/broadcaster.js +67 -0
- package/dist/server/websocket/broadcaster.js.map +1 -1
- package/dist/server/websocket/events.d.ts +3 -0
- package/dist/server/websocket/events.d.ts.map +1 -1
- package/dist/server/websocket/index.d.ts.map +1 -1
- package/dist/server/websocket/index.js +45 -2
- package/dist/server/websocket/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/client/assets/index-D4Xur2yY.css +0 -1
- package/dist/client/assets/index-Dz1uDy_d.js +0 -567
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import * as queries from '../db/queries.js';
|
|
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';
|
|
9
|
+
const router = Router();
|
|
10
|
+
const VALID_RELATION_TYPES = new Set([
|
|
11
|
+
'related',
|
|
12
|
+
'precedes',
|
|
13
|
+
'example_of',
|
|
14
|
+
'counter_example',
|
|
15
|
+
'refines',
|
|
16
|
+
]);
|
|
17
|
+
function normalizeTags(input) {
|
|
18
|
+
if (input === undefined || input === null)
|
|
19
|
+
return null;
|
|
20
|
+
if (typeof input === 'string') {
|
|
21
|
+
if (!input.trim())
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(input);
|
|
25
|
+
if (Array.isArray(parsed)) {
|
|
26
|
+
const cleaned = parsed.map(String).map(s => s.trim()).filter(Boolean);
|
|
27
|
+
return cleaned.length > 0 ? JSON.stringify(cleaned) : null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(input)) {
|
|
36
|
+
const cleaned = input.map(String).map(s => s.trim()).filter(Boolean);
|
|
37
|
+
return cleaned.length > 0 ? JSON.stringify(cleaned) : null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function isValidRelation(value) {
|
|
42
|
+
return typeof value === 'string' && VALID_RELATION_TYPES.has(value);
|
|
43
|
+
}
|
|
44
|
+
// ── Graph (combined) ──
|
|
45
|
+
router.get('/projects/:id/memory/graph', (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const project = queries.getProjectById(req.params.id);
|
|
48
|
+
if (!project) {
|
|
49
|
+
res.status(404).json({ error: 'Project not found' });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const nodes = queries.getMemoryNodesByProjectId(req.params.id);
|
|
53
|
+
const edges = queries.getMemoryEdgesByProjectId(req.params.id);
|
|
54
|
+
res.json({ nodes, edges });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// ── Nodes ──
|
|
61
|
+
router.get('/projects/:id/memory/nodes', (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const project = queries.getProjectById(req.params.id);
|
|
64
|
+
if (!project) {
|
|
65
|
+
res.status(404).json({ error: 'Project not found' });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res.json(queries.getMemoryNodesByProjectId(req.params.id));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
router.post('/projects/:id/memory/nodes', (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const project = queries.getProjectById(req.params.id);
|
|
77
|
+
if (!project) {
|
|
78
|
+
res.status(404).json({ error: 'Project not found' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const { title, body, tags, pinned } = req.body ?? {};
|
|
82
|
+
if (!title || typeof title !== 'string' || !title.trim()) {
|
|
83
|
+
res.status(400).json({ error: 'title is required' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
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
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
router.put('/memory/nodes/:nodeId', (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const existing = queries.getMemoryNodeById(req.params.nodeId);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
res.status(404).json({ error: 'Memory node not found' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { title, body, tags, pinned } = req.body ?? {};
|
|
116
|
+
const updates = {};
|
|
117
|
+
let titleChange = null;
|
|
118
|
+
if (title !== undefined) {
|
|
119
|
+
if (typeof title !== 'string' || !title.trim()) {
|
|
120
|
+
res.status(400).json({ error: 'title cannot be empty' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
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;
|
|
135
|
+
}
|
|
136
|
+
if (body !== undefined)
|
|
137
|
+
updates.body = typeof body === 'string' ? body : '';
|
|
138
|
+
if (tags !== undefined)
|
|
139
|
+
updates.tags = normalizeTags(tags);
|
|
140
|
+
if (pinned !== undefined)
|
|
141
|
+
updates.pinned = pinned ? 1 : 0;
|
|
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
|
+
}
|
|
168
|
+
res.json(updated);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
router.put('/memory/nodes/:nodeId/position', (req, res) => {
|
|
175
|
+
try {
|
|
176
|
+
const existing = queries.getMemoryNodeById(req.params.nodeId);
|
|
177
|
+
if (!existing) {
|
|
178
|
+
res.status(404).json({ error: 'Memory node not found' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const { position_x, position_y } = req.body ?? {};
|
|
182
|
+
const x = Number(position_x);
|
|
183
|
+
const y = Number(position_y);
|
|
184
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
185
|
+
res.status(400).json({ error: 'position_x and position_y must be numbers' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
queries.updateMemoryNodePosition(req.params.nodeId, x, y);
|
|
189
|
+
res.status(204).send();
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
router.delete('/memory/nodes/:nodeId', (req, res) => {
|
|
196
|
+
try {
|
|
197
|
+
const existing = queries.getMemoryNodeById(req.params.nodeId);
|
|
198
|
+
if (!existing) {
|
|
199
|
+
res.status(404).json({ error: 'Memory node not found' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
queries.deleteMemoryNode(req.params.nodeId);
|
|
203
|
+
res.status(204).send();
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
207
|
+
}
|
|
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
|
+
});
|
|
286
|
+
// ── Edges ──
|
|
287
|
+
router.post('/projects/:id/memory/edges', (req, res) => {
|
|
288
|
+
try {
|
|
289
|
+
const project = queries.getProjectById(req.params.id);
|
|
290
|
+
if (!project) {
|
|
291
|
+
res.status(404).json({ error: 'Project not found' });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const { from_node_id, to_node_id, relation_type, label } = req.body ?? {};
|
|
295
|
+
if (!from_node_id || !to_node_id) {
|
|
296
|
+
res.status(400).json({ error: 'from_node_id and to_node_id are required' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (from_node_id === to_node_id) {
|
|
300
|
+
res.status(400).json({ error: 'Self-edges are not allowed' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const fromNode = queries.getMemoryNodeById(from_node_id);
|
|
304
|
+
const toNode = queries.getMemoryNodeById(to_node_id);
|
|
305
|
+
if (!fromNode || !toNode) {
|
|
306
|
+
res.status(404).json({ error: 'Source or target node not found' });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (fromNode.project_id !== req.params.id || toNode.project_id !== req.params.id) {
|
|
310
|
+
res.status(400).json({ error: 'Nodes must belong to this project' });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const rt = isValidRelation(relation_type) ? relation_type : 'related';
|
|
314
|
+
try {
|
|
315
|
+
const edge = queries.createMemoryEdge(req.params.id, from_node_id, to_node_id, rt, label ?? null);
|
|
316
|
+
res.status(201).json(edge);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
320
|
+
if (msg.includes('UNIQUE')) {
|
|
321
|
+
res.status(409).json({ error: 'An edge with this relation already exists between these nodes' });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
router.put('/memory/edges/:edgeId', (req, res) => {
|
|
332
|
+
try {
|
|
333
|
+
const existing = queries.getMemoryEdgeById(req.params.edgeId);
|
|
334
|
+
if (!existing) {
|
|
335
|
+
res.status(404).json({ error: 'Memory edge not found' });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const { relation_type, label } = req.body ?? {};
|
|
339
|
+
const updates = {};
|
|
340
|
+
if (relation_type !== undefined) {
|
|
341
|
+
if (!isValidRelation(relation_type)) {
|
|
342
|
+
res.status(400).json({ error: 'Invalid relation_type' });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
updates.relation_type = relation_type;
|
|
346
|
+
}
|
|
347
|
+
if (label !== undefined)
|
|
348
|
+
updates.label = label === null ? null : String(label);
|
|
349
|
+
const updated = queries.updateMemoryEdge(req.params.edgeId, updates);
|
|
350
|
+
res.json(updated);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
router.delete('/memory/edges/:edgeId', (req, res) => {
|
|
357
|
+
try {
|
|
358
|
+
const existing = queries.getMemoryEdgeById(req.params.edgeId);
|
|
359
|
+
if (!existing) {
|
|
360
|
+
res.status(404).json({ error: 'Memory edge not found' });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
queries.deleteMemoryEdge(req.params.edgeId);
|
|
364
|
+
res.status(204).send();
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
368
|
+
}
|
|
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
|
+
});
|
|
628
|
+
// ── Preview (build prompt block) ──
|
|
629
|
+
router.post('/projects/:id/memory/preview', (req, res) => {
|
|
630
|
+
try {
|
|
631
|
+
const project = queries.getProjectById(req.params.id);
|
|
632
|
+
if (!project) {
|
|
633
|
+
res.status(404).json({ error: 'Project not found' });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const { mode, nodeIds } = req.body ?? {};
|
|
637
|
+
const m = (mode === 'all' || mode === 'selected') ? mode : 'none';
|
|
638
|
+
const ids = Array.isArray(nodeIds) ? nodeIds.map(String).filter(Boolean) : [];
|
|
639
|
+
const result = buildMemoryBlock({ projectId: req.params.id, mode: m, nodeIds: ids });
|
|
640
|
+
res.json({
|
|
641
|
+
prompt: result?.block ?? '',
|
|
642
|
+
nodeCount: result?.nodeCount ?? 0,
|
|
643
|
+
edgeCount: result?.edgeCount ?? 0,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
export default router;
|
|
651
|
+
//# sourceMappingURL=memory.js.map
|