@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.
Files changed (67) hide show
  1. package/LICENSE +71 -0
  2. package/README.md +2 -2
  3. package/dist/hooks/cost-tracker.js +149 -11527
  4. package/dist/hooks/post-edit-context.js +127 -11493
  5. package/dist/hooks/post-tool-use.js +169 -11550
  6. package/dist/hooks/pre-compact.js +149 -11530
  7. package/dist/hooks/pre-delete-check.js +144 -11523
  8. package/dist/hooks/quality-event.js +149 -11527
  9. package/dist/hooks/session-end.js +188 -11570
  10. package/dist/hooks/session-start.js +159 -11534
  11. package/dist/hooks/user-prompt.js +149 -11530
  12. package/package.json +14 -19
  13. package/src/adr-generator.ts +292 -0
  14. package/src/analytics.ts +373 -0
  15. package/src/audit-trail.ts +450 -0
  16. package/src/backfill-sessions.ts +180 -0
  17. package/src/cli.ts +105 -0
  18. package/src/cloud-sync.ts +190 -0
  19. package/src/commands/doctor.ts +300 -0
  20. package/src/commands/init.ts +395 -0
  21. package/src/commands/install-hooks.ts +26 -0
  22. package/src/config.ts +357 -0
  23. package/src/cost-tracker.ts +355 -0
  24. package/src/db.ts +233 -0
  25. package/src/dependency-scorer.ts +337 -0
  26. package/src/docs-map.json +100 -0
  27. package/src/docs-tools.ts +517 -0
  28. package/src/domains.ts +181 -0
  29. package/src/hooks/cost-tracker.ts +66 -0
  30. package/src/hooks/intent-suggester.ts +131 -0
  31. package/src/hooks/post-edit-context.ts +91 -0
  32. package/src/hooks/post-tool-use.ts +175 -0
  33. package/src/hooks/pre-compact.ts +146 -0
  34. package/src/hooks/pre-delete-check.ts +153 -0
  35. package/src/hooks/quality-event.ts +127 -0
  36. package/src/hooks/security-gate.ts +121 -0
  37. package/src/hooks/session-end.ts +467 -0
  38. package/src/hooks/session-start.ts +210 -0
  39. package/src/hooks/user-prompt.ts +91 -0
  40. package/src/import-resolver.ts +224 -0
  41. package/src/memory-db.ts +1376 -0
  42. package/src/memory-tools.ts +391 -0
  43. package/src/middleware-tree.ts +70 -0
  44. package/src/observability-tools.ts +343 -0
  45. package/src/observation-extractor.ts +411 -0
  46. package/src/page-deps.ts +283 -0
  47. package/src/prompt-analyzer.ts +332 -0
  48. package/src/regression-detector.ts +319 -0
  49. package/src/rules.ts +57 -0
  50. package/src/schema-mapper.ts +232 -0
  51. package/src/security-scorer.ts +405 -0
  52. package/src/security-utils.ts +133 -0
  53. package/src/sentinel-db.ts +578 -0
  54. package/src/sentinel-scanner.ts +405 -0
  55. package/src/sentinel-tools.ts +512 -0
  56. package/src/sentinel-types.ts +140 -0
  57. package/src/server.ts +189 -0
  58. package/src/session-archiver.ts +112 -0
  59. package/src/session-state-generator.ts +174 -0
  60. package/src/team-knowledge.ts +407 -0
  61. package/src/tools.ts +847 -0
  62. package/src/transcript-parser.ts +458 -0
  63. package/src/trpc-index.ts +214 -0
  64. package/src/validate-features-runner.ts +106 -0
  65. package/src/validation-engine.ts +358 -0
  66. package/dist/cli.js +0 -7890
  67. package/dist/server.js +0 -7008
package/src/server.ts ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Massu. All rights reserved.
3
+ // Licensed under BSL 1.1 - see LICENSE file for details.
4
+
5
+ /**
6
+ * Massu MCP Server
7
+ *
8
+ * An MCP server that provides project-specific intelligence on top of
9
+ * vanilla CodeGraph. Communicates via JSON-RPC 2.0 over stdio.
10
+ *
11
+ * Tool names are configurable via massu.config.yaml toolPrefix.
12
+ */
13
+
14
+ import { getCodeGraphDb, getDataDb } from './db.ts';
15
+ import { getConfig } from './config.ts';
16
+ import { getToolDefinitions, handleToolCall } from './tools.ts';
17
+ import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
18
+
19
+ interface JsonRpcRequest {
20
+ jsonrpc: '2.0';
21
+ id?: number | string;
22
+ method: string;
23
+ params?: Record<string, unknown>;
24
+ }
25
+
26
+ interface JsonRpcResponse {
27
+ jsonrpc: '2.0';
28
+ id: number | string | null;
29
+ result?: unknown;
30
+ error?: { code: number; message: string; data?: unknown };
31
+ }
32
+
33
+ // Server state
34
+ let codegraphDb: ReturnType<typeof getCodeGraphDb> | null = null;
35
+ let dataDb: ReturnType<typeof getDataDb> | null = null;
36
+
37
+ function getDb() {
38
+ if (!codegraphDb) codegraphDb = getCodeGraphDb();
39
+ if (!dataDb) dataDb = getDataDb();
40
+ return { codegraphDb, dataDb: dataDb };
41
+ }
42
+
43
+ function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
44
+ const { method, params, id } = request;
45
+
46
+ switch (method) {
47
+ case 'initialize': {
48
+ return {
49
+ jsonrpc: '2.0',
50
+ id: id ?? null,
51
+ result: {
52
+ protocolVersion: '2024-11-05',
53
+ capabilities: {
54
+ tools: {},
55
+ },
56
+ serverInfo: {
57
+ name: 'massu',
58
+ version: '1.0.0',
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ case 'notifications/initialized': {
65
+ // Client acknowledges initialization - no response needed for notifications
66
+ return { jsonrpc: '2.0', id: id ?? null, result: {} };
67
+ }
68
+
69
+ case 'tools/list': {
70
+ const tools = getToolDefinitions();
71
+ return {
72
+ jsonrpc: '2.0',
73
+ id: id ?? null,
74
+ result: { tools },
75
+ };
76
+ }
77
+
78
+ case 'tools/call': {
79
+ const toolName = (params as { name: string })?.name;
80
+ const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
81
+
82
+ const { codegraphDb: cgDb, dataDb: lDb } = getDb();
83
+ const result = handleToolCall(toolName, toolArgs, lDb, cgDb);
84
+
85
+ return {
86
+ jsonrpc: '2.0',
87
+ id: id ?? null,
88
+ result,
89
+ };
90
+ }
91
+
92
+ case 'ping': {
93
+ return { jsonrpc: '2.0', id: id ?? null, result: {} };
94
+ }
95
+
96
+ default: {
97
+ return {
98
+ jsonrpc: '2.0',
99
+ id: id ?? null,
100
+ error: { code: -32601, message: `Method not found: ${method}` },
101
+ };
102
+ }
103
+ }
104
+ }
105
+
106
+ // === Startup: prune stale memory data (non-blocking) ===
107
+
108
+ function pruneMemoryOnStartup(): void {
109
+ try {
110
+ const memDb = getMemoryDb();
111
+ try {
112
+ const turns = pruneOldConversationTurns(memDb, 7);
113
+ const obsDeleted = pruneOldObservations(memDb, 90);
114
+
115
+ const totalPruned = turns.turnsDeleted + turns.detailsDeleted + obsDeleted;
116
+ if (totalPruned > 0) {
117
+ process.stderr.write(
118
+ `massu: Pruned memory DB on startup — ` +
119
+ `${turns.turnsDeleted} conversation turns, ` +
120
+ `${turns.detailsDeleted} tool call details (>7d), ` +
121
+ `${obsDeleted} observations (>90d)\n`
122
+ );
123
+ }
124
+ } finally {
125
+ memDb.close();
126
+ }
127
+ } catch (error) {
128
+ process.stderr.write(
129
+ `massu: Memory pruning failed (non-fatal): ${error instanceof Error ? error.message : String(error)}\n`
130
+ );
131
+ }
132
+ }
133
+
134
+ pruneMemoryOnStartup();
135
+
136
+ // === stdio JSON-RPC transport ===
137
+
138
+ let buffer = '';
139
+
140
+ process.stdin.setEncoding('utf-8');
141
+ process.stdin.on('data', (chunk: string) => {
142
+ buffer += chunk;
143
+
144
+ // Process complete messages (newline-delimited JSON-RPC)
145
+ let newlineIndex: number;
146
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
147
+ const line = buffer.slice(0, newlineIndex).trim();
148
+ buffer = buffer.slice(newlineIndex + 1);
149
+
150
+ if (!line) continue;
151
+
152
+ try {
153
+ const request = JSON.parse(line) as JsonRpcRequest;
154
+ const response = handleRequest(request);
155
+
156
+ // Don't send responses for notifications (no id)
157
+ if (request.id !== undefined) {
158
+ const responseStr = JSON.stringify(response);
159
+ process.stdout.write(responseStr + '\n');
160
+ }
161
+ } catch (error) {
162
+ const errorResponse: JsonRpcResponse = {
163
+ jsonrpc: '2.0',
164
+ id: null,
165
+ error: {
166
+ code: -32700,
167
+ message: `Parse error: ${error instanceof Error ? error.message : String(error)}`,
168
+ },
169
+ };
170
+ process.stdout.write(JSON.stringify(errorResponse) + '\n');
171
+ }
172
+ }
173
+ });
174
+
175
+ process.stdin.on('end', () => {
176
+ // Clean up database connections
177
+ if (codegraphDb) codegraphDb.close();
178
+ if (dataDb) dataDb.close();
179
+ process.exit(0);
180
+ });
181
+
182
+ // Handle errors gracefully
183
+ process.on('uncaughtException', (error) => {
184
+ process.stderr.write(`massu: Uncaught exception: ${error.message}\n`);
185
+ });
186
+
187
+ process.on('unhandledRejection', (reason) => {
188
+ process.stderr.write(`massu: Unhandled rejection: ${reason}\n`);
189
+ });
@@ -0,0 +1,112 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
5
+ import { resolve, dirname } from 'path';
6
+ import type Database from 'better-sqlite3';
7
+ import { generateCurrentMd } from './session-state-generator.ts';
8
+ import { getProjectRoot } from './config.ts';
9
+
10
+ // ============================================================
11
+ // P5-002: Session Archiver
12
+ // ============================================================
13
+
14
+ const PROJECT_ROOT = getProjectRoot();
15
+
16
+ /**
17
+ * Archive the current CURRENT.md and generate a new one from memory DB.
18
+ */
19
+ export function archiveAndRegenerate(db: Database.Database, sessionId: string): {
20
+ archived: boolean;
21
+ archivePath?: string;
22
+ newContent: string;
23
+ } {
24
+ const currentMdPath = resolve(PROJECT_ROOT, '.claude/session-state/CURRENT.md');
25
+ const archiveDir = resolve(PROJECT_ROOT, '.claude/session-state/archive');
26
+ let archived = false;
27
+ let archivePath: string | undefined;
28
+
29
+ // 1. Archive existing CURRENT.md if it exists and has content
30
+ if (existsSync(currentMdPath)) {
31
+ const existingContent = readFileSync(currentMdPath, 'utf-8');
32
+ if (existingContent.trim().length > 10) {
33
+ // Extract date and task description for filename
34
+ const { date, slug } = extractArchiveInfo(existingContent);
35
+ archivePath = resolve(archiveDir, `${date}-${slug}.md`);
36
+
37
+ // Ensure archive directory exists
38
+ if (!existsSync(archiveDir)) {
39
+ mkdirSync(archiveDir, { recursive: true });
40
+ }
41
+
42
+ // Move to archive (rename is atomic on same filesystem)
43
+ try {
44
+ renameSync(currentMdPath, archivePath);
45
+ archived = true;
46
+ } catch (_e) {
47
+ // If rename fails (cross-device), copy+delete
48
+ writeFileSync(archivePath, existingContent);
49
+ archived = true;
50
+ }
51
+ }
52
+ }
53
+
54
+ // 2. Generate new CURRENT.md from memory DB
55
+ const newContent = generateCurrentMd(db, sessionId);
56
+
57
+ // 3. Write new CURRENT.md
58
+ const dir = dirname(currentMdPath);
59
+ if (!existsSync(dir)) {
60
+ mkdirSync(dir, { recursive: true });
61
+ }
62
+ writeFileSync(currentMdPath, newContent, 'utf-8');
63
+
64
+ return { archived, archivePath, newContent };
65
+ }
66
+
67
+ /**
68
+ * Extract date and slug from existing CURRENT.md content for archive naming.
69
+ */
70
+ function extractArchiveInfo(content: string): { date: string; slug: string } {
71
+ // Try to extract date from "# Session State - January 30, 2026"
72
+ const dateMatch = content.match(/# Session State - (\w+ \d+, \d+)/);
73
+ let date = new Date().toISOString().split('T')[0]; // fallback
74
+
75
+ if (dateMatch) {
76
+ const parsed = new Date(dateMatch[1]);
77
+ if (!isNaN(parsed.getTime())) {
78
+ date = parsed.toISOString().split('T')[0];
79
+ }
80
+ }
81
+
82
+ // Also try ISO date format "**Last Updated**: 2026-01-30"
83
+ const isoMatch = content.match(/(\d{4}-\d{2}-\d{2})/);
84
+ if (isoMatch) {
85
+ date = isoMatch[1];
86
+ }
87
+
88
+ // Extract task description for slug
89
+ let slug = 'session';
90
+ const taskMatch = content.match(/\*\*Task\*\*:\s*(.+)/);
91
+ if (taskMatch) {
92
+ slug = taskMatch[1]
93
+ .toLowerCase()
94
+ .replace(/[^a-z0-9]+/g, '-')
95
+ .replace(/^-|-$/g, '')
96
+ .slice(0, 50);
97
+ }
98
+
99
+ // Extract status for slug if no task
100
+ if (slug === 'session') {
101
+ const statusMatch = content.match(/\*\*Status\*\*:\s*\w+\s*-\s*(.+)/);
102
+ if (statusMatch) {
103
+ slug = statusMatch[1]
104
+ .toLowerCase()
105
+ .replace(/[^a-z0-9]+/g, '-')
106
+ .replace(/^-|-$/g, '')
107
+ .slice(0, 50);
108
+ }
109
+ }
110
+
111
+ return { date, slug };
112
+ }
@@ -0,0 +1,174 @@
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
+
6
+ // ============================================================
7
+ // P5-001: CURRENT.md Generator
8
+ // ============================================================
9
+
10
+ /**
11
+ * Generate CURRENT.md content from memory database.
12
+ * Replaces manual session state maintenance.
13
+ */
14
+ export function generateCurrentMd(db: Database.Database, sessionId: string): string {
15
+ const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as Record<string, unknown> | undefined;
16
+ if (!session) return '# Session State\n\nNo active session found.\n';
17
+
18
+ const observations = db.prepare(
19
+ 'SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC'
20
+ ).all(sessionId) as Array<Record<string, unknown>>;
21
+
22
+ const summary = db.prepare(
23
+ 'SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1'
24
+ ).get(sessionId) as Record<string, unknown> | undefined;
25
+
26
+ const prompts = db.prepare(
27
+ 'SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC LIMIT 1'
28
+ ).all(sessionId) as Array<{ prompt_text: string }>;
29
+
30
+ const date = new Date().toISOString().split('T')[0];
31
+ const firstPrompt = prompts[0]?.prompt_text ?? 'Unknown task';
32
+ const taskSummary = firstPrompt.slice(0, 100).replace(/\n/g, ' ');
33
+
34
+ const lines: string[] = [];
35
+ lines.push(`# Session State - ${formatDate(date)}`);
36
+ lines.push('');
37
+ lines.push(`**Last Updated**: ${new Date().toISOString().replace('T', ' ').slice(0, 19)} (auto-generated from massu-memory)`);
38
+ lines.push(`**Status**: ${session.status === 'active' ? 'IN PROGRESS' : (session.status as string).toUpperCase()} - ${taskSummary}`);
39
+ lines.push(`**Task**: ${taskSummary}`);
40
+ lines.push(`**Session ID**: ${sessionId}`);
41
+ lines.push(`**Branch**: ${session.git_branch ?? 'unknown'}`);
42
+ lines.push('');
43
+ lines.push('---');
44
+ lines.push('');
45
+
46
+ // Completed work
47
+ const completedObs = observations.filter(o =>
48
+ ['feature', 'bugfix', 'refactor', 'file_change'].includes(o.type as string)
49
+ );
50
+ if (completedObs.length > 0 || summary) {
51
+ lines.push('## COMPLETED WORK');
52
+ lines.push('');
53
+
54
+ if (summary?.completed) {
55
+ lines.push(summary.completed as string);
56
+ lines.push('');
57
+ }
58
+
59
+ // Files created
60
+ const filesCreated = observations
61
+ .filter(o => o.type === 'file_change' && (o.title as string).startsWith('Created'))
62
+ .map(o => {
63
+ const files = safeParseJson(o.files_involved as string, []) as string[];
64
+ return files[0] ?? (o.title as string).replace('Created/wrote: ', '');
65
+ });
66
+
67
+ if (filesCreated.length > 0) {
68
+ lines.push('### Files Created');
69
+ lines.push('');
70
+ lines.push('| File | Purpose |');
71
+ lines.push('|------|---------|');
72
+ for (const f of filesCreated) {
73
+ lines.push(`| \`${f}\` | |`);
74
+ }
75
+ lines.push('');
76
+ }
77
+
78
+ // Files modified
79
+ const filesModified = observations
80
+ .filter(o => o.type === 'file_change' && (o.title as string).startsWith('Edited'))
81
+ .map(o => {
82
+ const files = safeParseJson(o.files_involved as string, []) as string[];
83
+ return files[0] ?? (o.title as string).replace('Edited: ', '');
84
+ });
85
+
86
+ if (filesModified.length > 0) {
87
+ lines.push('### Files Modified');
88
+ lines.push('');
89
+ lines.push('| File | Change |');
90
+ lines.push('|------|--------|');
91
+ for (const f of [...new Set(filesModified)]) {
92
+ lines.push(`| \`${f}\` | |`);
93
+ }
94
+ lines.push('');
95
+ }
96
+ }
97
+
98
+ // Key decisions
99
+ const decisions = observations.filter(o => o.type === 'decision');
100
+ if (decisions.length > 0) {
101
+ lines.push('### Key Decisions');
102
+ lines.push('');
103
+ for (const d of decisions) {
104
+ lines.push(`- ${d.title}`);
105
+ }
106
+ lines.push('');
107
+ }
108
+
109
+ // Failed attempts
110
+ const failures = observations.filter(o => o.type === 'failed_attempt');
111
+ if (failures.length > 0) {
112
+ lines.push('## FAILED ATTEMPTS (DO NOT RETRY)');
113
+ lines.push('');
114
+ for (const f of failures) {
115
+ lines.push(`- ${f.title}`);
116
+ if (f.detail) lines.push(` ${(f.detail as string).slice(0, 200)}`);
117
+ }
118
+ lines.push('');
119
+ }
120
+
121
+ // Verification evidence
122
+ const vrChecks = observations.filter(o => o.type === 'vr_check');
123
+ if (vrChecks.length > 0) {
124
+ lines.push('## VERIFICATION EVIDENCE');
125
+ lines.push('');
126
+ for (const v of vrChecks) {
127
+ lines.push(`- ${v.title}`);
128
+ }
129
+ lines.push('');
130
+ }
131
+
132
+ // Pending / next steps
133
+ if (summary?.next_steps) {
134
+ lines.push('## PENDING');
135
+ lines.push('');
136
+ lines.push(summary.next_steps as string);
137
+ lines.push('');
138
+ }
139
+
140
+ // Plan document
141
+ if (session.plan_file) {
142
+ lines.push('## PLAN DOCUMENT');
143
+ lines.push('');
144
+ lines.push(`\`${session.plan_file}\``);
145
+
146
+ // Show plan progress if available
147
+ if (summary?.plan_progress) {
148
+ const progress = safeParseJson(summary.plan_progress as string, {}) as Record<string, string>;
149
+ const total = Object.keys(progress).length;
150
+ const complete = Object.values(progress).filter(v => v === 'complete').length;
151
+ if (total > 0) {
152
+ lines.push(`- Progress: ${complete}/${total} items complete`);
153
+ }
154
+ }
155
+ lines.push('');
156
+ }
157
+
158
+ return lines.join('\n');
159
+ }
160
+
161
+ function formatDate(dateStr: string): string {
162
+ const months = ['January', 'February', 'March', 'April', 'May', 'June',
163
+ 'July', 'August', 'September', 'October', 'November', 'December'];
164
+ const [year, month, day] = dateStr.split('-').map(Number);
165
+ return `${months[month - 1]} ${day}, ${year}`;
166
+ }
167
+
168
+ function safeParseJson(json: string, fallback: unknown): unknown {
169
+ try {
170
+ return JSON.parse(json);
171
+ } catch (_e) {
172
+ return fallback;
173
+ }
174
+ }