@rigour-labs/cli 5.3.3 → 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 +793 -467
- 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-C-kzKQ6s.js +0 -322
- package/studio-dist/assets/index-hcvdpk3Y.css +0 -1
package/dist/commands/studio.js
CHANGED
|
@@ -1,543 +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`);
|
|
67
133
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|
|
148
|
+
}
|
|
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
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
staticServer.listen(options.port, () => {
|
|
89
|
-
setupApiAndLaunch(apiPort, options.port, eventsPath, cwd);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
async function setupApiAndLaunch(apiPort, studioPort, eventsPath, cwd, studioProcess) {
|
|
93
|
-
const allowedOrigins = new Set([
|
|
94
|
-
`http://localhost:${studioPort}`,
|
|
95
|
-
`http://127.0.0.1:${studioPort}`,
|
|
96
|
-
]);
|
|
97
|
-
const apiServer = http.createServer(async (req, res) => {
|
|
98
|
-
const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
|
|
99
|
-
const requestOrigin = req.headers.origin;
|
|
100
|
-
if (typeof requestOrigin === 'string' && allowedOrigins.has(requestOrigin)) {
|
|
101
|
-
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
|
102
|
-
res.setHeader('Vary', 'Origin');
|
|
103
|
-
}
|
|
104
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS, POST');
|
|
105
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
106
|
-
if (req.method === 'OPTIONS') {
|
|
107
|
-
res.writeHead(204);
|
|
108
|
-
res.end();
|
|
109
|
-
return;
|
|
173
|
+
res.end('Not found');
|
|
110
174
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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);
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
sendJson(res, 500, { error: e.message });
|
|
307
|
+
}
|
|
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));
|
|
297
328
|
}
|
|
298
329
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
330
|
+
catch (e) {
|
|
331
|
+
sendJson(res, 500, { error: e.message });
|
|
332
|
+
}
|
|
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
|
+
});
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
sendJson(res, 500, { error: e.message });
|
|
369
|
+
}
|
|
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 || {});
|
|
310
378
|
}
|
|
311
|
-
|
|
312
|
-
res
|
|
313
|
-
res.end(e.message);
|
|
379
|
+
else {
|
|
380
|
+
sendJson(res, 200, {});
|
|
314
381
|
}
|
|
315
382
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
383
|
+
catch (e) {
|
|
384
|
+
sendJson(res, 500, { error: e.message });
|
|
385
|
+
}
|
|
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);
|
|
328
395
|
}
|
|
329
|
-
|
|
330
|
-
res
|
|
331
|
-
res.end(e.message);
|
|
396
|
+
else {
|
|
397
|
+
sendJson(res, 200, []);
|
|
332
398
|
}
|
|
333
399
|
}
|
|
334
|
-
|
|
400
|
+
catch (e) {
|
|
401
|
+
sendJson(res, 500, { error: e.message });
|
|
402
|
+
}
|
|
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 });
|
|
410
|
+
}
|
|
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 });
|
|
467
|
+
}
|
|
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 () => {
|
|
335
531
|
try {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
res.end(JSON.stringify(findings));
|
|
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;
|
|
342
537
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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);
|
|
346
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
|
+
});
|
|
347
557
|
}
|
|
348
558
|
catch (e) {
|
|
349
|
-
res.
|
|
350
|
-
res.end(e.message);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
else if (url.pathname === '/api/drift') {
|
|
354
|
-
try {
|
|
355
|
-
const { generateTemporalDriftReport } = await import('@rigour-labs/core');
|
|
356
|
-
const report = generateTemporalDriftReport(cwd);
|
|
357
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
358
|
-
res.end(JSON.stringify(report || { totalScans: 0 }));
|
|
359
|
-
}
|
|
360
|
-
catch (e) {
|
|
361
|
-
// SQLite not available or no data
|
|
362
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
363
|
-
res.end(JSON.stringify({ totalScans: 0 }));
|
|
559
|
+
sendJson(res, 500, { error: e.message });
|
|
364
560
|
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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())) {
|
|
571
|
+
try {
|
|
572
|
+
handoffs.push(JSON.parse(line));
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// skip bad lines
|
|
576
|
+
}
|
|
577
|
+
}
|
|
373
578
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
}
|
|
595
|
+
}
|
|
377
596
|
}
|
|
597
|
+
sendJson(res, 200, {
|
|
598
|
+
handoffs: handoffs.slice(-100).reverse(),
|
|
599
|
+
count: handoffs.length,
|
|
600
|
+
});
|
|
378
601
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const { getTaskCostStats } = await import('@rigour-labs/core');
|
|
382
|
-
const taskId = url.searchParams.get('taskId') || undefined;
|
|
383
|
-
const costStats = await getTaskCostStats(taskId, cwd);
|
|
384
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
385
|
-
res.end(JSON.stringify(costStats));
|
|
386
|
-
}
|
|
387
|
-
catch (e) {
|
|
388
|
-
res.writeHead(500);
|
|
389
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
390
|
-
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
sendJson(res, 500, { error: e.message });
|
|
391
604
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
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,
|
|
694
|
+
});
|
|
403
695
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const { explainContext } = await import('@rigour-labs/core');
|
|
407
|
-
const target = url.searchParams.get('target') || 'all';
|
|
408
|
-
const taskId = url.searchParams.get('taskId') || undefined;
|
|
409
|
-
const explanation = await explainContext(target, taskId, cwd);
|
|
410
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
411
|
-
res.end(JSON.stringify(explanation));
|
|
412
|
-
}
|
|
413
|
-
catch (e) {
|
|
414
|
-
res.writeHead(500);
|
|
415
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
416
|
-
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
sendJson(res, 500, { error: e.message });
|
|
417
698
|
}
|
|
418
|
-
|
|
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 () => {
|
|
419
705
|
try {
|
|
420
|
-
const {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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);
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
importedCount = await importCursorUsageCsv(body, cwd);
|
|
713
|
+
}
|
|
714
|
+
sendJson(res, 200, { success: true, importedCount });
|
|
424
715
|
}
|
|
425
716
|
catch (e) {
|
|
426
|
-
res.
|
|
427
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
717
|
+
sendJson(res, 500, { error: e.message });
|
|
428
718
|
}
|
|
429
|
-
}
|
|
430
|
-
|
|
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 () => {
|
|
431
726
|
try {
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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 });
|
|
437
739
|
}
|
|
438
740
|
catch (e) {
|
|
439
741
|
res.writeHead(500);
|
|
440
|
-
res.end(
|
|
742
|
+
res.end(e.message);
|
|
441
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) {
|
|
772
|
+
setTimeout(async () => {
|
|
773
|
+
console.log(chalk.green(`\n✅ Rigour Studio is live at ${chalk.bold(url)}`));
|
|
774
|
+
try {
|
|
775
|
+
await execa('open', [url]);
|
|
442
776
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const { getCursorApiKey } = await import('@rigour-labs/core');
|
|
446
|
-
const configured = Boolean(getCursorApiKey());
|
|
447
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
448
|
-
res.end(JSON.stringify({ configured }));
|
|
449
|
-
}
|
|
450
|
-
catch (e) {
|
|
451
|
-
res.writeHead(500);
|
|
452
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
453
|
-
}
|
|
777
|
+
catch {
|
|
778
|
+
// non-mac or open unavailable
|
|
454
779
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
780
|
+
}, 800);
|
|
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
|
+
},
|
|
475
825
|
});
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const { importCursorUsageCsv, importCursorUsageJson } = await import('@rigour-labs/core');
|
|
483
|
-
let importedCount = 0;
|
|
484
|
-
if (body.trim().startsWith('{') || body.trim().startsWith('[')) {
|
|
485
|
-
importedCount = await importCursorUsageJson(JSON.parse(body), cwd);
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
importedCount = await importCursorUsageCsv(body, cwd);
|
|
489
|
-
}
|
|
490
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
491
|
-
res.end(JSON.stringify({ success: true, importedCount }));
|
|
492
|
-
}
|
|
493
|
-
catch (e) {
|
|
494
|
-
res.writeHead(500);
|
|
495
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
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();
|
|
496
832
|
}
|
|
497
833
|
});
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
let body = '';
|
|
501
|
-
req.on('data', chunk => body += chunk);
|
|
502
|
-
req.on('end', async () => {
|
|
503
|
-
try {
|
|
504
|
-
const decision = JSON.parse(body);
|
|
505
|
-
const logEntry = JSON.stringify({
|
|
506
|
-
id: randomUUID(),
|
|
507
|
-
timestamp: new Date().toISOString(),
|
|
508
|
-
tool: 'human_arbitration',
|
|
509
|
-
requestId: decision.requestId,
|
|
510
|
-
decision: decision.decision,
|
|
511
|
-
status: decision.decision === 'approve' ? 'success' : 'error',
|
|
512
|
-
arbitrated: true
|
|
513
|
-
}) + "\n";
|
|
514
|
-
await fs.appendFile(eventsPath, logEntry);
|
|
515
|
-
res.writeHead(200);
|
|
516
|
-
res.end(JSON.stringify({ success: true }));
|
|
517
|
-
}
|
|
518
|
-
catch (e) {
|
|
519
|
-
res.writeHead(500);
|
|
520
|
-
res.end(e.message);
|
|
521
|
-
}
|
|
834
|
+
apiServer.listen(apiPort, '127.0.0.1', () => {
|
|
835
|
+
console.log(chalk.gray(`API Streamer active on 127.0.0.1:${apiPort}`));
|
|
522
836
|
});
|
|
837
|
+
announce(`http://127.0.0.1:${studioPort}`);
|
|
838
|
+
await studioProcess;
|
|
839
|
+
return;
|
|
523
840
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
res.end();
|
|
841
|
+
catch {
|
|
842
|
+
console.log(chalk.dim('Development mode failed, falling back to standalone...'));
|
|
527
843
|
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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'}`);
|
|
535
855
|
try {
|
|
536
|
-
await
|
|
856
|
+
if (await handleApiRequest(req, res, url, ctx))
|
|
857
|
+
return;
|
|
858
|
+
await serveStaticFile(localStudioDist, url.pathname, res);
|
|
537
859
|
}
|
|
538
|
-
catch {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
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
|
+
});
|