@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
|
@@ -0,0 +1,343 @@
|
|
|
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 {
|
|
7
|
+
getConversationTurns,
|
|
8
|
+
searchConversationTurns,
|
|
9
|
+
getToolPatterns,
|
|
10
|
+
getSessionStats,
|
|
11
|
+
getObservabilityDbSize,
|
|
12
|
+
pruneOldConversationTurns,
|
|
13
|
+
} from './memory-db.ts';
|
|
14
|
+
import { getConfig } from './config.ts';
|
|
15
|
+
|
|
16
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
17
|
+
function p(baseName: string): string {
|
|
18
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// Observability MCP Tools (P3-001 through P3-004)
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
const OBSERVABILITY_BASE_NAMES = new Set([
|
|
26
|
+
'session_replay',
|
|
27
|
+
'prompt_analysis',
|
|
28
|
+
'tool_patterns',
|
|
29
|
+
'session_stats',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export function isObservabilityTool(name: string): boolean {
|
|
33
|
+
const prefix = getConfig().toolPrefix + '_';
|
|
34
|
+
const baseName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
35
|
+
return OBSERVABILITY_BASE_NAMES.has(baseName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getObservabilityToolDefinitions(): ToolDefinition[] {
|
|
39
|
+
return [
|
|
40
|
+
// P3-001: session_replay
|
|
41
|
+
{
|
|
42
|
+
name: p('session_replay'),
|
|
43
|
+
description: 'Replay a past session as a chronological conversation with user prompts, assistant responses, and optional tool call details. Requires conversation data captured by the session-end hook.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
session_id: { type: 'string', description: 'Session ID to replay' },
|
|
48
|
+
turn_from: { type: 'number', description: 'Start from turn N (optional)' },
|
|
49
|
+
turn_to: { type: 'number', description: 'End at turn N (optional)' },
|
|
50
|
+
include_tool_calls: { type: 'boolean', description: 'Include tool call details in replay (default: false)' },
|
|
51
|
+
},
|
|
52
|
+
required: ['session_id'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
// P3-002: prompt_analysis
|
|
56
|
+
{
|
|
57
|
+
name: p('prompt_analysis'),
|
|
58
|
+
description: 'Search and analyze prompts across sessions using FTS5 full-text search on conversation_turns_fts. Find prompts by keyword, filter by date range or complexity (tool call count).',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
query: { type: 'string', description: 'FTS5 search query within prompts and responses' },
|
|
63
|
+
session_id: { type: 'string', description: 'Filter to specific session' },
|
|
64
|
+
date_from: { type: 'string', description: 'Start date (ISO format)' },
|
|
65
|
+
date_to: { type: 'string', description: 'End date (ISO format)' },
|
|
66
|
+
min_tool_calls: { type: 'number', description: 'Filter by turns with N+ tool calls' },
|
|
67
|
+
limit: { type: 'number', description: 'Max results (default: 20)' },
|
|
68
|
+
},
|
|
69
|
+
required: [],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
// P3-003: tool_patterns
|
|
73
|
+
{
|
|
74
|
+
name: p('tool_patterns'),
|
|
75
|
+
description: 'Analyze tool usage patterns across sessions. Shows counts, success rates, average I/O sizes per tool. Can group by tool name, session, or day.',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
session_id: { type: 'string', description: 'Filter to specific session' },
|
|
80
|
+
tool_name: { type: 'string', description: 'Filter to specific tool (Read, Write, Edit, Bash, etc.)' },
|
|
81
|
+
date_from: { type: 'string', description: 'Start date (ISO format)' },
|
|
82
|
+
group_by: { type: 'string', description: '"tool" | "session" | "day" (default: "tool")' },
|
|
83
|
+
},
|
|
84
|
+
required: [],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
// P3-004: session_stats
|
|
88
|
+
{
|
|
89
|
+
name: p('session_stats'),
|
|
90
|
+
description: 'Get per-session statistics: turn count, tool call breakdown, token usage, duration. Includes database size monitoring for observability data.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
session_id: { type: 'string', description: 'Specific session ID (default: all recent)' },
|
|
95
|
+
limit: { type: 'number', description: 'Max sessions to show (default: 10)' },
|
|
96
|
+
},
|
|
97
|
+
required: [],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function handleObservabilityToolCall(
|
|
104
|
+
name: string,
|
|
105
|
+
args: Record<string, unknown>,
|
|
106
|
+
memoryDb: Database.Database
|
|
107
|
+
): ToolResult {
|
|
108
|
+
try {
|
|
109
|
+
const prefix = getConfig().toolPrefix + '_';
|
|
110
|
+
const baseName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
111
|
+
|
|
112
|
+
switch (baseName) {
|
|
113
|
+
case 'session_replay':
|
|
114
|
+
return handleSessionReplay(args, memoryDb);
|
|
115
|
+
case 'prompt_analysis':
|
|
116
|
+
return handlePromptAnalysis(args, memoryDb);
|
|
117
|
+
case 'tool_patterns':
|
|
118
|
+
return handleToolPatterns(args, memoryDb);
|
|
119
|
+
case 'session_stats':
|
|
120
|
+
return handleSessionStats(args, memoryDb);
|
|
121
|
+
default:
|
|
122
|
+
return text(`Unknown observability tool: ${name}`);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================
|
|
130
|
+
// Tool Handlers
|
|
131
|
+
// ============================================================
|
|
132
|
+
|
|
133
|
+
function handleSessionReplay(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
134
|
+
const sessionId = args.session_id as string;
|
|
135
|
+
if (!sessionId) return text('Error: session_id is required');
|
|
136
|
+
|
|
137
|
+
const includeToolCalls = (args.include_tool_calls as boolean) ?? false;
|
|
138
|
+
const turns = getConversationTurns(db, sessionId, {
|
|
139
|
+
turnFrom: args.turn_from as number | undefined,
|
|
140
|
+
turnTo: args.turn_to as number | undefined,
|
|
141
|
+
includeToolCalls,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (turns.length === 0) {
|
|
145
|
+
return text(`No conversation turns found for session ${sessionId.slice(0, 8)}...\nNote: Conversation data is captured by the session-end hook. Only sessions that have ended will have replay data.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lines = [`## Session Replay: ${sessionId.slice(0, 12)}...`, `Turns: ${turns.length}`, ''];
|
|
149
|
+
|
|
150
|
+
for (const turn of turns) {
|
|
151
|
+
lines.push(`### Turn ${turn.turn_number}`);
|
|
152
|
+
lines.push(`**User**: ${turn.user_prompt.slice(0, 2000)}`);
|
|
153
|
+
lines.push('');
|
|
154
|
+
|
|
155
|
+
if (turn.assistant_response) {
|
|
156
|
+
lines.push(`**Assistant**: ${turn.assistant_response.slice(0, 2000)}`);
|
|
157
|
+
lines.push('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (includeToolCalls && turn.tool_calls_json) {
|
|
161
|
+
try {
|
|
162
|
+
const toolCalls = JSON.parse(turn.tool_calls_json) as Array<{ name: string; input_summary: string; is_error: boolean }>;
|
|
163
|
+
if (toolCalls.length > 0) {
|
|
164
|
+
lines.push(`**Tool Calls** (${toolCalls.length}):`);
|
|
165
|
+
for (const tc of toolCalls) {
|
|
166
|
+
const status = tc.is_error ? ' [ERROR]' : '';
|
|
167
|
+
lines.push(` - ${tc.name}: ${tc.input_summary}${status}`);
|
|
168
|
+
}
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
} catch (_e) {
|
|
172
|
+
// Skip invalid JSON
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (turn.tool_call_count > 0) {
|
|
177
|
+
lines.push(`_${turn.tool_call_count} tool calls | ~${turn.prompt_tokens ?? 0} prompt tokens | ~${turn.response_tokens ?? 0} response tokens_`);
|
|
178
|
+
}
|
|
179
|
+
lines.push('---');
|
|
180
|
+
lines.push('');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return text(lines.join('\n'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function handlePromptAnalysis(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
187
|
+
const query = args.query as string | undefined;
|
|
188
|
+
|
|
189
|
+
if (!query) {
|
|
190
|
+
// No FTS query -- show recent turns summary
|
|
191
|
+
const recentTurns = db.prepare(
|
|
192
|
+
'SELECT ct.session_id, ct.turn_number, ct.user_prompt, ct.tool_call_count, ct.response_tokens, ct.created_at FROM conversation_turns ct ORDER BY ct.created_at_epoch DESC LIMIT ?'
|
|
193
|
+
).all((args.limit as number) ?? 20) as Array<{
|
|
194
|
+
session_id: string; turn_number: number; user_prompt: string;
|
|
195
|
+
tool_call_count: number; response_tokens: number | null; created_at: string;
|
|
196
|
+
}>;
|
|
197
|
+
|
|
198
|
+
if (recentTurns.length === 0) {
|
|
199
|
+
return text('No conversation turns recorded yet.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const lines = ['## Recent Prompts', ''];
|
|
203
|
+
lines.push('| Session | Turn | Prompt (truncated) | Tools | Response Tokens | Date |');
|
|
204
|
+
lines.push('|---------|------|--------------------|-------|-----------------|------|');
|
|
205
|
+
|
|
206
|
+
for (const t of recentTurns) {
|
|
207
|
+
lines.push(`| ${t.session_id.slice(0, 8)}... | ${t.turn_number} | ${t.user_prompt.slice(0, 60).replace(/\|/g, '\\|')} | ${t.tool_call_count} | ${t.response_tokens ?? '-'} | ${t.created_at.split('T')[0]} |`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return text(lines.join('\n'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const results = searchConversationTurns(db, query, {
|
|
214
|
+
sessionId: args.session_id as string | undefined,
|
|
215
|
+
dateFrom: args.date_from as string | undefined,
|
|
216
|
+
dateTo: args.date_to as string | undefined,
|
|
217
|
+
minToolCalls: args.min_tool_calls as number | undefined,
|
|
218
|
+
limit: args.limit as number | undefined,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (results.length === 0) {
|
|
222
|
+
return text(`No prompts found matching "${query}".`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const lines = [`## Prompt Search: "${query}" (${results.length} results)`, ''];
|
|
226
|
+
lines.push('| Session | Turn | Prompt (truncated) | Tools | Response Tokens | Date |');
|
|
227
|
+
lines.push('|---------|------|--------------------|-------|-----------------|------|');
|
|
228
|
+
|
|
229
|
+
for (const r of results) {
|
|
230
|
+
lines.push(`| ${r.session_id.slice(0, 8)}... | ${r.turn_number} | ${r.user_prompt.slice(0, 60).replace(/\|/g, '\\|')} | ${r.tool_call_count} | ${r.response_tokens ?? '-'} | ${r.created_at.split('T')[0]} |`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return text(lines.join('\n'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function handleToolPatterns(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
237
|
+
const groupBy = (args.group_by as 'tool' | 'session' | 'day') ?? 'tool';
|
|
238
|
+
const patterns = getToolPatterns(db, {
|
|
239
|
+
sessionId: args.session_id as string | undefined,
|
|
240
|
+
toolName: args.tool_name as string | undefined,
|
|
241
|
+
dateFrom: args.date_from as string | undefined,
|
|
242
|
+
groupBy,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (patterns.length === 0) {
|
|
246
|
+
return text('No tool usage data recorded yet.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const lines = [`## Tool Usage Patterns (grouped by ${groupBy})`, ''];
|
|
250
|
+
|
|
251
|
+
switch (groupBy) {
|
|
252
|
+
case 'tool':
|
|
253
|
+
lines.push('| Tool | Calls | Successes | Failures | Success Rate | Avg Output Size | Avg Input Size |');
|
|
254
|
+
lines.push('|------|-------|-----------|----------|--------------|-----------------|----------------|');
|
|
255
|
+
for (const p of patterns) {
|
|
256
|
+
const total = p.call_count as number;
|
|
257
|
+
const successes = p.successes as number;
|
|
258
|
+
const failures = p.failures as number;
|
|
259
|
+
const rate = total > 0 ? Math.round((successes / total) * 100) : 0;
|
|
260
|
+
lines.push(`| ${p.tool_name} | ${total} | ${successes} | ${failures} | ${rate}% | ${Math.round(p.avg_output_size as number ?? 0)} | ${Math.round(p.avg_input_size as number ?? 0)} |`);
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case 'session':
|
|
264
|
+
lines.push('| Session | Calls | Unique Tools | Successes | Failures | Avg Output Size |');
|
|
265
|
+
lines.push('|---------|-------|--------------|-----------|----------|-----------------|');
|
|
266
|
+
for (const p of patterns) {
|
|
267
|
+
lines.push(`| ${(p.session_id as string).slice(0, 8)}... | ${p.call_count} | ${p.unique_tools} | ${p.successes} | ${p.failures} | ${Math.round(p.avg_output_size as number ?? 0)} |`);
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case 'day':
|
|
271
|
+
lines.push('| Day | Calls | Unique Tools | Successes |');
|
|
272
|
+
lines.push('|-----|-------|--------------|-----------|');
|
|
273
|
+
for (const p of patterns) {
|
|
274
|
+
lines.push(`| ${p.day} | ${p.call_count} | ${p.unique_tools} | ${p.successes} |`);
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return text(lines.join('\n'));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function handleSessionStats(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
283
|
+
const stats = getSessionStats(db, {
|
|
284
|
+
sessionId: args.session_id as string | undefined,
|
|
285
|
+
limit: args.limit as number | undefined,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (stats.length === 0) {
|
|
289
|
+
return text('No session stats available.');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const lines = ['## Session Statistics', ''];
|
|
293
|
+
|
|
294
|
+
if (args.session_id) {
|
|
295
|
+
// Detailed single session view
|
|
296
|
+
const s = stats[0];
|
|
297
|
+
lines.push(`### Session: ${(s.session_id as string).slice(0, 12)}...`);
|
|
298
|
+
lines.push(`- **Status**: ${s.status}`);
|
|
299
|
+
lines.push(`- **Started**: ${s.started_at ?? '-'}`);
|
|
300
|
+
lines.push(`- **Ended**: ${s.ended_at ?? '-'}`);
|
|
301
|
+
lines.push(`- **Turns**: ${s.turn_count}`);
|
|
302
|
+
lines.push(`- **Total Tool Calls**: ${s.total_tool_calls}`);
|
|
303
|
+
lines.push(`- **Prompt Tokens**: ~${s.total_prompt_tokens}`);
|
|
304
|
+
lines.push(`- **Response Tokens**: ~${s.total_response_tokens}`);
|
|
305
|
+
lines.push('');
|
|
306
|
+
|
|
307
|
+
const breakdown = s.tool_breakdown as Array<Record<string, unknown>> | undefined;
|
|
308
|
+
if (breakdown && breakdown.length > 0) {
|
|
309
|
+
lines.push('#### Tool Breakdown');
|
|
310
|
+
lines.push('| Tool | Calls |');
|
|
311
|
+
lines.push('|------|-------|');
|
|
312
|
+
for (const tb of breakdown) {
|
|
313
|
+
lines.push(`| ${tb.tool_name} | ${tb.count} |`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
// Multi-session summary
|
|
318
|
+
lines.push('| Session | Status | Turns | Tool Calls | Prompt Tokens | Response Tokens | Started |');
|
|
319
|
+
lines.push('|---------|--------|-------|------------|---------------|-----------------|---------|');
|
|
320
|
+
for (const s of stats) {
|
|
321
|
+
lines.push(`| ${(s.session_id as string).slice(0, 8)}... | ${s.status} | ${s.turn_count} | ${s.total_tool_calls} | ~${s.total_prompt_tokens} | ~${s.total_response_tokens} | ${(s.started_at as string)?.split('T')[0] ?? '-'} |`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Database size info (P4-001)
|
|
326
|
+
lines.push('');
|
|
327
|
+
lines.push('### Database Size');
|
|
328
|
+
const dbSize = getObservabilityDbSize(db);
|
|
329
|
+
lines.push(`- Conversation turns: ${dbSize.conversation_turns_count}`);
|
|
330
|
+
lines.push(`- Tool call details: ${dbSize.tool_call_details_count}`);
|
|
331
|
+
lines.push(`- Observations: ${dbSize.observations_count}`);
|
|
332
|
+
lines.push(`- Database size: ~${dbSize.estimated_size_mb} MB`);
|
|
333
|
+
|
|
334
|
+
return text(lines.join('\n'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================================
|
|
338
|
+
// Helpers
|
|
339
|
+
// ============================================================
|
|
340
|
+
|
|
341
|
+
function text(content: string): ToolResult {
|
|
342
|
+
return { content: [{ type: 'text', text: content }] };
|
|
343
|
+
}
|