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.
- package/README.md +6 -3
- package/README_KR.md +6 -3
- 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/db/queries.d.ts +39 -3
- package/dist/server/db/queries.d.ts.map +1 -1
- package/dist/server/db/queries.js +118 -3
- 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 +65 -0
- package/dist/server/db/schema.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.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/memory.d.ts.map +1 -1
- package/dist/server/routes/memory.js +396 -4
- package/dist/server/routes/memory.js.map +1 -1
- package/dist/server/routes/projects.js +2 -2
- package/dist/server/routes/projects.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/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-orchestrator.d.ts.map +1 -1
- package/dist/server/services/discussion-orchestrator.js +22 -0
- package/dist/server/services/discussion-orchestrator.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-injector.d.ts.map +1 -1
- package/dist/server/services/memory-injector.js +32 -2
- package/dist/server/services/memory-injector.js.map +1 -1
- 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/orchestrator.d.ts.map +1 -1
- package/dist/server/services/orchestrator.js +10 -0
- package/dist/server/services/orchestrator.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 +1 -1
- package/dist/client/assets/index-CcsvxPmx.css +0 -1
- 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":"
|
|
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
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|