@rigour-labs/mcp 6.0.0 → 6.1.0-beta.1
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/index.js +78 -4
- package/dist/tools/agent-handlers.js +4 -11
- package/dist/tools/context-handlers.js +66 -19
- package/dist/tools/hooks-handler.js +4 -1
- package/dist/tools/index-handlers.js +1 -1
- package/dist/tools/memory-handlers.js +24 -4
- package/dist/tools/pattern-handlers.js +24 -1
- package/dist/utils/context-telemetry.d.ts +29 -0
- package/dist/utils/context-telemetry.js +4 -2
- package/dist/utils/impact-receipt.d.ts +13 -0
- package/dist/utils/impact-receipt.js +43 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { GateRunner } from "@rigour-labs/core";
|
|
|
16
16
|
import { loadConfig, loadMcpSettings, logStudioEvent } from './utils/config.js';
|
|
17
17
|
import { bindServer } from './utils/notifications.js';
|
|
18
18
|
import { getMcpVersion } from './utils/package-version.js';
|
|
19
|
+
import { buildMcpResultMeta, buildStudioImpact } from './utils/impact-receipt.js';
|
|
19
20
|
// Dashboard (MCP App)
|
|
20
21
|
import { DASHBOARD_URI, getDashboardHtml, pushTimelineEntry, updateScore } from './dashboard/index.js';
|
|
21
22
|
// Tool definitions & advertised registry
|
|
@@ -32,9 +33,25 @@ import { handleCheckDeep, handleDeepStats } from './tools/deep-handlers.js';
|
|
|
32
33
|
import { handleMcpGetSettings, handleMcpSetSettings } from './tools/mcp-settings-handler.js';
|
|
33
34
|
import { handleContextStats, handleTaskCost, handleCacheStats, handleContextExplain, handleContextScope } from './tools/context-handlers.js';
|
|
34
35
|
import { handleIndex } from './tools/index-handlers.js';
|
|
35
|
-
import { recordContextEvent, estimateTokenCount } from '@rigour-labs/core';
|
|
36
|
+
import { recordContextEvent, recordInteractionEvidence, recordInteractionLesson, syncTeamOutbox, estimateTokenCount } from '@rigour-labs/core';
|
|
36
37
|
// ─── Server Setup ─────────────────────────────────────────────────
|
|
37
38
|
const server = new Server({ name: "rigour-mcp", version: getMcpVersion('5.5.0') }, { capabilities: { tools: {}, prompts: {}, logging: {}, resources: {} } });
|
|
39
|
+
function interactionMetadata(args) {
|
|
40
|
+
const input = (args && typeof args === 'object' ? args : {});
|
|
41
|
+
const rawFiles = Array.isArray(input.files)
|
|
42
|
+
? input.files
|
|
43
|
+
: Array.isArray(input.filesChanged)
|
|
44
|
+
? input.filesChanged
|
|
45
|
+
: Array.isArray(input.filesInScope)
|
|
46
|
+
? input.filesInScope
|
|
47
|
+
: input.file ? [input.file] : [];
|
|
48
|
+
return {
|
|
49
|
+
taskId: String(input.taskId || input.task_id || '') || undefined,
|
|
50
|
+
agentId: String(input.agentId || input.agent_id || input.agent || '') || undefined,
|
|
51
|
+
sessionId: String(input.sessionId || input.session_id || '') || undefined,
|
|
52
|
+
files: rawFiles.filter((file) => typeof file === 'string').slice(0, 100),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
38
55
|
// Bind server for logging notifications from handlers
|
|
39
56
|
bindServer(server);
|
|
40
57
|
// ─── Tool Listing ─────────────────────────────────────────────────
|
|
@@ -69,6 +86,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
69
86
|
const requestId = randomUUID();
|
|
70
87
|
try {
|
|
71
88
|
await logStudioEvent(cwd, { type: "tool_call", requestId, tool: name, arguments: args });
|
|
89
|
+
try {
|
|
90
|
+
await recordInteractionEvidence(cwd, {
|
|
91
|
+
tool: name,
|
|
92
|
+
outcome: 'success',
|
|
93
|
+
phase: 'request',
|
|
94
|
+
requestId,
|
|
95
|
+
...interactionMetadata(args),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Evidence capture must never block the requested governance operation.
|
|
100
|
+
}
|
|
72
101
|
// ── Image DLP warning ──────────────────────────────
|
|
73
102
|
// MCP args may contain base64 image data. Text DLP cannot scan images.
|
|
74
103
|
const argsStr = JSON.stringify(args ?? {});
|
|
@@ -233,12 +262,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
233
262
|
candidateTokens: telemetry?.candidateTokens ?? returnedTokens,
|
|
234
263
|
returnedTokens,
|
|
235
264
|
deduplicatedTokens: telemetry?.deduplicatedTokens ?? 0,
|
|
265
|
+
candidateFiles: telemetry?.candidateFiles,
|
|
266
|
+
returnedFiles: telemetry?.returnedFiles,
|
|
236
267
|
}, cwd);
|
|
237
268
|
}
|
|
238
269
|
}
|
|
239
270
|
catch {
|
|
240
271
|
// non-blocking context telemetry logging
|
|
241
272
|
}
|
|
273
|
+
try {
|
|
274
|
+
const reportStatus = result?._rigour_report?.status;
|
|
275
|
+
const deterministic = Boolean(reportStatus === 'PASS');
|
|
276
|
+
const outcome = result?.isError ? 'error' : 'success';
|
|
277
|
+
await recordInteractionEvidence(cwd, {
|
|
278
|
+
tool: name,
|
|
279
|
+
outcome,
|
|
280
|
+
phase: 'response',
|
|
281
|
+
requestId,
|
|
282
|
+
deterministic,
|
|
283
|
+
summary: result?._guidance?.recommendation,
|
|
284
|
+
...interactionMetadata(args),
|
|
285
|
+
});
|
|
286
|
+
await recordInteractionLesson(cwd, {
|
|
287
|
+
tool: name,
|
|
288
|
+
outcome,
|
|
289
|
+
requestId,
|
|
290
|
+
deterministic,
|
|
291
|
+
verifiedOutcome: deterministic,
|
|
292
|
+
summary: result?._guidance?.recommendation,
|
|
293
|
+
...interactionMetadata(args),
|
|
294
|
+
});
|
|
295
|
+
void syncTeamOutbox().catch(() => undefined);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Learning is evidence collection and must not block governance tools.
|
|
299
|
+
}
|
|
242
300
|
// ── Prepend image DLP warning if image content was detected ──
|
|
243
301
|
if (hasImageContent && Array.isArray(result.content)) {
|
|
244
302
|
result.content.unshift({
|
|
@@ -246,9 +304,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
246
304
|
text: "⚠ DLP Notice: Image content detected in this request. Text-based credential scanning cannot analyze images. Avoid sharing screenshots containing API keys, tokens, or passwords.",
|
|
247
305
|
});
|
|
248
306
|
}
|
|
307
|
+
const studioImpact = buildStudioImpact(result._guidance, result._telemetry);
|
|
249
308
|
await logStudioEvent(cwd, {
|
|
250
|
-
type: "tool_response", requestId, tool: name, status: "success",
|
|
251
|
-
content: result.content,
|
|
309
|
+
type: "tool_response", requestId, tool: name, status: result.isError ? "error" : "success",
|
|
310
|
+
content: result.content,
|
|
311
|
+
_rigour_report: result._rigour_report,
|
|
312
|
+
...studioImpact,
|
|
252
313
|
});
|
|
253
314
|
// ─── Dashboard State + MCP App UI Hint ──────────────
|
|
254
315
|
const report = result._rigour_report;
|
|
@@ -261,7 +322,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
261
322
|
}
|
|
262
323
|
// Attach UI hint for MCP-App-capable clients (Claude Desktop, VS Code, ChatGPT, Goose)
|
|
263
324
|
if (!result.isError) {
|
|
264
|
-
result._meta =
|
|
325
|
+
result._meta = buildMcpResultMeta(result._meta, requestId, DASHBOARD_URI, result._guidance, result._telemetry);
|
|
265
326
|
}
|
|
266
327
|
// Notify clients that the dashboard resource has updated
|
|
267
328
|
try {
|
|
@@ -280,6 +341,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
280
341
|
content: [{ type: "text", text: `RIGOUR ERROR: ${error.message}` }],
|
|
281
342
|
isError: true,
|
|
282
343
|
};
|
|
344
|
+
try {
|
|
345
|
+
await recordInteractionEvidence(cwd, {
|
|
346
|
+
tool: name,
|
|
347
|
+
outcome: 'error',
|
|
348
|
+
phase: 'response',
|
|
349
|
+
requestId,
|
|
350
|
+
summary: String(error?.message || 'Unknown error').slice(0, 500),
|
|
351
|
+
...interactionMetadata(args),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// Failure evidence capture is intentionally non-blocking.
|
|
356
|
+
}
|
|
283
357
|
await logStudioEvent(cwd, {
|
|
284
358
|
type: "tool_response", requestId, tool: name,
|
|
285
359
|
status: "error", error: error.message, content: errorResponse.content,
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import fs from "fs-extra";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { logStudioEvent } from '../utils/config.js';
|
|
12
|
-
import { setTaskCheckpointCache, getTaskCheckpointCache, estimateTokenCount, } from '@rigour-labs/core';
|
|
13
|
-
import {
|
|
12
|
+
import { setTaskCheckpointCache, getTaskCheckpointCache, estimateTokenCount, updateAutomaticIndexForFiles, } from '@rigour-labs/core';
|
|
13
|
+
import { loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
|
|
14
14
|
import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
|
|
15
15
|
import { appendContextFooter } from '../utils/context-footer.js';
|
|
16
16
|
const HANDOFF_CONTEXT_TOKEN_LIMIT = 2000;
|
|
@@ -38,16 +38,9 @@ function globMatchesFile(filePath, glob) {
|
|
|
38
38
|
async function refreshIndexForFiles(cwd, filesChanged) {
|
|
39
39
|
if (filesChanged.length === 0)
|
|
40
40
|
return null;
|
|
41
|
-
const indexPath = getDefaultIndexPath(cwd);
|
|
42
|
-
const existingIndex = await loadPatternIndex(indexPath);
|
|
43
|
-
if (!existingIndex) {
|
|
44
|
-
return 'Index not found — run rigour_index to enable incremental updates.';
|
|
45
|
-
}
|
|
46
41
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
await savePatternIndex(updated, indexPath);
|
|
50
|
-
return `Index refreshed for ${filesChanged.length} changed file(s) — ${updated.stats.totalPatterns} patterns total.`;
|
|
42
|
+
const { index, affectedFiles } = await updateAutomaticIndexForFiles(cwd, filesChanged);
|
|
43
|
+
return `Index refreshed ${filesChanged.length} changed and ${affectedFiles.length} affected file(s) — ${index.stats.totalPatterns} patterns total.`;
|
|
51
44
|
}
|
|
52
45
|
catch (error) {
|
|
53
46
|
return `Index refresh failed: ${error.message}`;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import fs from 'fs-extra';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
|
-
import { getTaskContextStats, getTaskCostStats, getCacheStats, explainContext, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, filterExistingEditScope, estimateTokenCount, } from '@rigour-labs/core';
|
|
14
|
+
import { getTaskContextStats, getTaskCostStats, getCacheStats, explainContext, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, filterExistingEditScope, estimateTokenCount, ensureAutomaticIndex, getApplicableLessons, searchTeamKnowledge, } from '@rigour-labs/core';
|
|
15
15
|
import { loadPatternIndex, getDefaultIndexPath, generateEmbedding, semanticSearch, } from '@rigour-labs/core/pattern-index';
|
|
16
16
|
import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
|
|
17
17
|
import { appendContextFooter } from '../utils/context-footer.js';
|
|
@@ -107,6 +107,10 @@ function scopeTelemetryMeta(opts) {
|
|
|
107
107
|
taskId: opts.taskId,
|
|
108
108
|
agentId: opts.agentId,
|
|
109
109
|
queryHash: hashScopeQuery(opts.query),
|
|
110
|
+
candidateTokens: opts.candidateTokens,
|
|
111
|
+
returnedTokens: opts.returnedTokens,
|
|
112
|
+
candidateFiles: opts.candidateFiles,
|
|
113
|
+
returnedFiles: opts.returnedFiles,
|
|
110
114
|
});
|
|
111
115
|
}
|
|
112
116
|
function textMatchPatterns(query, patterns, limit) {
|
|
@@ -155,6 +159,7 @@ async function serveCachedScope(cached, opts) {
|
|
|
155
159
|
cacheNote: opts.note,
|
|
156
160
|
};
|
|
157
161
|
const cachedText = JSON.stringify(payload, null, 2);
|
|
162
|
+
const returnedTokens = estimateTokenCount(cachedText);
|
|
158
163
|
const telemetry = scopeTelemetryMeta({
|
|
159
164
|
candidateText: opts.fullIndexCandidate,
|
|
160
165
|
returnedText: cachedText,
|
|
@@ -162,6 +167,11 @@ async function serveCachedScope(cached, opts) {
|
|
|
162
167
|
query: opts.query,
|
|
163
168
|
taskId: opts.taskId,
|
|
164
169
|
agentId: opts.agentId,
|
|
170
|
+
candidateTokens: cached.sourceTokens,
|
|
171
|
+
returnedTokens,
|
|
172
|
+
candidateFiles: cached.candidateFiles,
|
|
173
|
+
returnedFiles: valid.length,
|
|
174
|
+
deduplicatedTokens: Math.max(0, (cached.sourceTokens ?? returnedTokens) - returnedTokens),
|
|
165
175
|
});
|
|
166
176
|
const label = opts.cacheStatus === 'partial-hit'
|
|
167
177
|
? 'CONTEXT SCOPE (partial cache)'
|
|
@@ -170,6 +180,15 @@ async function serveCachedScope(cached, opts) {
|
|
|
170
180
|
return {
|
|
171
181
|
content: [{ type: 'text', text: appendContextFooter(text, telemetry, 'rigour_check_pattern before creating code') }],
|
|
172
182
|
_telemetry: telemetry,
|
|
183
|
+
_guidance: {
|
|
184
|
+
...(cached.guidance ?? {
|
|
185
|
+
kind: 'context-scope',
|
|
186
|
+
recommendation: `Reuse the cached scope of ${valid.length} file(s).`,
|
|
187
|
+
}),
|
|
188
|
+
query: opts.query,
|
|
189
|
+
selectedFiles: valid,
|
|
190
|
+
excludedFileCount: Math.max(0, (cached.candidateFiles ?? valid.length) - valid.length),
|
|
191
|
+
},
|
|
173
192
|
};
|
|
174
193
|
}
|
|
175
194
|
export async function handleContextScope(cwd, query, limit = 10, taskId, agentId) {
|
|
@@ -205,22 +224,9 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
205
224
|
if (served)
|
|
206
225
|
return served;
|
|
207
226
|
}
|
|
208
|
-
const index = await loadPatternIndex(indexPath)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
+ 'Without an index, agents tend to read entire directories — wasting tokens.';
|
|
212
|
-
return {
|
|
213
|
-
content: [{ type: 'text', text }],
|
|
214
|
-
_telemetry: scopeTelemetryMeta({
|
|
215
|
-
candidateText: fullIndexCandidate,
|
|
216
|
-
returnedText: text,
|
|
217
|
-
cacheStatus: 'miss',
|
|
218
|
-
query,
|
|
219
|
-
taskId,
|
|
220
|
-
agentId,
|
|
221
|
-
}),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
227
|
+
const index = await loadPatternIndex(indexPath) ?? await ensureAutomaticIndex(cwd, {
|
|
228
|
+
backgroundSemantic: process.env.NODE_ENV !== 'test',
|
|
229
|
+
});
|
|
224
230
|
let matched = [];
|
|
225
231
|
const hasEmbeddings = index.patterns.some(p => p.embedding && p.embedding.length > 0);
|
|
226
232
|
if (hasEmbeddings) {
|
|
@@ -252,6 +258,10 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
252
258
|
.slice(0, Math.max(0, 20 - editScope.length))
|
|
253
259
|
.map(f => ({ file: f, reason: 'Outside semantic match for query' }));
|
|
254
260
|
const signatures = matched.map(p => `- ${p.file}:${p.line} ${p.type} ${p.name} — ${p.signature || p.description || '(no signature)'}`);
|
|
261
|
+
const [applicableLearning, semanticKnowledge] = await Promise.all([
|
|
262
|
+
getApplicableLessons(cwd, agentId, process.env.RIGOUR_TEAM_ID),
|
|
263
|
+
searchTeamKnowledge(query, Math.min(limit, 8)),
|
|
264
|
+
]);
|
|
255
265
|
let fullFileTokens = 0;
|
|
256
266
|
for (const file of editScope) {
|
|
257
267
|
try {
|
|
@@ -262,22 +272,51 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
262
272
|
fullFileTokens += 500;
|
|
263
273
|
}
|
|
264
274
|
}
|
|
275
|
+
const guidance = {
|
|
276
|
+
kind: 'context-scope',
|
|
277
|
+
query,
|
|
278
|
+
recommendation: editScope.length > 0
|
|
279
|
+
? `Read only the ${editScope.length} evidence-backed file(s) in this scope.`
|
|
280
|
+
: 'Refine the task query before reading broadly.',
|
|
281
|
+
patternRefs: matched.map(pattern => ({ id: pattern.id, label: pattern.name, file: pattern.file })),
|
|
282
|
+
lessonRefs: applicableLearning.lessons.map(lesson => ({ id: lesson.id, label: lesson.subject, visibility: lesson.visibility })),
|
|
283
|
+
selectedFiles: editScope,
|
|
284
|
+
excludedFileCount: Math.max(0, index.stats.totalFiles - editScope.length),
|
|
285
|
+
conflicts: applicableLearning.conflicts.length,
|
|
286
|
+
};
|
|
265
287
|
const scopePayload = {
|
|
266
288
|
query,
|
|
267
289
|
editScope,
|
|
268
290
|
skipScope,
|
|
269
291
|
signatures,
|
|
292
|
+
learningGuidance: applicableLearning.lessons.map(lesson => ({
|
|
293
|
+
subject: lesson.subject,
|
|
294
|
+
scope: lesson.repositoryId,
|
|
295
|
+
visibility: lesson.visibility,
|
|
296
|
+
confidence: lesson.confidence,
|
|
297
|
+
provenance: lesson.provenance,
|
|
298
|
+
})),
|
|
299
|
+
learningConflicts: applicableLearning.conflicts,
|
|
300
|
+
semanticKnowledge: {
|
|
301
|
+
...semanticKnowledge,
|
|
302
|
+
purpose: 'Advisory cross-repository recall only; similarity never promotes or enforces a lesson.',
|
|
303
|
+
},
|
|
270
304
|
indexHealth: {
|
|
271
305
|
totalPatterns: index.stats.totalPatterns,
|
|
272
306
|
totalFiles: index.stats.totalFiles,
|
|
273
307
|
lastUpdated: index.lastUpdated,
|
|
274
308
|
semanticEnabled: hasEmbeddings,
|
|
275
309
|
},
|
|
276
|
-
estimatedTokensSaved:
|
|
310
|
+
estimatedTokensSaved: 0,
|
|
277
311
|
recommendation: editScope.length > 0
|
|
278
312
|
? `Read ONLY these ${editScope.length} file(s). Do NOT scan the whole repo.`
|
|
279
313
|
: 'No strong matches — refine query or run rigour_index --semantic',
|
|
280
314
|
};
|
|
315
|
+
// Compare compact guidance with the raw contents of the selected files only.
|
|
316
|
+
// Repository-wide files excluded by structural matching are reported separately.
|
|
317
|
+
let resultText = JSON.stringify(scopePayload, null, 2);
|
|
318
|
+
scopePayload.estimatedTokensSaved = Math.max(0, fullFileTokens - estimateTokenCount(resultText));
|
|
319
|
+
resultText = JSON.stringify(scopePayload, null, 2);
|
|
281
320
|
await setSemanticQueryCache(query, commitSha, {
|
|
282
321
|
query,
|
|
283
322
|
resolvedOwner: editScope[0] ? path.dirname(editScope[0]) : 'unknown',
|
|
@@ -286,8 +325,11 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
286
325
|
evidence: signatures.slice(0, 5),
|
|
287
326
|
commitSha,
|
|
288
327
|
confidence: matched.length > 0 ? 0.85 : 0.3,
|
|
328
|
+
sourceTokens: fullFileTokens,
|
|
329
|
+
candidateFiles: index.stats.totalFiles,
|
|
330
|
+
returnedFiles: editScope.length,
|
|
331
|
+
guidance,
|
|
289
332
|
}, cwd);
|
|
290
|
-
const resultText = JSON.stringify(scopePayload, null, 2);
|
|
291
333
|
const telemetry = scopeTelemetryMeta({
|
|
292
334
|
candidateText: fullIndexCandidate,
|
|
293
335
|
returnedText: resultText,
|
|
@@ -296,6 +338,10 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
296
338
|
query,
|
|
297
339
|
taskId,
|
|
298
340
|
agentId,
|
|
341
|
+
candidateTokens: fullFileTokens,
|
|
342
|
+
returnedTokens: estimateTokenCount(resultText),
|
|
343
|
+
candidateFiles: index.stats.totalFiles,
|
|
344
|
+
returnedFiles: editScope.length,
|
|
299
345
|
});
|
|
300
346
|
return {
|
|
301
347
|
content: [{
|
|
@@ -303,5 +349,6 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
|
|
|
303
349
|
text: appendContextFooter(`CONTEXT SCOPE\n\n${resultText}`, telemetry, 'rigour_check_pattern before creating code'),
|
|
304
350
|
}],
|
|
305
351
|
_telemetry: telemetry,
|
|
352
|
+
_guidance: guidance,
|
|
306
353
|
};
|
|
307
354
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @since v3.0.0 — real-time hooks for AI coding tools
|
|
8
8
|
* @since v4.2.0 — AI Agent DLP warning layer
|
|
9
9
|
*/
|
|
10
|
-
import { runHookChecker, generateHookFiles } from "@rigour-labs/core";
|
|
10
|
+
import { runHookChecker, generateHookFiles, updateAutomaticIndexForFiles } from "@rigour-labs/core";
|
|
11
11
|
import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, generateDLPHookFiles } from "@rigour-labs/core";
|
|
12
12
|
import fs from "fs-extra";
|
|
13
13
|
import path from "path";
|
|
@@ -76,6 +76,9 @@ export async function handleHooksCheck(cwd, files, timeout, text, agent) {
|
|
|
76
76
|
if (agent)
|
|
77
77
|
input.agentId = agent;
|
|
78
78
|
const result = await runHookChecker(input);
|
|
79
|
+
if (files.length > 0) {
|
|
80
|
+
await updateAutomaticIndexForFiles(cwd, files).catch(() => undefined);
|
|
81
|
+
}
|
|
79
82
|
if (result.status === 'pass') {
|
|
80
83
|
return {
|
|
81
84
|
content: [{
|
|
@@ -52,7 +52,7 @@ export async function handleIndex(cwd, options = {}) {
|
|
|
52
52
|
const candidateEstimate = 'Full codebase AST scan for pattern extraction';
|
|
53
53
|
try {
|
|
54
54
|
notifyProgress('info', 'Building pattern index...');
|
|
55
|
-
const indexer = new PatternIndexer(cwd, { useEmbeddings: options.semantic ??
|
|
55
|
+
const indexer = new PatternIndexer(cwd, { useEmbeddings: options.semantic ?? true });
|
|
56
56
|
const existingIndex = await loadPatternIndex(indexPath);
|
|
57
57
|
let index;
|
|
58
58
|
if (existingIndex && !options.force) {
|
|
@@ -60,7 +60,7 @@ async function getIndexHealthBlock(cwd) {
|
|
|
60
60
|
}
|
|
61
61
|
return `\n\n📊 Pattern Index: ${index.stats.totalPatterns} patterns across ${index.stats.totalFiles} files (updated ${index.lastUpdated}).`;
|
|
62
62
|
}
|
|
63
|
-
function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0) {
|
|
63
|
+
function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0, guidance) {
|
|
64
64
|
const telemetry = buildTelemetryMeta({
|
|
65
65
|
candidateText,
|
|
66
66
|
returnedText: text,
|
|
@@ -73,6 +73,7 @@ function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens =
|
|
|
73
73
|
text: appendContextFooter(text, telemetry, 'rigour_context_scope("your task")'),
|
|
74
74
|
}],
|
|
75
75
|
_telemetry: telemetry,
|
|
76
|
+
_guidance: guidance,
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
export async function handleRemember(cwd, key, value) {
|
|
@@ -137,7 +138,12 @@ export async function handleRecall(cwd, key) {
|
|
|
137
138
|
if (cached?.evidence?.length) {
|
|
138
139
|
const cachedBody = cached.evidence.join('\n');
|
|
139
140
|
const indexHealth = await getIndexHealthBlock(cwd);
|
|
140
|
-
return wrapRecallResult(`${cachedBody}${indexHealth}`, candidateText, 'semantic-hit', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(cachedBody))
|
|
141
|
+
return wrapRecallResult(`${cachedBody}${indexHealth}`, candidateText, 'semantic-hit', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(cachedBody)), cached.guidance ?? {
|
|
142
|
+
kind: 'memory',
|
|
143
|
+
query: key ?? 'all memories',
|
|
144
|
+
recommendation: key ? `Apply recalled memory "${key}" where relevant.` : 'Apply the recalled project memories where relevant.',
|
|
145
|
+
memoryRefs: key ? [{ id: key, label: key }] : [],
|
|
146
|
+
});
|
|
141
147
|
}
|
|
142
148
|
if (key) {
|
|
143
149
|
const memory = store.memories[key];
|
|
@@ -164,6 +170,12 @@ export async function handleRecall(cwd, key) {
|
|
|
164
170
|
const body = `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})`;
|
|
165
171
|
const indexHealth = await getIndexHealthBlock(cwd);
|
|
166
172
|
const fullText = `${body}${indexHealth}`;
|
|
173
|
+
const guidance = {
|
|
174
|
+
kind: 'memory',
|
|
175
|
+
query: key,
|
|
176
|
+
recommendation: `Apply recalled memory "${key}" where relevant.`,
|
|
177
|
+
memoryRefs: [{ id: key, label: key }],
|
|
178
|
+
};
|
|
167
179
|
await setSemanticQueryCache(cacheQuery, commitSha, {
|
|
168
180
|
query: cacheQuery,
|
|
169
181
|
resolvedOwner: 'memory',
|
|
@@ -172,8 +184,9 @@ export async function handleRecall(cwd, key) {
|
|
|
172
184
|
evidence: [body],
|
|
173
185
|
commitSha,
|
|
174
186
|
confidence: 1,
|
|
187
|
+
guidance,
|
|
175
188
|
}, cwd);
|
|
176
|
-
return wrapRecallResult(fullText, candidateText, 'miss');
|
|
189
|
+
return wrapRecallResult(fullText, candidateText, 'miss', 0, guidance);
|
|
177
190
|
}
|
|
178
191
|
const keys = Object.keys(store.memories);
|
|
179
192
|
if (keys.length === 0) {
|
|
@@ -207,6 +220,12 @@ export async function handleRecall(cwd, key) {
|
|
|
207
220
|
text = "NO MEMORIES STORED. Use rigour_remember to persist important instructions.";
|
|
208
221
|
}
|
|
209
222
|
text += await getIndexHealthBlock(cwd);
|
|
223
|
+
const guidance = {
|
|
224
|
+
kind: 'memory',
|
|
225
|
+
query: 'all memories',
|
|
226
|
+
recommendation: `Apply ${cleanMemories.length} recalled project memory item(s) where relevant.`,
|
|
227
|
+
memoryRefs: keys.filter(k => !taintedKeys.includes(k)).map(k => ({ id: k, label: k })),
|
|
228
|
+
};
|
|
210
229
|
await setSemanticQueryCache(cacheQuery, commitSha, {
|
|
211
230
|
query: cacheQuery,
|
|
212
231
|
resolvedOwner: 'memory',
|
|
@@ -215,8 +234,9 @@ export async function handleRecall(cwd, key) {
|
|
|
215
234
|
evidence: [text],
|
|
216
235
|
commitSha,
|
|
217
236
|
confidence: 1,
|
|
237
|
+
guidance,
|
|
218
238
|
}, cwd);
|
|
219
|
-
return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)));
|
|
239
|
+
return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)), guidance);
|
|
220
240
|
}
|
|
221
241
|
export async function handleForget(cwd, key) {
|
|
222
242
|
const store = await loadMemory(cwd);
|
|
@@ -68,9 +68,15 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
68
68
|
text: appendContextFooter(cachedText, telemetry, 'proceed with implementation or rigour_check when done'),
|
|
69
69
|
}],
|
|
70
70
|
_telemetry: telemetry,
|
|
71
|
+
_guidance: cached.guidance ?? {
|
|
72
|
+
kind: 'pattern',
|
|
73
|
+
query: patternName,
|
|
74
|
+
recommendation: `Apply the cached pattern guidance for "${patternName}".`,
|
|
75
|
+
},
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
78
|
let resultText = "";
|
|
79
|
+
let matchedPattern;
|
|
74
80
|
// 0. File Guard — BLOCK writes to protected paths
|
|
75
81
|
if (file) {
|
|
76
82
|
const matched = await checkFileGuard(cwd, file);
|
|
@@ -88,6 +94,12 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
88
94
|
return {
|
|
89
95
|
content: [{ type: "text", text: appendContextFooter(resultText, telemetry) }],
|
|
90
96
|
_telemetry: telemetry,
|
|
97
|
+
_guidance: {
|
|
98
|
+
kind: 'pattern',
|
|
99
|
+
query: patternName,
|
|
100
|
+
recommendation: `STOP. Do not modify protected path "${file}" without human review.`,
|
|
101
|
+
selectedFiles: [file],
|
|
102
|
+
},
|
|
91
103
|
};
|
|
92
104
|
}
|
|
93
105
|
}
|
|
@@ -96,6 +108,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
96
108
|
const matcher = new PatternMatcher(index);
|
|
97
109
|
const matchResult = await matcher.match({ name: patternName, type, intent });
|
|
98
110
|
if (matchResult.status === "FOUND_SIMILAR") {
|
|
111
|
+
matchedPattern = matchResult.matches[0].pattern;
|
|
99
112
|
resultText += `🚨 PATTERN REINVENTION DETECTED\n`;
|
|
100
113
|
resultText += `Similar pattern already exists: "${matchResult.matches[0].pattern.name}" in ${matchResult.matches[0].pattern.file}\n`;
|
|
101
114
|
resultText += `SUGGESTION: ${matchResult.suggestion}\n\n`;
|
|
@@ -128,11 +141,12 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
128
141
|
resultText += `\n`;
|
|
129
142
|
}
|
|
130
143
|
}
|
|
144
|
+
let recommendation = 'Proceed with implementation; no conflicting codebase pattern was found.';
|
|
131
145
|
if (!resultText) {
|
|
132
146
|
resultText = `✅ Pattern "${patternName}" is fresh, secure, and unique to the codebase.\n\nRECOMMENDED ACTION: Proceed with implementation.`;
|
|
133
147
|
}
|
|
134
148
|
else {
|
|
135
|
-
|
|
149
|
+
recommendation = "Proceed with caution, addressing the warnings above.";
|
|
136
150
|
if (resultText.includes("🚨 PATTERN REINVENTION")) {
|
|
137
151
|
recommendation = "STOP and REUSE the existing pattern mentioned above. Do not create a duplicate.";
|
|
138
152
|
}
|
|
@@ -144,6 +158,13 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
144
158
|
}
|
|
145
159
|
resultText += `\nRECOMMENDED ACTION: ${recommendation}`;
|
|
146
160
|
}
|
|
161
|
+
const guidance = {
|
|
162
|
+
kind: 'pattern',
|
|
163
|
+
query: patternName,
|
|
164
|
+
recommendation,
|
|
165
|
+
patternRefs: matchedPattern ? [{ id: matchedPattern.id, label: matchedPattern.name, file: matchedPattern.file }] : [],
|
|
166
|
+
selectedFiles: file ? [file] : matchedPattern?.file ? [matchedPattern.file] : [],
|
|
167
|
+
};
|
|
147
168
|
await setSemanticQueryCache(cacheQuery, commitSha, {
|
|
148
169
|
query: cacheQuery,
|
|
149
170
|
resolvedOwner: file ? path.dirname(file) : 'patterns',
|
|
@@ -152,6 +173,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
152
173
|
evidence: [resultText],
|
|
153
174
|
commitSha,
|
|
154
175
|
confidence: resultText.includes('✅') ? 0.9 : 0.7,
|
|
176
|
+
guidance,
|
|
155
177
|
}, cwd);
|
|
156
178
|
const telemetry = buildTelemetryMeta({
|
|
157
179
|
candidateText: indexScanEstimate,
|
|
@@ -167,6 +189,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
|
|
|
167
189
|
text: appendContextFooter(resultText, telemetry, 'rigour_check before declaring done'),
|
|
168
190
|
}],
|
|
169
191
|
_telemetry: telemetry,
|
|
192
|
+
_guidance: guidance,
|
|
170
193
|
};
|
|
171
194
|
}
|
|
172
195
|
export async function handleSecurityAudit(cwd) {
|
|
@@ -8,8 +8,32 @@ export type TelemetryMeta = {
|
|
|
8
8
|
taskId?: string;
|
|
9
9
|
agentId?: string;
|
|
10
10
|
queryHash?: string;
|
|
11
|
+
candidateFiles?: number;
|
|
12
|
+
returnedFiles?: number;
|
|
11
13
|
skipRecording?: boolean;
|
|
12
14
|
};
|
|
15
|
+
export type GuidanceMeta = {
|
|
16
|
+
kind: 'context-scope' | 'pattern' | 'memory';
|
|
17
|
+
query?: string;
|
|
18
|
+
recommendation: string;
|
|
19
|
+
patternRefs?: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
file?: string;
|
|
23
|
+
}>;
|
|
24
|
+
lessonRefs?: Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
visibility?: string;
|
|
28
|
+
}>;
|
|
29
|
+
memoryRefs?: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
}>;
|
|
33
|
+
selectedFiles?: string[];
|
|
34
|
+
excludedFileCount?: number;
|
|
35
|
+
conflicts?: number;
|
|
36
|
+
};
|
|
13
37
|
export type ToolResult = {
|
|
14
38
|
content: {
|
|
15
39
|
type: string;
|
|
@@ -20,6 +44,7 @@ export type ToolResult = {
|
|
|
20
44
|
_rigour_report?: unknown;
|
|
21
45
|
_meta?: unknown;
|
|
22
46
|
_telemetry?: TelemetryMeta;
|
|
47
|
+
_guidance?: GuidanceMeta;
|
|
23
48
|
};
|
|
24
49
|
export declare function buildTelemetryMeta(opts: {
|
|
25
50
|
candidateText: string;
|
|
@@ -29,6 +54,10 @@ export declare function buildTelemetryMeta(opts: {
|
|
|
29
54
|
taskId?: string;
|
|
30
55
|
agentId?: string;
|
|
31
56
|
queryHash?: string;
|
|
57
|
+
candidateTokens?: number;
|
|
58
|
+
returnedTokens?: number;
|
|
59
|
+
candidateFiles?: number;
|
|
60
|
+
returnedFiles?: number;
|
|
32
61
|
skipRecording?: boolean;
|
|
33
62
|
}): TelemetryMeta;
|
|
34
63
|
/** Resolve a short commit/ref fingerprint for cache keys. */
|
|
@@ -6,14 +6,16 @@ import path from 'path';
|
|
|
6
6
|
import { estimateTokenCount } from '@rigour-labs/core';
|
|
7
7
|
export function buildTelemetryMeta(opts) {
|
|
8
8
|
return {
|
|
9
|
-
candidateTokens: estimateTokenCount(opts.candidateText),
|
|
10
|
-
returnedTokens: estimateTokenCount(opts.returnedText),
|
|
9
|
+
candidateTokens: opts.candidateTokens ?? estimateTokenCount(opts.candidateText),
|
|
10
|
+
returnedTokens: opts.returnedTokens ?? estimateTokenCount(opts.returnedText),
|
|
11
11
|
cacheStatus: opts.cacheStatus,
|
|
12
12
|
deduplicatedTokens: opts.deduplicatedTokens ?? 0,
|
|
13
13
|
isEstimated: true,
|
|
14
14
|
taskId: opts.taskId,
|
|
15
15
|
agentId: opts.agentId,
|
|
16
16
|
queryHash: opts.queryHash,
|
|
17
|
+
candidateFiles: opts.candidateFiles,
|
|
18
|
+
returnedFiles: opts.returnedFiles,
|
|
17
19
|
skipRecording: opts.skipRecording,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GuidanceMeta, TelemetryMeta } from './context-telemetry.js';
|
|
2
|
+
export declare function buildStudioImpact(guidance?: GuidanceMeta, telemetry?: TelemetryMeta): {
|
|
3
|
+
guidance: GuidanceMeta | undefined;
|
|
4
|
+
telemetry: {
|
|
5
|
+
candidateTokens: number;
|
|
6
|
+
returnedTokens: number;
|
|
7
|
+
candidateFiles: number | undefined;
|
|
8
|
+
returnedFiles: number | undefined;
|
|
9
|
+
cacheStatus: import("./context-telemetry.js").CacheStatus;
|
|
10
|
+
classification: "observed" | "measured estimate";
|
|
11
|
+
} | undefined;
|
|
12
|
+
};
|
|
13
|
+
export declare function buildMcpResultMeta(existingMeta: unknown, requestId: string, resourceUri: string, guidance?: GuidanceMeta, telemetry?: TelemetryMeta): Record<string, unknown>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function classification(telemetry) {
|
|
2
|
+
return telemetry?.isEstimated ? 'measured estimate' : 'observed';
|
|
3
|
+
}
|
|
4
|
+
export function buildStudioImpact(guidance, telemetry) {
|
|
5
|
+
return {
|
|
6
|
+
guidance,
|
|
7
|
+
telemetry: telemetry ? {
|
|
8
|
+
candidateTokens: telemetry.candidateTokens,
|
|
9
|
+
returnedTokens: telemetry.returnedTokens,
|
|
10
|
+
candidateFiles: telemetry.candidateFiles,
|
|
11
|
+
returnedFiles: telemetry.returnedFiles,
|
|
12
|
+
cacheStatus: telemetry.cacheStatus,
|
|
13
|
+
classification: classification(telemetry),
|
|
14
|
+
} : undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function buildMcpResultMeta(existingMeta, requestId, resourceUri, guidance, telemetry) {
|
|
18
|
+
const base = existingMeta && typeof existingMeta === 'object' && !Array.isArray(existingMeta)
|
|
19
|
+
? existingMeta
|
|
20
|
+
: {};
|
|
21
|
+
if (!guidance)
|
|
22
|
+
return { ...base, ui: { resourceUri } };
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
ui: { resourceUri },
|
|
26
|
+
rigourImpact: {
|
|
27
|
+
requestId,
|
|
28
|
+
classification: classification(telemetry),
|
|
29
|
+
guidance,
|
|
30
|
+
context: telemetry ? {
|
|
31
|
+
candidateTokens: telemetry.candidateTokens,
|
|
32
|
+
returnedTokens: telemetry.returnedTokens,
|
|
33
|
+
avoidedTokens: Math.max(0, telemetry.candidateTokens - telemetry.returnedTokens),
|
|
34
|
+
candidateFiles: telemetry.candidateFiles,
|
|
35
|
+
returnedFiles: telemetry.returnedFiles,
|
|
36
|
+
excludedFiles: typeof telemetry.candidateFiles === 'number' && typeof telemetry.returnedFiles === 'number'
|
|
37
|
+
? Math.max(0, telemetry.candidateFiles - telemetry.returnedFiles)
|
|
38
|
+
: undefined,
|
|
39
|
+
cacheStatus: telemetry.cacheStatus,
|
|
40
|
+
} : undefined,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/mcp",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.1.0-beta.1",
|
|
4
4
|
"description": "MCP server + live dashboard for AI code governance — OWASP LLM Top 10 (10/10), real-time MCP App UI, 25+ security patterns, Bayesian learning Brain, hallucinated import detection, multi-agent governance. Works with Claude, Cursor, VS Code, ChatGPT, Goose, Windsurf. Industry presets for HIPAA, SOC2, FedRAMP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"execa": "^8.0.1",
|
|
63
63
|
"fs-extra": "^11.4.0",
|
|
64
64
|
"yaml": "^2.8.2",
|
|
65
|
-
"@rigour-labs/core": "6.0.
|
|
65
|
+
"@rigour-labs/core": "6.1.0-beta.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^25.0.3",
|