@massu/core 0.1.0 → 0.1.2
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/LICENSE +71 -0
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +149 -11527
- package/dist/hooks/post-edit-context.js +127 -11493
- package/dist/hooks/post-tool-use.js +169 -11550
- package/dist/hooks/pre-compact.js +149 -11530
- package/dist/hooks/pre-delete-check.js +144 -11523
- package/dist/hooks/quality-event.js +149 -11527
- package/dist/hooks/session-end.js +188 -11570
- package/dist/hooks/session-start.js +159 -11534
- package/dist/hooks/user-prompt.js +149 -11530
- package/package.json +14 -19
- package/src/adr-generator.ts +292 -0
- package/src/analytics.ts +373 -0
- package/src/audit-trail.ts +450 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +190 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +395 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/cost-tracker.ts +355 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +337 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +517 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +1376 -0
- package/src/memory-tools.ts +391 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +343 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +332 -0
- package/src/regression-detector.ts +319 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +405 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +578 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +512 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +189 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +407 -0
- package/src/tools.ts +847 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +106 -0
- package/src/validation-engine.ts +358 -0
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
package/package.json
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, feature registry, code intelligence, and rule enforcement",
|
|
6
|
-
"
|
|
7
|
-
"author": "Massu AI <hello@massu.ai>",
|
|
8
|
-
"main": "dist/server.js",
|
|
9
|
-
"types": "src/server.ts",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": "./dist/server.js",
|
|
12
|
-
"./cli": "./dist/cli.js"
|
|
13
|
-
},
|
|
6
|
+
"main": "src/server.ts",
|
|
14
7
|
"bin": {
|
|
15
|
-
"massu": "./
|
|
8
|
+
"massu": "./src/cli.ts"
|
|
16
9
|
},
|
|
17
10
|
"scripts": {
|
|
18
11
|
"start": "npx tsx src/server.ts",
|
|
19
12
|
"test": "vitest run",
|
|
20
|
-
"build": "tsc --noEmit && npm run build:
|
|
21
|
-
"build:
|
|
22
|
-
"
|
|
23
|
-
"test:integration": "vitest run src/__tests__/integration/",
|
|
24
|
-
"prepublishOnly": "npm run build"
|
|
13
|
+
"build": "tsc --noEmit && npm run build:hooks",
|
|
14
|
+
"build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
15
|
+
"prepublishOnly": "bash ../../scripts/prepublish-check.sh && npm run build"
|
|
25
16
|
},
|
|
26
17
|
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
27
19
|
"better-sqlite3": "^12.6.2",
|
|
28
20
|
"yaml": "^2.4.0",
|
|
29
21
|
"zod": "^3.23.0"
|
|
@@ -36,7 +28,10 @@
|
|
|
36
28
|
"vitest": "^4.0.18"
|
|
37
29
|
},
|
|
38
30
|
"files": [
|
|
39
|
-
"
|
|
31
|
+
"src/**/*",
|
|
32
|
+
"!src/__tests__/**",
|
|
33
|
+
"dist/**/*",
|
|
34
|
+
"LICENSE"
|
|
40
35
|
],
|
|
41
36
|
"keywords": [
|
|
42
37
|
"mcp",
|
|
@@ -51,11 +46,11 @@
|
|
|
51
46
|
},
|
|
52
47
|
"repository": {
|
|
53
48
|
"type": "git",
|
|
54
|
-
"url": "https://github.com/
|
|
49
|
+
"url": "https://github.com/massu-ai/massu.git",
|
|
55
50
|
"directory": "packages/core"
|
|
56
51
|
},
|
|
57
52
|
"bugs": {
|
|
58
|
-
"url": "https://github.com/
|
|
53
|
+
"url": "https://github.com/massu-ai/massu/issues"
|
|
59
54
|
},
|
|
60
|
-
"homepage": "https://massu
|
|
55
|
+
"homepage": "https://github.com/massu-ai/massu#readme"
|
|
61
56
|
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import type Database from 'better-sqlite3';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
|
+
import { getConfig } from './config.ts';
|
|
7
|
+
|
|
8
|
+
// ============================================================
|
|
9
|
+
// ADR (Architecture Decision Record) Auto-Generation
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
13
|
+
function p(baseName: string): string {
|
|
14
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Default decision detection phrases. Configurable via governance.adr.detection_phrases */
|
|
18
|
+
const DEFAULT_DETECTION_PHRASES = ['chose', 'decided', 'switching to', 'moving from', 'going with'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get decision detection phrases from config or defaults.
|
|
22
|
+
*/
|
|
23
|
+
function getDetectionPhrases(): string[] {
|
|
24
|
+
return getConfig().governance?.adr?.detection_phrases ?? DEFAULT_DETECTION_PHRASES;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect decision patterns in text.
|
|
29
|
+
*/
|
|
30
|
+
export function detectDecisionPatterns(text: string): boolean {
|
|
31
|
+
const phrases = getDetectionPhrases();
|
|
32
|
+
const lower = text.toLowerCase();
|
|
33
|
+
return phrases.some(phrase => lower.includes(phrase));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract alternatives from a decision description.
|
|
38
|
+
*/
|
|
39
|
+
export function extractAlternatives(description: string): string[] {
|
|
40
|
+
const alternatives: string[] = [];
|
|
41
|
+
|
|
42
|
+
// Pattern: "X over Y"
|
|
43
|
+
const overMatch = description.match(/(\w[\w\s-]+)\s+over\s+(\w[\w\s-]+)/i);
|
|
44
|
+
if (overMatch) {
|
|
45
|
+
alternatives.push(overMatch[1].trim());
|
|
46
|
+
alternatives.push(overMatch[2].trim());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Pattern: "X instead of Y"
|
|
50
|
+
const insteadMatch = description.match(/(\w[\w\s-]+)\s+instead\s+of\s+(\w[\w\s-]+)/i);
|
|
51
|
+
if (insteadMatch) {
|
|
52
|
+
alternatives.push(insteadMatch[1].trim());
|
|
53
|
+
alternatives.push(insteadMatch[2].trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Pattern: "switching from X to Y"
|
|
57
|
+
const switchMatch = description.match(/switching\s+from\s+(\w[\w\s-]+)\s+to\s+(\w[\w\s-]+)/i);
|
|
58
|
+
if (switchMatch) {
|
|
59
|
+
alternatives.push(switchMatch[2].trim()); // Chosen
|
|
60
|
+
alternatives.push(switchMatch[1].trim()); // Rejected
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return [...new Set(alternatives)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Store an architecture decision.
|
|
68
|
+
*/
|
|
69
|
+
export function storeDecision(
|
|
70
|
+
db: Database.Database,
|
|
71
|
+
decision: {
|
|
72
|
+
title: string;
|
|
73
|
+
context: string;
|
|
74
|
+
decision: string;
|
|
75
|
+
alternatives: string[];
|
|
76
|
+
consequences: string;
|
|
77
|
+
sessionId?: string;
|
|
78
|
+
status?: string;
|
|
79
|
+
}
|
|
80
|
+
): number {
|
|
81
|
+
const result = db.prepare(`
|
|
82
|
+
INSERT INTO architecture_decisions
|
|
83
|
+
(title, context, decision, alternatives, consequences, session_id, status)
|
|
84
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
85
|
+
`).run(
|
|
86
|
+
decision.title,
|
|
87
|
+
decision.context,
|
|
88
|
+
decision.decision,
|
|
89
|
+
JSON.stringify(decision.alternatives),
|
|
90
|
+
decision.consequences,
|
|
91
|
+
decision.sessionId ?? null,
|
|
92
|
+
decision.status ?? 'accepted'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return Number(result.lastInsertRowid);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================
|
|
99
|
+
// MCP Tool Definitions & Handlers
|
|
100
|
+
// ============================================================
|
|
101
|
+
|
|
102
|
+
export function getAdrToolDefinitions(): ToolDefinition[] {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
name: p('adr_list'),
|
|
106
|
+
description: 'List all recorded architecture decisions. Filter by status or search text.',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
status: { type: 'string', description: 'Filter by status: accepted, superseded, deprecated' },
|
|
111
|
+
search: { type: 'string', description: 'Search in title and context' },
|
|
112
|
+
},
|
|
113
|
+
required: [],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: p('adr_detail'),
|
|
118
|
+
description: 'Get full details of a specific architecture decision by ID.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
id: { type: 'number', description: 'ADR ID' },
|
|
123
|
+
},
|
|
124
|
+
required: ['id'],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: p('adr_create'),
|
|
129
|
+
description: 'Generate an ADR from a description. Extracts alternatives, context, and consequences.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
title: { type: 'string', description: 'Decision title' },
|
|
134
|
+
context: { type: 'string', description: 'Why was this decision needed?' },
|
|
135
|
+
decision: { type: 'string', description: 'What was decided?' },
|
|
136
|
+
consequences: { type: 'string', description: 'What are the consequences?' },
|
|
137
|
+
},
|
|
138
|
+
required: ['title', 'decision'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ADR_BASE_NAMES = new Set(['adr_list', 'adr_detail', 'adr_create']);
|
|
145
|
+
|
|
146
|
+
export function isAdrTool(name: string): boolean {
|
|
147
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
148
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
149
|
+
return ADR_BASE_NAMES.has(baseName);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function handleAdrToolCall(
|
|
153
|
+
name: string,
|
|
154
|
+
args: Record<string, unknown>,
|
|
155
|
+
memoryDb: Database.Database
|
|
156
|
+
): ToolResult {
|
|
157
|
+
try {
|
|
158
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
159
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
160
|
+
|
|
161
|
+
switch (baseName) {
|
|
162
|
+
case 'adr_list':
|
|
163
|
+
return handleAdrList(args, memoryDb);
|
|
164
|
+
case 'adr_detail':
|
|
165
|
+
return handleAdrDetail(args, memoryDb);
|
|
166
|
+
case 'adr_create':
|
|
167
|
+
return handleAdrGenerate(args, memoryDb);
|
|
168
|
+
default:
|
|
169
|
+
return text(`Unknown ADR tool: ${name}`);
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('adr_list')} {}, ${p('adr_create')} { title: "...", decision: "..." }`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleAdrList(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
177
|
+
let sql = 'SELECT id, title, status, created_at FROM architecture_decisions WHERE 1=1';
|
|
178
|
+
const params: string[] = [];
|
|
179
|
+
|
|
180
|
+
if (args.status) {
|
|
181
|
+
sql += ' AND status = ?';
|
|
182
|
+
params.push(args.status as string);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (args.search) {
|
|
186
|
+
sql += ' AND (title LIKE ? OR context LIKE ?)';
|
|
187
|
+
const searchTerm = `%${args.search}%`;
|
|
188
|
+
params.push(searchTerm, searchTerm);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
sql += ' ORDER BY created_at DESC';
|
|
192
|
+
|
|
193
|
+
const decisions = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
194
|
+
|
|
195
|
+
if (decisions.length === 0) {
|
|
196
|
+
return text(`No architecture decisions found. Decisions are recorded when you use ${p('adr_create')} during design discussions. Try: ${p('adr_create')} { title: "Use Redis for caching", decision: "We chose Redis over Memcached" }`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const lines = [
|
|
200
|
+
`## Architecture Decisions (${decisions.length})`,
|
|
201
|
+
'',
|
|
202
|
+
'| ID | Title | Status | Date |',
|
|
203
|
+
'|----|-------|--------|------|',
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
for (const d of decisions) {
|
|
207
|
+
lines.push(`| ${d.id} | ${d.title} | ${d.status} | ${(d.created_at as string).slice(0, 10)} |`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return text(lines.join('\n'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleAdrDetail(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
214
|
+
const id = args.id as number;
|
|
215
|
+
if (!id) return text(`Usage: ${p('adr_detail')} { id: 1 } - Get full details of an architecture decision.`);
|
|
216
|
+
|
|
217
|
+
const decision = db.prepare(
|
|
218
|
+
'SELECT * FROM architecture_decisions WHERE id = ?'
|
|
219
|
+
).get(id) as Record<string, unknown> | undefined;
|
|
220
|
+
|
|
221
|
+
if (!decision) {
|
|
222
|
+
return text(`ADR #${id} not found. Decisions are stored when created via ${p('adr_create')}. Try: ${p('adr_list')} {} to see all recorded decisions.`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const alternatives = JSON.parse((decision.alternatives as string) || '[]') as string[];
|
|
226
|
+
|
|
227
|
+
const lines = [
|
|
228
|
+
`# ADR-${decision.id}: ${decision.title}`,
|
|
229
|
+
`Status: ${decision.status}`,
|
|
230
|
+
`Date: ${(decision.created_at as string).slice(0, 10)}`,
|
|
231
|
+
'',
|
|
232
|
+
'## Context',
|
|
233
|
+
decision.context as string || 'Not specified',
|
|
234
|
+
'',
|
|
235
|
+
'## Decision',
|
|
236
|
+
decision.decision as string,
|
|
237
|
+
'',
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
if (alternatives.length > 0) {
|
|
241
|
+
lines.push('## Alternatives Considered');
|
|
242
|
+
for (const alt of alternatives) {
|
|
243
|
+
lines.push(`- ${alt}`);
|
|
244
|
+
}
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (decision.consequences) {
|
|
249
|
+
lines.push('## Consequences');
|
|
250
|
+
lines.push(decision.consequences as string);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return text(lines.join('\n'));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleAdrGenerate(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
257
|
+
const title = args.title as string;
|
|
258
|
+
const decisionText = args.decision as string;
|
|
259
|
+
if (!title || !decisionText) return text(`Usage: ${p('adr_create')} { title: "Use Redis for caching", decision: "We chose Redis over Memcached for caching", context: "...", consequences: "..." }`);
|
|
260
|
+
|
|
261
|
+
const context = (args.context as string) ?? '';
|
|
262
|
+
const consequences = (args.consequences as string) ?? '';
|
|
263
|
+
|
|
264
|
+
// Extract alternatives from the decision text
|
|
265
|
+
const alternatives = extractAlternatives(decisionText);
|
|
266
|
+
|
|
267
|
+
const id = storeDecision(db, {
|
|
268
|
+
title,
|
|
269
|
+
context,
|
|
270
|
+
decision: decisionText,
|
|
271
|
+
alternatives,
|
|
272
|
+
consequences,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const lines = [
|
|
276
|
+
`## ADR-${id} Created: ${title}`,
|
|
277
|
+
'',
|
|
278
|
+
`**Status**: accepted`,
|
|
279
|
+
`**Decision**: ${decisionText}`,
|
|
280
|
+
alternatives.length > 0 ? `**Alternatives**: ${alternatives.join(', ')}` : '',
|
|
281
|
+
context ? `**Context**: ${context}` : '',
|
|
282
|
+
consequences ? `**Consequences**: ${consequences}` : '',
|
|
283
|
+
'',
|
|
284
|
+
`View full details: ${p('adr_detail')} { id: ${id} }`,
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
return text(lines.filter(Boolean).join('\n'));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function text(content: string): ToolResult {
|
|
291
|
+
return { content: [{ type: 'text', text: content }] };
|
|
292
|
+
}
|