@rigour-labs/cli 5.4.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/studio.js +792 -496
- package/package.json +2 -2
- package/studio-dist/assets/index-BvR6Si_S.js +362 -0
- package/studio-dist/assets/index-jC3k12bf.css +1 -0
- package/studio-dist/index.html +3 -3
- package/studio-dist/assets/index-BLGQnkLS.js +0 -322
- package/studio-dist/assets/index-hcvdpk3Y.css +0 -1
package/dist/commands/studio.js
CHANGED
|
@@ -1,573 +1,869 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import { execa } from 'execa';
|
|
5
6
|
import fs from 'fs-extra';
|
|
6
7
|
import http from 'http';
|
|
7
8
|
import { randomUUID } from 'crypto';
|
|
8
|
-
|
|
9
|
-
.
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// When running from source: cli/dist/commands/studio.js → cli/studio-dist/
|
|
18
|
-
// When running via npx: node_modules/@rigour-labs/cli/dist/commands/studio.js → cli/dist/studio-dist/
|
|
19
|
-
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
20
|
-
const candidates = [
|
|
21
|
-
path.join(__dirname, '../studio-dist'), // npm publish: cli/dist/studio-dist/
|
|
22
|
-
path.join(__dirname, '../../studio-dist'), // monorepo: cli/studio-dist/
|
|
23
|
-
path.join(__dirname, '../../../studio-dist'), // npx: @rigour-labs/cli/studio-dist/
|
|
24
|
-
];
|
|
25
|
-
const localStudioDist = candidates.find(p => fs.pathExistsSync(p)) ?? candidates[0];
|
|
26
|
-
const workspaceRoot = path.join(__dirname, '../../../../');
|
|
27
|
-
console.log(chalk.bold.cyan('\n🛡️ Launching Rigour Studio...'));
|
|
28
|
-
console.log(chalk.gray(`Project Root: ${cwd}`));
|
|
29
|
-
// Pre-flight check: Is the project initialized?
|
|
30
|
-
const configPath = path.join(cwd, 'rigour.yml');
|
|
31
|
-
if (!(await fs.pathExists(configPath))) {
|
|
32
|
-
console.log(chalk.yellow('\n⚠️ Warning: rigour.yml not found.'));
|
|
33
|
-
console.log(chalk.dim('The Studio will be empty until you initialize the project.'));
|
|
34
|
-
console.log(chalk.cyan('Suggest: ') + chalk.bold('npx @rigour-labs/cli init') + '\n');
|
|
9
|
+
function sendJson(res, status, body) {
|
|
10
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
11
|
+
res.end(JSON.stringify(body));
|
|
12
|
+
}
|
|
13
|
+
async function readJsonIfExists(filePath) {
|
|
14
|
+
if (!(await fs.pathExists(filePath)))
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
return await fs.readJson(filePath);
|
|
35
18
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function mergeMemoryStores(cwd) {
|
|
24
|
+
const sources = [];
|
|
25
|
+
const memories = {};
|
|
26
|
+
const projectPath = path.join(cwd, '.rigour/memory.json');
|
|
27
|
+
const globalPath = path.join(os.homedir(), '.rigour/memory.json');
|
|
28
|
+
for (const [label, filePath] of [
|
|
29
|
+
['project', projectPath],
|
|
30
|
+
['global', globalPath],
|
|
31
|
+
]) {
|
|
32
|
+
const data = await readJsonIfExists(filePath);
|
|
33
|
+
if (!data)
|
|
34
|
+
continue;
|
|
35
|
+
sources.push(label);
|
|
36
|
+
const entries = data.memories && typeof data.memories === 'object' ? data.memories : data;
|
|
37
|
+
for (const [key, value] of Object.entries(entries || {})) {
|
|
38
|
+
const namespaced = memories[key] ? `${label}:${key}` : key;
|
|
39
|
+
memories[namespaced] = {
|
|
40
|
+
...(typeof value === 'object' && value !== null ? value : { value }),
|
|
41
|
+
source: label,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { memories, sources };
|
|
46
|
+
}
|
|
47
|
+
function mapCheckpointMetrics(metrics) {
|
|
48
|
+
return metrics.map((m) => {
|
|
49
|
+
const raw = m.rawStateTokens || 0;
|
|
50
|
+
const packed = m.checkpointTokens || 0;
|
|
51
|
+
const avoided = m.replayTokensAvoided || Math.max(0, raw - packed);
|
|
52
|
+
const compression = packed > 0 ? Math.round(raw / packed) : 0;
|
|
53
|
+
const qualityScore = Math.max(40, Math.min(100, 60 + Math.min(40, compression)));
|
|
54
|
+
const createdAt = m.createdAt ?? Date.now();
|
|
55
|
+
return {
|
|
56
|
+
checkpointId: m.checkpointId,
|
|
57
|
+
agentId: m.agentId,
|
|
58
|
+
taskId: m.taskId,
|
|
59
|
+
timestamp: new Date(createdAt).toISOString(),
|
|
60
|
+
progressPct: Math.min(100, Math.round((avoided / Math.max(raw, 1)) * 100)),
|
|
61
|
+
filesChanged: [],
|
|
62
|
+
summary: `Compressed ${raw.toLocaleString()} → ${packed.toLocaleString()} tokens; avoided ${avoided.toLocaleString()} replay tokens${compression ? ` (${compression}×)` : ''}.`,
|
|
63
|
+
qualityScore,
|
|
64
|
+
warnings: [],
|
|
65
|
+
rawStateTokens: raw,
|
|
66
|
+
checkpointTokens: packed,
|
|
67
|
+
replayTokensAvoided: avoided,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function synthesizeAgents(cwd, checkpoints) {
|
|
72
|
+
const sessionPath = path.join(cwd, '.rigour/agent-session.json');
|
|
73
|
+
const session = await readJsonIfExists(sessionPath);
|
|
74
|
+
if (session?.agents?.length) {
|
|
75
|
+
return session;
|
|
76
|
+
}
|
|
77
|
+
const byAgent = new Map();
|
|
78
|
+
for (const cp of checkpoints) {
|
|
79
|
+
const existing = byAgent.get(cp.agentId);
|
|
80
|
+
if (!existing) {
|
|
81
|
+
byAgent.set(cp.agentId, {
|
|
82
|
+
agentId: cp.agentId,
|
|
83
|
+
taskScope: cp.taskId ? [`task:${cp.taskId}`] : [],
|
|
84
|
+
registeredAt: cp.timestamp,
|
|
85
|
+
lastCheckpoint: cp.timestamp,
|
|
86
|
+
status: 'completed',
|
|
46
87
|
});
|
|
47
|
-
await setupApiAndLaunch(apiPort, options.port, eventsPath, cwd, studioProcess);
|
|
48
|
-
return;
|
|
49
88
|
}
|
|
50
|
-
|
|
51
|
-
|
|
89
|
+
else {
|
|
90
|
+
existing.lastCheckpoint = cp.timestamp;
|
|
91
|
+
if (cp.taskId && !existing.taskScope.includes(`task:${cp.taskId}`)) {
|
|
92
|
+
existing.taskScope.push(`task:${cp.taskId}`);
|
|
93
|
+
}
|
|
52
94
|
}
|
|
53
95
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
96
|
+
const agents = [...byAgent.values()];
|
|
97
|
+
return {
|
|
98
|
+
sessionId: agents.length ? 'derived-from-checkpoints' : 'inactive',
|
|
99
|
+
agents,
|
|
100
|
+
status: agents.length ? 'completed' : 'inactive',
|
|
101
|
+
createdAt: agents[0]?.registeredAt || new Date().toISOString(),
|
|
102
|
+
derived: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function handleApiRequest(req, res, url, ctx) {
|
|
106
|
+
if (!url.pathname.startsWith('/api'))
|
|
107
|
+
return false;
|
|
108
|
+
const requestOrigin = req.headers.origin;
|
|
109
|
+
if (typeof requestOrigin === 'string' && ctx.allowedOrigins.has(requestOrigin)) {
|
|
110
|
+
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
|
111
|
+
res.setHeader('Vary', 'Origin');
|
|
60
112
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
113
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS, POST, DELETE');
|
|
114
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
115
|
+
if (req.method === 'OPTIONS') {
|
|
116
|
+
res.writeHead(204);
|
|
117
|
+
res.end();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
const { cwd, eventsPath } = ctx;
|
|
121
|
+
if (url.pathname === '/api/events') {
|
|
122
|
+
res.writeHead(200, {
|
|
123
|
+
'Content-Type': 'text/event-stream',
|
|
124
|
+
'Cache-Control': 'no-cache',
|
|
125
|
+
Connection: 'keep-alive',
|
|
126
|
+
});
|
|
127
|
+
if (await fs.pathExists(eventsPath)) {
|
|
128
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
129
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
130
|
+
// Send recent history only — full 77MB dumps freeze the UI
|
|
131
|
+
for (const line of lines.slice(-200)) {
|
|
132
|
+
res.write(`data: ${line}\n\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
await fs.ensureDir(path.dirname(eventsPath));
|
|
136
|
+
const watcher = fs.watch(path.dirname(eventsPath), async (_eventType, filename) => {
|
|
137
|
+
if (filename === 'events.jsonl') {
|
|
138
|
+
try {
|
|
139
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
140
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
141
|
+
const lastLine = lines[lines.length - 1];
|
|
142
|
+
if (lastLine)
|
|
143
|
+
res.write(`data: ${lastLine}\n\n`);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore transient reads
|
|
147
|
+
}
|
|
67
148
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
149
|
+
});
|
|
150
|
+
req.on('close', () => watcher.close());
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (url.pathname === '/api/file') {
|
|
154
|
+
const filePath = url.searchParams.get('path');
|
|
155
|
+
if (!filePath) {
|
|
156
|
+
res.writeHead(400);
|
|
157
|
+
res.end('Missing path');
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
const absolutePath = path.resolve(cwd, filePath);
|
|
161
|
+
if (!absolutePath.startsWith(cwd)) {
|
|
162
|
+
res.writeHead(403);
|
|
163
|
+
res.end('Forbidden');
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
168
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
81
169
|
res.end(content);
|
|
82
170
|
}
|
|
83
|
-
catch
|
|
171
|
+
catch {
|
|
84
172
|
res.writeHead(404);
|
|
85
|
-
res.end('Not
|
|
173
|
+
res.end('Not found');
|
|
86
174
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (req.method === 'OPTIONS') {
|
|
107
|
-
res.writeHead(204);
|
|
108
|
-
res.end();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (url.pathname === '/api/events') {
|
|
112
|
-
res.writeHead(200, {
|
|
113
|
-
'Content-Type': 'text/event-stream',
|
|
114
|
-
'Cache-Control': 'no-cache',
|
|
115
|
-
'Connection': 'keep-alive'
|
|
116
|
-
});
|
|
117
|
-
if (await fs.pathExists(eventsPath)) {
|
|
118
|
-
const content = await fs.readFile(eventsPath, 'utf8');
|
|
119
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
120
|
-
for (const line of lines) {
|
|
121
|
-
res.write(`data: ${line}\n\n`);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (url.pathname === '/api/info') {
|
|
178
|
+
try {
|
|
179
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
180
|
+
const pkg = (await fs.pathExists(pkgPath)) ? await fs.readJson(pkgPath) : {};
|
|
181
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
182
|
+
const cliPkgPath = path.join(__dirname, '../../package.json');
|
|
183
|
+
const mcpPkgCandidates = [
|
|
184
|
+
path.join(__dirname, '../../../rigour-mcp/package.json'),
|
|
185
|
+
path.join(__dirname, '../../../../packages/rigour-mcp/package.json'),
|
|
186
|
+
];
|
|
187
|
+
const cliPkg = (await fs.pathExists(cliPkgPath)) ? await fs.readJson(cliPkgPath) : {};
|
|
188
|
+
let mcpVersion = '5.5.0';
|
|
189
|
+
for (const candidate of mcpPkgCandidates) {
|
|
190
|
+
if (await fs.pathExists(candidate)) {
|
|
191
|
+
const mcpPkg = await fs.readJson(candidate);
|
|
192
|
+
mcpVersion = mcpPkg.version || mcpVersion;
|
|
193
|
+
break;
|
|
122
194
|
}
|
|
123
195
|
}
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
// Product version is the MCP/governance release; CLI package may differ.
|
|
197
|
+
const studioVersion = mcpVersion || cliPkg.version || '0.0.0';
|
|
198
|
+
sendJson(res, 200, {
|
|
199
|
+
name: pkg.name || path.basename(cwd),
|
|
200
|
+
projectName: pkg.name || path.basename(cwd),
|
|
201
|
+
path: cwd,
|
|
202
|
+
projectPath: cwd,
|
|
203
|
+
version: pkg.version || '0.0.0',
|
|
204
|
+
projectVersion: pkg.version || '0.0.0',
|
|
205
|
+
studioVersion,
|
|
206
|
+
mcpVersion,
|
|
207
|
+
brainDb: path.join(os.homedir(), '.rigour/rigour.db'),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
res.writeHead(500);
|
|
212
|
+
res.end(e.message);
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (url.pathname === '/api/tree') {
|
|
217
|
+
try {
|
|
218
|
+
const getTree = async (dir) => {
|
|
219
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
220
|
+
let files = [];
|
|
221
|
+
const exclude = ['node_modules', '.git', '.rigour', '.venv', 'dist', 'build'];
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (exclude.includes(entry.name) || entry.name.startsWith('.'))
|
|
224
|
+
continue;
|
|
225
|
+
const fullPath = path.join(dir, entry.name);
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
files = [...files, ...(await getTree(fullPath))];
|
|
134
228
|
}
|
|
135
|
-
|
|
136
|
-
|
|
229
|
+
else {
|
|
230
|
+
files.push(path.relative(cwd, fullPath));
|
|
137
231
|
}
|
|
138
232
|
}
|
|
139
|
-
|
|
140
|
-
|
|
233
|
+
return files;
|
|
234
|
+
};
|
|
235
|
+
sendJson(res, 200, await getTree(cwd));
|
|
141
236
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
res.
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
const content = await fs.readFile(absolutePath, 'utf8');
|
|
157
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
158
|
-
res.end(content);
|
|
237
|
+
catch (e) {
|
|
238
|
+
res.writeHead(500);
|
|
239
|
+
res.end(e.message);
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (url.pathname === '/api/config') {
|
|
244
|
+
try {
|
|
245
|
+
const configPath = path.join(cwd, 'rigour.yml');
|
|
246
|
+
if (await fs.pathExists(configPath)) {
|
|
247
|
+
res.writeHead(200, { 'Content-Type': 'text/yaml' });
|
|
248
|
+
res.end(await fs.readFile(configPath, 'utf8'));
|
|
159
249
|
}
|
|
160
|
-
|
|
250
|
+
else {
|
|
161
251
|
res.writeHead(404);
|
|
162
252
|
res.end('Not found');
|
|
163
253
|
}
|
|
164
254
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const pkg = await fs.pathExists(pkgPath) ? await fs.readJson(pkgPath) : {};
|
|
169
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
170
|
-
res.end(JSON.stringify({
|
|
171
|
-
name: pkg.name || path.basename(cwd),
|
|
172
|
-
path: cwd,
|
|
173
|
-
version: pkg.version || '0.0.0'
|
|
174
|
-
}));
|
|
175
|
-
}
|
|
176
|
-
catch (e) {
|
|
177
|
-
res.writeHead(500);
|
|
178
|
-
res.end(e.message);
|
|
179
|
-
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
res.writeHead(500);
|
|
257
|
+
res.end(e.message);
|
|
180
258
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const exclude = ['node_modules', '.git', '.rigour', '.venv', 'dist', 'build'];
|
|
187
|
-
for (const entry of entries) {
|
|
188
|
-
if (exclude.includes(entry.name) || entry.name.startsWith('.'))
|
|
189
|
-
continue;
|
|
190
|
-
const fullPath = path.join(dir, entry.name);
|
|
191
|
-
if (entry.isDirectory()) {
|
|
192
|
-
files = [...files, ...(await getTree(fullPath))];
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
files.push(path.relative(cwd, fullPath));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return files;
|
|
199
|
-
};
|
|
200
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
201
|
-
res.end(JSON.stringify(await getTree(cwd)));
|
|
202
|
-
}
|
|
203
|
-
catch (e) {
|
|
204
|
-
res.writeHead(500);
|
|
205
|
-
res.end(e.message);
|
|
206
|
-
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (url.pathname === '/api/memory') {
|
|
262
|
+
try {
|
|
263
|
+
sendJson(res, 200, await mergeMemoryStores(cwd));
|
|
207
264
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const configPath = path.join(cwd, 'rigour.yml');
|
|
211
|
-
if (await fs.pathExists(configPath)) {
|
|
212
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
213
|
-
res.end(await fs.readFile(configPath, 'utf8'));
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
res.writeHead(404);
|
|
217
|
-
res.end('Not found');
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
catch (e) {
|
|
221
|
-
res.writeHead(500);
|
|
222
|
-
res.end(e.message);
|
|
223
|
-
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
sendJson(res, 500, { error: e.message });
|
|
224
267
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
else {
|
|
233
|
-
res.end(JSON.stringify({}));
|
|
234
|
-
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (url.pathname === '/api/index-stats') {
|
|
271
|
+
try {
|
|
272
|
+
const indexPath = path.join(cwd, '.rigour/patterns.json');
|
|
273
|
+
if (await fs.pathExists(indexPath)) {
|
|
274
|
+
sendJson(res, 200, await fs.readJson(indexPath));
|
|
235
275
|
}
|
|
236
|
-
|
|
237
|
-
res
|
|
238
|
-
res.end(e.message);
|
|
276
|
+
else {
|
|
277
|
+
sendJson(res, 200, { patterns: [], stats: { totalPatterns: 0, totalFiles: 0, byType: {} } });
|
|
239
278
|
}
|
|
240
279
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const indexPath = path.join(cwd, '.rigour/patterns.json');
|
|
244
|
-
if (await fs.pathExists(indexPath)) {
|
|
245
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
246
|
-
res.end(JSON.stringify(await fs.readJson(indexPath)));
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
res.end(JSON.stringify({ patterns: [], stats: { totalPatterns: 0, totalFiles: 0, byType: {} } }));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch (e) {
|
|
253
|
-
res.writeHead(500);
|
|
254
|
-
res.end(e.message);
|
|
255
|
-
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
sendJson(res, 500, { error: e.message });
|
|
256
282
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const { generateEmbedding, semanticSearch } = await import('@rigour-labs/core/pattern-index');
|
|
266
|
-
const indexPath = path.join(cwd, '.rigour/patterns.json');
|
|
267
|
-
const indexData = await fs.readJson(indexPath);
|
|
268
|
-
const queryVector = await generateEmbedding(query);
|
|
269
|
-
const similarities = semanticSearch(queryVector, indexData.patterns);
|
|
270
|
-
const results = indexData.patterns.map((p, i) => ({ ...p, similarity: similarities[i] }))
|
|
271
|
-
.filter((p) => p.similarity > 0.3)
|
|
272
|
-
.sort((a, b) => b.similarity - a.similarity)
|
|
273
|
-
.slice(0, 20);
|
|
274
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
275
|
-
res.end(JSON.stringify(results));
|
|
276
|
-
}
|
|
277
|
-
catch (e) {
|
|
278
|
-
res.writeHead(500);
|
|
279
|
-
res.end(e.message);
|
|
280
|
-
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
if (url.pathname === '/api/index-search') {
|
|
286
|
+
const query = url.searchParams.get('q');
|
|
287
|
+
if (!query) {
|
|
288
|
+
res.writeHead(400);
|
|
289
|
+
res.end('Missing query');
|
|
290
|
+
return true;
|
|
281
291
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
catch (e) {
|
|
295
|
-
res.writeHead(500);
|
|
296
|
-
res.end(e.message);
|
|
297
|
-
}
|
|
292
|
+
try {
|
|
293
|
+
const { generateEmbedding, semanticSearch } = await import('@rigour-labs/core/pattern-index');
|
|
294
|
+
const indexPath = path.join(cwd, '.rigour/patterns.json');
|
|
295
|
+
const indexData = await fs.readJson(indexPath);
|
|
296
|
+
const queryVector = await generateEmbedding(query);
|
|
297
|
+
const similarities = semanticSearch(queryVector, indexData.patterns);
|
|
298
|
+
const results = indexData.patterns
|
|
299
|
+
.map((p, i) => ({ ...p, similarity: similarities[i] }))
|
|
300
|
+
.filter((p) => p.similarity > 0.3)
|
|
301
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
302
|
+
.slice(0, 20);
|
|
303
|
+
sendJson(res, 200, results);
|
|
298
304
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const checkpointPath = path.join(cwd, '.rigour/checkpoint-session.json');
|
|
302
|
-
if (await fs.pathExists(checkpointPath)) {
|
|
303
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
304
|
-
res.end(await fs.readFile(checkpointPath, 'utf-8'));
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
308
|
-
res.end(JSON.stringify({ checkpoints: [], status: 'inactive' }));
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
catch (e) {
|
|
312
|
-
res.writeHead(500);
|
|
313
|
-
res.end(e.message);
|
|
314
|
-
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
sendJson(res, 500, { error: e.message });
|
|
315
307
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (url.pathname === '/api/checkpoints' || url.pathname === '/api/agents') {
|
|
311
|
+
try {
|
|
312
|
+
const { getCheckpointMetrics, } = await import('@rigour-labs/core');
|
|
313
|
+
const metrics = await getCheckpointMetrics(undefined, cwd);
|
|
314
|
+
const mapped = mapCheckpointMetrics(metrics);
|
|
315
|
+
const sessionFile = await readJsonIfExists(path.join(cwd, '.rigour/checkpoint-session.json'));
|
|
316
|
+
const sessionCheckpoints = Array.isArray(sessionFile?.checkpoints) ? sessionFile.checkpoints : [];
|
|
317
|
+
const checkpoints = sessionCheckpoints.length > 0 ? sessionCheckpoints : mapped;
|
|
318
|
+
if (url.pathname === '/api/checkpoints') {
|
|
319
|
+
sendJson(res, 200, {
|
|
320
|
+
checkpoints,
|
|
321
|
+
status: checkpoints.length ? 'active' : 'inactive',
|
|
322
|
+
source: sessionCheckpoints.length ? 'session' : 'brain-metrics',
|
|
323
|
+
metricsCount: metrics.length,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
sendJson(res, 200, await synthesizeAgents(cwd, checkpoints));
|
|
332
328
|
}
|
|
333
329
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const reportPath = path.join(cwd, 'rigour-report.json');
|
|
337
|
-
if (await fs.pathExists(reportPath)) {
|
|
338
|
-
const report = await fs.readJson(reportPath);
|
|
339
|
-
const findings = (report.failures || []).filter((f) => f.provenance === 'deep-analysis' || f.source === 'llm' || f.source === 'hybrid');
|
|
340
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
341
|
-
res.end(JSON.stringify(findings));
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
345
|
-
res.end(JSON.stringify([]));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch (e) {
|
|
349
|
-
res.writeHead(500);
|
|
350
|
-
res.end(e.message);
|
|
351
|
-
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
sendJson(res, 500, { error: e.message });
|
|
352
332
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (url.pathname === '/api/overview') {
|
|
336
|
+
try {
|
|
337
|
+
const { getTaskContextStats, getTaskCostStats, getCacheStats, getCheckpointSummary, getCheckpointMetrics, } = await import('@rigour-labs/core');
|
|
338
|
+
const [context, cost, cache, checkpointSummary, metrics, memory, indexStats] = await Promise.all([
|
|
339
|
+
getTaskContextStats(undefined, cwd),
|
|
340
|
+
getTaskCostStats(undefined, cwd),
|
|
341
|
+
getCacheStats(cwd),
|
|
342
|
+
getCheckpointSummary(undefined, cwd),
|
|
343
|
+
getCheckpointMetrics(undefined, cwd),
|
|
344
|
+
mergeMemoryStores(cwd),
|
|
345
|
+
readJsonIfExists(path.join(cwd, '.rigour/patterns.json')),
|
|
346
|
+
]);
|
|
347
|
+
let recentEvents = 0;
|
|
348
|
+
if (await fs.pathExists(eventsPath)) {
|
|
349
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
350
|
+
recentEvents = content.split('\n').filter((l) => l.trim()).length;
|
|
351
|
+
}
|
|
352
|
+
sendJson(res, 200, {
|
|
353
|
+
context,
|
|
354
|
+
cost,
|
|
355
|
+
cache,
|
|
356
|
+
checkpointSummary,
|
|
357
|
+
checkpointCount: metrics.length,
|
|
358
|
+
memoryCount: Object.keys(memory.memories).length,
|
|
359
|
+
memorySources: memory.sources,
|
|
360
|
+
patternCount: indexStats?.stats?.totalPatterns ?? indexStats?.patterns?.length ?? 0,
|
|
361
|
+
patternFiles: indexStats?.stats?.totalFiles ?? 0,
|
|
362
|
+
eventCount: recentEvents,
|
|
363
|
+
projectPath: cwd,
|
|
364
|
+
brainDb: path.join(os.homedir(), '.rigour/rigour.db'),
|
|
365
|
+
});
|
|
365
366
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const { getTaskContextStats } = await import('@rigour-labs/core');
|
|
369
|
-
const taskId = url.searchParams.get('taskId') || undefined;
|
|
370
|
-
const stats = await getTaskContextStats(taskId, cwd);
|
|
371
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
372
|
-
res.end(JSON.stringify(stats));
|
|
373
|
-
}
|
|
374
|
-
catch (e) {
|
|
375
|
-
res.writeHead(500);
|
|
376
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
377
|
-
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
sendJson(res, 500, { error: e.message });
|
|
378
369
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
if (url.pathname === '/api/report-stats') {
|
|
373
|
+
try {
|
|
374
|
+
const reportPath = path.join(cwd, 'rigour-report.json');
|
|
375
|
+
if (await fs.pathExists(reportPath)) {
|
|
376
|
+
const report = await fs.readJson(reportPath);
|
|
377
|
+
sendJson(res, 200, report.stats || {});
|
|
386
378
|
}
|
|
387
|
-
|
|
388
|
-
res
|
|
389
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
379
|
+
else {
|
|
380
|
+
sendJson(res, 200, {});
|
|
390
381
|
}
|
|
391
382
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const { getCacheStats } = await import('@rigour-labs/core');
|
|
395
|
-
const stats = await getCacheStats(cwd);
|
|
396
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
397
|
-
res.end(JSON.stringify(stats));
|
|
398
|
-
}
|
|
399
|
-
catch (e) {
|
|
400
|
-
res.writeHead(500);
|
|
401
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
402
|
-
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
sendJson(res, 500, { error: e.message });
|
|
403
385
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
if (url.pathname === '/api/deep-findings') {
|
|
389
|
+
try {
|
|
390
|
+
const reportPath = path.join(cwd, 'rigour-report.json');
|
|
391
|
+
if (await fs.pathExists(reportPath)) {
|
|
392
|
+
const report = await fs.readJson(reportPath);
|
|
393
|
+
const findings = (report.failures || []).filter((f) => f.provenance === 'deep-analysis' || f.source === 'llm' || f.source === 'hybrid');
|
|
394
|
+
sendJson(res, 200, findings);
|
|
412
395
|
}
|
|
413
|
-
|
|
414
|
-
res
|
|
415
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
396
|
+
else {
|
|
397
|
+
sendJson(res, 200, []);
|
|
416
398
|
}
|
|
417
399
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const { getContextScopeSummary } = await import('@rigour-labs/core');
|
|
421
|
-
const summary = await getContextScopeSummary(cwd);
|
|
422
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
423
|
-
res.end(JSON.stringify(summary));
|
|
424
|
-
}
|
|
425
|
-
catch (e) {
|
|
426
|
-
res.writeHead(500);
|
|
427
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
428
|
-
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
sendJson(res, 500, { error: e.message });
|
|
429
402
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
catch (e) {
|
|
439
|
-
res.writeHead(500);
|
|
440
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
441
|
-
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
if (url.pathname === '/api/drift') {
|
|
406
|
+
try {
|
|
407
|
+
const { generateTemporalDriftReport } = await import('@rigour-labs/core');
|
|
408
|
+
const report = generateTemporalDriftReport(cwd);
|
|
409
|
+
sendJson(res, 200, report || { totalScans: 0 });
|
|
442
410
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
411
|
+
catch {
|
|
412
|
+
sendJson(res, 200, { totalScans: 0 });
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
if (url.pathname === '/api/context-stats') {
|
|
417
|
+
try {
|
|
418
|
+
const { getTaskContextStats } = await import('@rigour-labs/core');
|
|
419
|
+
const taskId = url.searchParams.get('taskId') || undefined;
|
|
420
|
+
sendJson(res, 200, await getTaskContextStats(taskId, cwd));
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
sendJson(res, 500, { error: e.message });
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (url.pathname === '/api/task-cost') {
|
|
428
|
+
try {
|
|
429
|
+
const { getTaskCostStats } = await import('@rigour-labs/core');
|
|
430
|
+
const taskId = url.searchParams.get('taskId') || undefined;
|
|
431
|
+
sendJson(res, 200, await getTaskCostStats(taskId, cwd));
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
sendJson(res, 500, { error: e.message });
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
if (url.pathname === '/api/cache-stats') {
|
|
439
|
+
try {
|
|
440
|
+
const { getCacheStats } = await import('@rigour-labs/core');
|
|
441
|
+
sendJson(res, 200, await getCacheStats(cwd));
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
sendJson(res, 500, { error: e.message });
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
if (url.pathname === '/api/context-explain') {
|
|
449
|
+
try {
|
|
450
|
+
const { explainContext } = await import('@rigour-labs/core');
|
|
451
|
+
const target = url.searchParams.get('target') || 'all';
|
|
452
|
+
const taskId = url.searchParams.get('taskId') || undefined;
|
|
453
|
+
sendJson(res, 200, await explainContext(target, taskId, cwd));
|
|
454
|
+
}
|
|
455
|
+
catch (e) {
|
|
456
|
+
sendJson(res, 500, { error: e.message });
|
|
457
|
+
}
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
if (url.pathname === '/api/context-scope') {
|
|
461
|
+
try {
|
|
462
|
+
const { getContextScopeSummary } = await import('@rigour-labs/core');
|
|
463
|
+
sendJson(res, 200, await getContextScopeSummary(cwd));
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
sendJson(res, 500, { error: e.message });
|
|
455
467
|
}
|
|
456
|
-
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
if (url.pathname === '/api/checkpoint-metrics') {
|
|
471
|
+
try {
|
|
472
|
+
const { getCheckpointSummary } = await import('@rigour-labs/core');
|
|
473
|
+
const taskId = url.searchParams.get('taskId') || undefined;
|
|
474
|
+
sendJson(res, 200, await getCheckpointSummary(taskId, cwd));
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
sendJson(res, 500, { error: e.message });
|
|
478
|
+
}
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
if (url.pathname === '/api/cursor-api-key/status') {
|
|
482
|
+
try {
|
|
483
|
+
const { getCursorApiKey, getCursorApiKeyHint, countCursorAdminImportedEvents, } = await import('@rigour-labs/core');
|
|
484
|
+
const key = getCursorApiKey();
|
|
485
|
+
const fromEnv = Boolean(process.env.RIGOUR_CURSOR_API_KEY?.trim() || process.env.CURSOR_ADMIN_API_KEY?.trim());
|
|
486
|
+
sendJson(res, 200, {
|
|
487
|
+
configured: Boolean(key),
|
|
488
|
+
hint: getCursorApiKeyHint(),
|
|
489
|
+
source: key ? (fromEnv ? 'env' : 'file') : 'none',
|
|
490
|
+
importedCount: await countCursorAdminImportedEvents(cwd),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
sendJson(res, 500, { error: e.message });
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
if (url.pathname === '/api/cursor-sync' && req.method === 'POST') {
|
|
499
|
+
try {
|
|
500
|
+
const { syncCursorUsageFromAdminApi } = await import('@rigour-labs/core');
|
|
501
|
+
const result = await syncCursorUsageFromAdminApi(cwd);
|
|
502
|
+
sendJson(res, 200, { success: true, ...result });
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
sendJson(res, 502, { success: false, error: e.message || 'Cursor usage sync failed' });
|
|
506
|
+
}
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
if (url.pathname === '/api/cursor-api-key' && req.method === 'DELETE') {
|
|
510
|
+
try {
|
|
511
|
+
const { removeCursorApiKey, getCursorApiKey, getCursorApiKeyHint, } = await import('@rigour-labs/core');
|
|
512
|
+
removeCursorApiKey();
|
|
513
|
+
const key = getCursorApiKey();
|
|
514
|
+
const fromEnv = Boolean(process.env.RIGOUR_CURSOR_API_KEY?.trim() || process.env.CURSOR_ADMIN_API_KEY?.trim());
|
|
515
|
+
sendJson(res, 200, {
|
|
516
|
+
success: true,
|
|
517
|
+
configured: Boolean(key),
|
|
518
|
+
hint: getCursorApiKeyHint(),
|
|
519
|
+
source: key ? (fromEnv ? 'env' : 'file') : 'none',
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (e) {
|
|
523
|
+
sendJson(res, 500, { error: e.message });
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
if (url.pathname === '/api/cursor-api-key' && req.method === 'POST') {
|
|
528
|
+
let body = '';
|
|
529
|
+
req.on('data', (chunk) => (body += chunk));
|
|
530
|
+
req.on('end', async () => {
|
|
457
531
|
try {
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
532
|
+
const parsed = JSON.parse(body || '{}');
|
|
533
|
+
const apiKey = typeof parsed.apiKey === 'string' ? parsed.apiKey.trim() : '';
|
|
534
|
+
if (!apiKey || apiKey.length > 512) {
|
|
535
|
+
sendJson(res, 400, { error: 'Missing or invalid apiKey' });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const { updateCursorApiKey, syncCursorUsageFromAdminApi, getCursorApiKeyHint, } = await import('@rigour-labs/core');
|
|
539
|
+
updateCursorApiKey(apiKey);
|
|
540
|
+
let syncResult = { importedCount: 0, totalEvents: 0 };
|
|
541
|
+
let syncError;
|
|
542
|
+
try {
|
|
543
|
+
syncResult = await syncCursorUsageFromAdminApi(cwd);
|
|
544
|
+
}
|
|
545
|
+
catch (syncErr) {
|
|
546
|
+
syncError = syncErr?.message || 'Initial Cursor sync failed';
|
|
547
|
+
}
|
|
548
|
+
sendJson(res, 200, {
|
|
549
|
+
success: true,
|
|
550
|
+
configured: true,
|
|
551
|
+
hint: getCursorApiKeyHint(),
|
|
552
|
+
source: 'file',
|
|
553
|
+
importedCount: syncResult.importedCount,
|
|
554
|
+
totalEvents: syncResult.totalEvents,
|
|
555
|
+
syncError,
|
|
556
|
+
});
|
|
462
557
|
}
|
|
463
558
|
catch (e) {
|
|
464
|
-
res
|
|
465
|
-
res.end(JSON.stringify({
|
|
466
|
-
success: false,
|
|
467
|
-
error: e.message || 'Cursor usage sync failed',
|
|
468
|
-
}));
|
|
559
|
+
sendJson(res, 500, { error: e.message });
|
|
469
560
|
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
const { updateCursorApiKey, syncCursorUsageFromAdminApi } = await import('@rigour-labs/core');
|
|
483
|
-
updateCursorApiKey(apiKey.trim());
|
|
484
|
-
let syncResult = { importedCount: 0, totalEvents: 0 };
|
|
485
|
-
let syncError;
|
|
561
|
+
});
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
if (url.pathname === '/api/handoffs') {
|
|
565
|
+
try {
|
|
566
|
+
const handoffPath = path.join(cwd, '.rigour/handoffs.jsonl');
|
|
567
|
+
const handoffs = [];
|
|
568
|
+
if (await fs.pathExists(handoffPath)) {
|
|
569
|
+
const content = await fs.readFile(handoffPath, 'utf8');
|
|
570
|
+
for (const line of content.split('\n').filter((l) => l.trim())) {
|
|
486
571
|
try {
|
|
487
|
-
|
|
572
|
+
handoffs.push(JSON.parse(line));
|
|
488
573
|
}
|
|
489
|
-
catch
|
|
490
|
-
|
|
574
|
+
catch {
|
|
575
|
+
// skip bad lines
|
|
491
576
|
}
|
|
492
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
493
|
-
res.end(JSON.stringify({
|
|
494
|
-
success: true,
|
|
495
|
-
configured: true,
|
|
496
|
-
importedCount: syncResult.importedCount,
|
|
497
|
-
totalEvents: syncResult.totalEvents,
|
|
498
|
-
syncError,
|
|
499
|
-
}));
|
|
500
577
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
578
|
+
}
|
|
579
|
+
if (await fs.pathExists(eventsPath)) {
|
|
580
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
581
|
+
for (const line of content.split('\n').filter((l) => l.trim()).slice(-500)) {
|
|
582
|
+
try {
|
|
583
|
+
const ev = JSON.parse(line);
|
|
584
|
+
if (ev.type === 'handoff_accepted' && ev.handoffId) {
|
|
585
|
+
const target = handoffs.find((h) => h.handoffId === ev.handoffId);
|
|
586
|
+
if (target) {
|
|
587
|
+
target.status = 'accepted';
|
|
588
|
+
target.acceptedAt = ev.timestamp || ev.ts;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// skip
|
|
594
|
+
}
|
|
504
595
|
}
|
|
596
|
+
}
|
|
597
|
+
sendJson(res, 200, {
|
|
598
|
+
handoffs: handoffs.slice(-100).reverse(),
|
|
599
|
+
count: handoffs.length,
|
|
505
600
|
});
|
|
506
601
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
602
|
+
catch (e) {
|
|
603
|
+
sendJson(res, 500, { error: e.message });
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
if (url.pathname === '/api/enforcement') {
|
|
608
|
+
try {
|
|
609
|
+
const { getCheckpointMetrics, } = await import('@rigour-labs/core');
|
|
610
|
+
const metrics = await getCheckpointMetrics(undefined, cwd);
|
|
611
|
+
const mapped = mapCheckpointMetrics(metrics);
|
|
612
|
+
const agentsSession = await synthesizeAgents(cwd, mapped);
|
|
613
|
+
const memory = await mergeMemoryStores(cwd);
|
|
614
|
+
let events = [];
|
|
615
|
+
if (await fs.pathExists(eventsPath)) {
|
|
616
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
617
|
+
events = content
|
|
618
|
+
.split('\n')
|
|
619
|
+
.filter((l) => l.trim())
|
|
620
|
+
.slice(-400)
|
|
621
|
+
.map((l) => {
|
|
622
|
+
try {
|
|
623
|
+
return JSON.parse(l);
|
|
516
624
|
}
|
|
517
|
-
|
|
518
|
-
|
|
625
|
+
catch {
|
|
626
|
+
return null;
|
|
519
627
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
628
|
+
})
|
|
629
|
+
.filter(Boolean);
|
|
630
|
+
}
|
|
631
|
+
const handoffPath = path.join(cwd, '.rigour/handoffs.jsonl');
|
|
632
|
+
let handoffCount = 0;
|
|
633
|
+
let acceptedHandoffs = 0;
|
|
634
|
+
if (await fs.pathExists(handoffPath)) {
|
|
635
|
+
const content = await fs.readFile(handoffPath, 'utf8');
|
|
636
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
637
|
+
handoffCount = lines.length;
|
|
638
|
+
acceptedHandoffs = events.filter((e) => e.type === 'handoff_accepted').length;
|
|
639
|
+
}
|
|
640
|
+
const typeCount = (types) => events.filter((e) => types.includes(e.type) || types.includes(e.tool)).length;
|
|
641
|
+
const registerCount = Math.max(agentsSession.agents?.length || 0, typeCount(['agent_registered', 'rigour_agent_register']));
|
|
642
|
+
const scopeCount = typeCount(['context_scoped', 'rigour_context_scope', 'scope_resolved']);
|
|
643
|
+
const gateCount = typeCount([
|
|
644
|
+
'gate_failed',
|
|
645
|
+
'gate_passed',
|
|
646
|
+
'hook_blocked',
|
|
647
|
+
'interception_requested',
|
|
648
|
+
'rigour_check',
|
|
649
|
+
]);
|
|
650
|
+
const gateBlocked = typeCount(['gate_failed', 'hook_blocked', 'interception_requested']);
|
|
651
|
+
const checkpointCount = Math.max(mapped.length, typeCount(['checkpoint_recorded', 'rigour_checkpoint']));
|
|
652
|
+
const memoryCount = Object.keys(memory.memories || {}).length;
|
|
653
|
+
const stage = (id, label, count, status, detail) => ({ id, label, count, status, detail });
|
|
654
|
+
const stages = [
|
|
655
|
+
stage('register', 'Register', registerCount, registerCount > 0 ? 'pass' : 'idle', registerCount ? `${registerCount} agent scope(s)` : 'Awaiting rigour_agent_register'),
|
|
656
|
+
stage('scope', 'Context scope', scopeCount, scopeCount > 0 ? 'pass' : registerCount > 0 ? 'warn' : 'idle', scopeCount ? `${scopeCount} scope event(s)` : 'Call rigour_context_scope / recall'),
|
|
657
|
+
stage('gates', 'Gates', gateCount, gateBlocked > 0 ? 'block' : gateCount > 0 ? 'pass' : 'idle', gateBlocked > 0
|
|
658
|
+
? `${gateBlocked} block/intercept event(s)`
|
|
659
|
+
: gateCount
|
|
660
|
+
? 'Gates exercised'
|
|
661
|
+
: 'Hooks & quality gates idle'),
|
|
662
|
+
stage('checkpoint', 'Checkpoint', checkpointCount, checkpointCount > 0 ? 'pass' : 'idle', checkpointCount ? `${checkpointCount} checkpoint(s)` : 'Awaiting rigour_checkpoint'),
|
|
663
|
+
stage('handoff', 'Handoff', handoffCount, handoffCount > 0 ? (acceptedHandoffs > 0 ? 'pass' : 'warn') : 'idle', handoffCount
|
|
664
|
+
? `${acceptedHandoffs}/${handoffCount} accepted`
|
|
665
|
+
: 'Awaiting rigour_handoff'),
|
|
666
|
+
stage('memory', 'Memory', memoryCount, memoryCount > 0 ? 'pass' : 'idle', memoryCount ? `${memoryCount} stable memor(ies)` : 'Awaiting rigour_remember'),
|
|
667
|
+
];
|
|
668
|
+
const timeline = events
|
|
669
|
+
.filter((e) => [
|
|
670
|
+
'agent_registered',
|
|
671
|
+
'checkpoint_recorded',
|
|
672
|
+
'handoff_initiated',
|
|
673
|
+
'handoff_accepted',
|
|
674
|
+
'gate_failed',
|
|
675
|
+
'gate_passed',
|
|
676
|
+
'hook_blocked',
|
|
677
|
+
'interception_requested',
|
|
678
|
+
'memory_stored',
|
|
679
|
+
].includes(e.type))
|
|
680
|
+
.slice(-40)
|
|
681
|
+
.reverse()
|
|
682
|
+
.map((e) => ({
|
|
683
|
+
type: e.type,
|
|
684
|
+
timestamp: e.timestamp || e.ts || null,
|
|
685
|
+
agentId: e.agentId || e.fromAgentId || null,
|
|
686
|
+
summary: e.summary || e.taskDescription || e.tool || e.type,
|
|
687
|
+
}));
|
|
688
|
+
sendJson(res, 200, {
|
|
689
|
+
stages,
|
|
690
|
+
timeline,
|
|
691
|
+
derived: Boolean(agentsSession.derived),
|
|
692
|
+
agentCount: agentsSession.agents?.length || 0,
|
|
693
|
+
sessionStatus: agentsSession.status,
|
|
527
694
|
});
|
|
528
695
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}) + "\n";
|
|
544
|
-
await fs.appendFile(eventsPath, logEntry);
|
|
545
|
-
res.writeHead(200);
|
|
546
|
-
res.end(JSON.stringify({ success: true }));
|
|
696
|
+
catch (e) {
|
|
697
|
+
sendJson(res, 500, { error: e.message });
|
|
698
|
+
}
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
if (url.pathname === '/api/import-cursor-usage' && req.method === 'POST') {
|
|
702
|
+
let body = '';
|
|
703
|
+
req.on('data', (chunk) => (body += chunk));
|
|
704
|
+
req.on('end', async () => {
|
|
705
|
+
try {
|
|
706
|
+
const { importCursorUsageCsv, importCursorUsageJson } = await import('@rigour-labs/core');
|
|
707
|
+
let importedCount = 0;
|
|
708
|
+
if (body.trim().startsWith('{') || body.trim().startsWith('[')) {
|
|
709
|
+
importedCount = await importCursorUsageJson(JSON.parse(body), cwd);
|
|
547
710
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
res.end(e.message);
|
|
711
|
+
else {
|
|
712
|
+
importedCount = await importCursorUsageCsv(body, cwd);
|
|
551
713
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
714
|
+
sendJson(res, 200, { success: true, importedCount });
|
|
715
|
+
}
|
|
716
|
+
catch (e) {
|
|
717
|
+
sendJson(res, 500, { error: e.message });
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
if (url.pathname === '/api/arbitrate' && req.method === 'POST') {
|
|
723
|
+
let body = '';
|
|
724
|
+
req.on('data', (chunk) => (body += chunk));
|
|
725
|
+
req.on('end', async () => {
|
|
726
|
+
try {
|
|
727
|
+
const decision = JSON.parse(body);
|
|
728
|
+
const logEntry = JSON.stringify({
|
|
729
|
+
id: randomUUID(),
|
|
730
|
+
timestamp: new Date().toISOString(),
|
|
731
|
+
tool: 'human_arbitration',
|
|
732
|
+
requestId: decision.requestId,
|
|
733
|
+
decision: decision.decision,
|
|
734
|
+
status: decision.decision === 'approve' ? 'success' : 'error',
|
|
735
|
+
arbitrated: true,
|
|
736
|
+
}) + '\n';
|
|
737
|
+
await fs.appendFile(eventsPath, logEntry);
|
|
738
|
+
sendJson(res, 200, { success: true });
|
|
739
|
+
}
|
|
740
|
+
catch (e) {
|
|
741
|
+
res.writeHead(500);
|
|
742
|
+
res.end(e.message);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
res.writeHead(404);
|
|
748
|
+
res.end();
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
async function serveStaticFile(studioDist, pathname, res) {
|
|
752
|
+
let filePath = path.join(studioDist, pathname === '/' ? 'index.html' : pathname);
|
|
753
|
+
if (!(await fs.pathExists(filePath)) || (await fs.stat(filePath)).isDirectory()) {
|
|
754
|
+
filePath = path.join(studioDist, 'index.html');
|
|
755
|
+
}
|
|
756
|
+
const content = await fs.readFile(filePath);
|
|
757
|
+
const ext = path.extname(filePath);
|
|
758
|
+
const contentTypes = {
|
|
759
|
+
'.html': 'text/html',
|
|
760
|
+
'.js': 'application/javascript',
|
|
761
|
+
'.css': 'text/css',
|
|
762
|
+
'.json': 'application/json',
|
|
763
|
+
'.png': 'image/png',
|
|
764
|
+
'.jpg': 'image/jpeg',
|
|
765
|
+
'.svg': 'image/svg+xml',
|
|
766
|
+
'.ico': 'image/x-icon',
|
|
767
|
+
};
|
|
768
|
+
res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'application/octet-stream' });
|
|
769
|
+
res.end(content);
|
|
770
|
+
}
|
|
771
|
+
function announce(url) {
|
|
562
772
|
setTimeout(async () => {
|
|
563
|
-
const url = `http://localhost:${studioPort}`;
|
|
564
773
|
console.log(chalk.green(`\n✅ Rigour Studio is live at ${chalk.bold(url)}`));
|
|
565
774
|
try {
|
|
566
775
|
await execa('open', [url]);
|
|
567
776
|
}
|
|
568
|
-
catch {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
777
|
+
catch {
|
|
778
|
+
// non-mac or open unavailable
|
|
779
|
+
}
|
|
780
|
+
}, 800);
|
|
573
781
|
}
|
|
782
|
+
export const studioCommand = new Command('studio')
|
|
783
|
+
.description('Launch Rigour Studio (Local-First Governance UI)')
|
|
784
|
+
.option('-p, --port <number>', 'Port to run the studio on', '3000')
|
|
785
|
+
.option('--dev', 'Opt-in: run Vite against monorepo studio source (developers only)', false)
|
|
786
|
+
.action(async (options) => {
|
|
787
|
+
const cwd = process.cwd();
|
|
788
|
+
const studioPort = String(options.port);
|
|
789
|
+
const apiPort = parseInt(studioPort, 10) + 1;
|
|
790
|
+
const eventsPath = path.join(cwd, '.rigour/events.jsonl');
|
|
791
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
792
|
+
const candidates = [
|
|
793
|
+
path.join(__dirname, '../studio-dist'),
|
|
794
|
+
path.join(__dirname, '../../studio-dist'),
|
|
795
|
+
path.join(__dirname, '../../../studio-dist'),
|
|
796
|
+
];
|
|
797
|
+
const localStudioDist = candidates.find((p) => fs.pathExistsSync(p)) ?? candidates[0];
|
|
798
|
+
const workspaceRoot = path.join(__dirname, '../../../../');
|
|
799
|
+
const allowedOrigins = new Set([
|
|
800
|
+
`http://localhost:${studioPort}`,
|
|
801
|
+
`http://127.0.0.1:${studioPort}`,
|
|
802
|
+
]);
|
|
803
|
+
const ctx = { cwd, eventsPath, allowedOrigins };
|
|
804
|
+
console.log(chalk.bold.cyan('\n🛡️ Launching Rigour Studio...'));
|
|
805
|
+
console.log(chalk.gray(`Project Root: ${cwd}`));
|
|
806
|
+
const configPath = path.join(cwd, 'rigour.yml');
|
|
807
|
+
if (!(await fs.pathExists(configPath))) {
|
|
808
|
+
console.log(chalk.yellow('\n⚠️ Warning: rigour.yml not found.'));
|
|
809
|
+
console.log(chalk.dim('The Studio will be empty until you initialize the project.'));
|
|
810
|
+
console.log(chalk.cyan('Suggest: ') + chalk.bold('npx @rigour-labs/cli init') + '\n');
|
|
811
|
+
}
|
|
812
|
+
console.log(chalk.gray(`Shadowing interactions in ${eventsPath}\n`));
|
|
813
|
+
const isMonorepo = await fs.pathExists(path.join(workspaceRoot, 'packages/rigour-studio'));
|
|
814
|
+
if (isMonorepo && options.dev) {
|
|
815
|
+
console.log(chalk.yellow('Monorepo detected: Launching Studio in Development Mode...'));
|
|
816
|
+
console.log(chalk.gray(`Vite :${studioPort} → API :${apiPort} (same-origin via proxy)`));
|
|
817
|
+
try {
|
|
818
|
+
const studioProcess = execa('pnpm', ['--filter', '@rigour-labs/studio', 'dev', '--port', studioPort], {
|
|
819
|
+
stdio: 'inherit',
|
|
820
|
+
cwd: workspaceRoot,
|
|
821
|
+
env: {
|
|
822
|
+
...process.env,
|
|
823
|
+
RIGOUR_API_PORT: String(apiPort),
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
const apiServer = http.createServer(async (req, res) => {
|
|
827
|
+
const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
|
|
828
|
+
const handled = await handleApiRequest(req, res, url, ctx);
|
|
829
|
+
if (!handled) {
|
|
830
|
+
res.writeHead(404);
|
|
831
|
+
res.end();
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
apiServer.listen(apiPort, '127.0.0.1', () => {
|
|
835
|
+
console.log(chalk.gray(`API Streamer active on 127.0.0.1:${apiPort}`));
|
|
836
|
+
});
|
|
837
|
+
announce(`http://127.0.0.1:${studioPort}`);
|
|
838
|
+
await studioProcess;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
console.log(chalk.dim('Development mode failed, falling back to standalone...'));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
console.log(chalk.green('Launching Studio in Standalone Mode (same-origin API)...'));
|
|
846
|
+
if (!(await fs.pathExists(localStudioDist))) {
|
|
847
|
+
console.error(chalk.red(`\n❌ Error: Studio UI artifacts not found at ${localStudioDist}`));
|
|
848
|
+
console.log(chalk.yellow('If you are a developer, run "pnpm build" in the monorepo root first.\n'));
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
// Critical UX fix: serve UI + /api on ONE port so fetch('/api/...') works.
|
|
852
|
+
// Bind loopback only — Studio can accept optional vendor secrets locally.
|
|
853
|
+
const server = http.createServer(async (req, res) => {
|
|
854
|
+
const url = new URL(req.url || '', `http://${req.headers.host || '127.0.0.1'}`);
|
|
855
|
+
try {
|
|
856
|
+
if (await handleApiRequest(req, res, url, ctx))
|
|
857
|
+
return;
|
|
858
|
+
await serveStaticFile(localStudioDist, url.pathname, res);
|
|
859
|
+
}
|
|
860
|
+
catch (e) {
|
|
861
|
+
res.writeHead(500);
|
|
862
|
+
res.end(e.message || 'Internal error');
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
server.listen(parseInt(studioPort, 10), '127.0.0.1', () => {
|
|
866
|
+
console.log(chalk.gray(`Studio + API on 127.0.0.1:${studioPort}`));
|
|
867
|
+
announce(`http://127.0.0.1:${studioPort}`);
|
|
868
|
+
});
|
|
869
|
+
});
|