@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,91 @@
|
|
|
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
|
+
// P3-004: UserPromptSubmit Hook
|
|
7
|
+
// Captures user prompts for search and context.
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
import { getMemoryDb, createSession, addUserPrompt, linkSessionToTask, autoDetectTaskId } from '../memory-db.ts';
|
|
11
|
+
|
|
12
|
+
interface HookInput {
|
|
13
|
+
session_id: string;
|
|
14
|
+
transcript_path: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
hook_event_name: string;
|
|
17
|
+
prompt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main(): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
const input = await readStdin();
|
|
23
|
+
const hookInput = JSON.parse(input) as HookInput;
|
|
24
|
+
const { session_id, prompt } = hookInput;
|
|
25
|
+
|
|
26
|
+
if (!prompt || !prompt.trim()) {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const db = getMemoryDb();
|
|
32
|
+
try {
|
|
33
|
+
// 1. Create session if not exists
|
|
34
|
+
const gitBranch = await getGitBranch();
|
|
35
|
+
createSession(db, session_id, { branch: gitBranch });
|
|
36
|
+
|
|
37
|
+
// 2. Scan prompt for plan file references
|
|
38
|
+
const planFileMatch = prompt.match(/([^\s]+docs\/plans\/[^\s]+\.md)/);
|
|
39
|
+
if (planFileMatch) {
|
|
40
|
+
const planFile = planFileMatch[1];
|
|
41
|
+
db.prepare('UPDATE sessions SET plan_file = ? WHERE session_id = ?').run(planFile, session_id);
|
|
42
|
+
|
|
43
|
+
// Auto-detect and link task_id
|
|
44
|
+
const taskId = autoDetectTaskId(planFile);
|
|
45
|
+
if (taskId) {
|
|
46
|
+
linkSessionToTask(db, session_id, taskId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Get current prompt count for this session
|
|
51
|
+
const countResult = db.prepare(
|
|
52
|
+
'SELECT COUNT(*) as count FROM user_prompts WHERE session_id = ?'
|
|
53
|
+
).get(session_id) as { count: number };
|
|
54
|
+
const promptNumber = countResult.count + 1;
|
|
55
|
+
|
|
56
|
+
// 4. Insert prompt
|
|
57
|
+
addUserPrompt(db, session_id, prompt.trim(), promptNumber);
|
|
58
|
+
} finally {
|
|
59
|
+
db.close();
|
|
60
|
+
}
|
|
61
|
+
} catch (_e) {
|
|
62
|
+
// Best-effort: never block Claude Code
|
|
63
|
+
}
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function getGitBranch(): Promise<string | undefined> {
|
|
68
|
+
try {
|
|
69
|
+
const { spawnSync } = await import('child_process');
|
|
70
|
+
const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
});
|
|
74
|
+
if (result.status !== 0 || result.error) return undefined;
|
|
75
|
+
return result.stdout.trim();
|
|
76
|
+
} catch (_e) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readStdin(): Promise<string> {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
let data = '';
|
|
84
|
+
process.stdin.setEncoding('utf-8');
|
|
85
|
+
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
86
|
+
process.stdin.on('end', () => resolve(data));
|
|
87
|
+
setTimeout(() => resolve(data), 3000);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main();
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
5
|
+
import { resolve, dirname, join } from 'path';
|
|
6
|
+
import type Database from 'better-sqlite3';
|
|
7
|
+
import { getResolvedPaths, getProjectRoot } from './config.ts';
|
|
8
|
+
|
|
9
|
+
interface ImportEdge {
|
|
10
|
+
source_file: string;
|
|
11
|
+
target_file: string;
|
|
12
|
+
import_type: 'named' | 'default' | 'namespace' | 'side_effect' | 'dynamic';
|
|
13
|
+
imported_names: string; // JSON array
|
|
14
|
+
line: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ParsedImport {
|
|
18
|
+
type: 'named' | 'default' | 'namespace' | 'side_effect' | 'dynamic';
|
|
19
|
+
names: string[];
|
|
20
|
+
specifier: string;
|
|
21
|
+
line: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse import statements from TypeScript/JavaScript source code.
|
|
26
|
+
*/
|
|
27
|
+
export function parseImports(source: string): ParsedImport[] {
|
|
28
|
+
const imports: ParsedImport[] = [];
|
|
29
|
+
const lines = source.split('\n');
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const line = lines[i];
|
|
33
|
+
const lineNum = i + 1;
|
|
34
|
+
|
|
35
|
+
// Skip comments
|
|
36
|
+
if (line.trimStart().startsWith('//') || line.trimStart().startsWith('*')) continue;
|
|
37
|
+
|
|
38
|
+
// Named imports: import { Foo, Bar } from 'module'
|
|
39
|
+
const namedMatch = line.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
|
|
40
|
+
if (namedMatch) {
|
|
41
|
+
const names = namedMatch[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
|
42
|
+
imports.push({ type: 'named', names, specifier: namedMatch[2], line: lineNum });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Default import: import Foo from 'module'
|
|
47
|
+
const defaultMatch = line.match(/import\s+([A-Z_$][\w$]*)\s+from\s+['"]([^'"]+)['"]/);
|
|
48
|
+
if (defaultMatch) {
|
|
49
|
+
imports.push({ type: 'default', names: [defaultMatch[1]], specifier: defaultMatch[2], line: lineNum });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Default + named: import Foo, { Bar } from 'module'
|
|
54
|
+
const mixedMatch = line.match(/import\s+([A-Z_$][\w$]*)\s*,\s*\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
|
|
55
|
+
if (mixedMatch) {
|
|
56
|
+
const names = [mixedMatch[1], ...mixedMatch[2].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean)];
|
|
57
|
+
imports.push({ type: 'named', names, specifier: mixedMatch[3], line: lineNum });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Namespace import: import * as Foo from 'module'
|
|
62
|
+
const namespaceMatch = line.match(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/);
|
|
63
|
+
if (namespaceMatch) {
|
|
64
|
+
imports.push({ type: 'namespace', names: [namespaceMatch[1]], specifier: namespaceMatch[2], line: lineNum });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Type imports: import type { Foo } from 'module'
|
|
69
|
+
const typeMatch = line.match(/import\s+type\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
|
|
70
|
+
if (typeMatch) {
|
|
71
|
+
const names = typeMatch[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
|
72
|
+
imports.push({ type: 'named', names, specifier: typeMatch[2], line: lineNum });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Side effect import: import 'module'
|
|
77
|
+
const sideEffectMatch = line.match(/^import\s+['"]([^'"]+)['"]/);
|
|
78
|
+
if (sideEffectMatch) {
|
|
79
|
+
imports.push({ type: 'side_effect', names: [], specifier: sideEffectMatch[1], line: lineNum });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Dynamic import: await import('module') or import('module')
|
|
84
|
+
const dynamicMatch = line.match(/(?:await\s+)?import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
85
|
+
if (dynamicMatch) {
|
|
86
|
+
imports.push({ type: 'dynamic', names: [], specifier: dynamicMatch[1], line: lineNum });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return imports;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve an import specifier to an absolute file path.
|
|
96
|
+
* Returns null for external packages (bare imports).
|
|
97
|
+
*/
|
|
98
|
+
export function resolveImportPath(specifier: string, fromFile: string): string | null {
|
|
99
|
+
// Skip external/bare imports (no . or @ prefix)
|
|
100
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('@/')) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let basePath: string;
|
|
105
|
+
|
|
106
|
+
// Handle @/ alias -> src/
|
|
107
|
+
if (specifier.startsWith('@/')) {
|
|
108
|
+
const paths = getResolvedPaths();
|
|
109
|
+
basePath = resolve(paths.pathAlias['@'] ?? paths.srcDir, specifier.slice(2));
|
|
110
|
+
} else {
|
|
111
|
+
// Relative path
|
|
112
|
+
basePath = resolve(dirname(fromFile), specifier);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Try exact path first
|
|
116
|
+
if (existsSync(basePath) && !isDirectory(basePath)) {
|
|
117
|
+
return toRelative(basePath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Try with extensions
|
|
121
|
+
const resolvedPaths = getResolvedPaths();
|
|
122
|
+
for (const ext of resolvedPaths.extensions) {
|
|
123
|
+
const withExt = basePath + ext;
|
|
124
|
+
if (existsSync(withExt)) {
|
|
125
|
+
return toRelative(withExt);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Try index files (if path is a directory or could be)
|
|
130
|
+
for (const indexFile of resolvedPaths.indexFiles) {
|
|
131
|
+
const indexPath = join(basePath, indexFile);
|
|
132
|
+
if (existsSync(indexPath)) {
|
|
133
|
+
return toRelative(indexPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isDirectory(path: string): boolean {
|
|
141
|
+
try {
|
|
142
|
+
return statSync(path).isDirectory();
|
|
143
|
+
} catch {
|
|
144
|
+
// Path doesn't exist or is inaccessible
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toRelative(absPath: string): string {
|
|
150
|
+
const root = getProjectRoot();
|
|
151
|
+
if (absPath.startsWith(root)) {
|
|
152
|
+
return absPath.slice(root.length + 1);
|
|
153
|
+
}
|
|
154
|
+
return absPath;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the full import graph for all files in CodeGraph.
|
|
159
|
+
* Stores results in massu_imports table.
|
|
160
|
+
*/
|
|
161
|
+
export function buildImportIndex(dataDb: Database.Database, codegraphDb: Database.Database): number {
|
|
162
|
+
// Get all files from CodeGraph
|
|
163
|
+
const files = codegraphDb.prepare("SELECT path FROM files WHERE path LIKE 'src/%'").all() as { path: string }[];
|
|
164
|
+
|
|
165
|
+
// Clear existing import edges
|
|
166
|
+
dataDb.exec('DELETE FROM massu_imports');
|
|
167
|
+
|
|
168
|
+
const insertStmt = dataDb.prepare(
|
|
169
|
+
'INSERT INTO massu_imports (source_file, target_file, import_type, imported_names, line) VALUES (?, ?, ?, ?, ?)'
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
let edgeCount = 0;
|
|
173
|
+
const projectRoot = getProjectRoot();
|
|
174
|
+
|
|
175
|
+
const insertMany = dataDb.transaction((edges: ImportEdge[]) => {
|
|
176
|
+
for (const edge of edges) {
|
|
177
|
+
insertStmt.run(edge.source_file, edge.target_file, edge.import_type, edge.imported_names, edge.line);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const batchSize = 500;
|
|
182
|
+
let batch: ImportEdge[] = [];
|
|
183
|
+
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
const absPath = resolve(projectRoot, file.path);
|
|
186
|
+
if (!existsSync(absPath)) continue;
|
|
187
|
+
|
|
188
|
+
let source: string;
|
|
189
|
+
try {
|
|
190
|
+
source = readFileSync(absPath, 'utf-8');
|
|
191
|
+
} catch {
|
|
192
|
+
// Skip unreadable source files
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const imports = parseImports(source);
|
|
197
|
+
|
|
198
|
+
for (const imp of imports) {
|
|
199
|
+
const targetPath = resolveImportPath(imp.specifier, absPath);
|
|
200
|
+
if (!targetPath) continue; // Skip external packages
|
|
201
|
+
|
|
202
|
+
batch.push({
|
|
203
|
+
source_file: file.path,
|
|
204
|
+
target_file: targetPath,
|
|
205
|
+
import_type: imp.type,
|
|
206
|
+
imported_names: JSON.stringify(imp.names),
|
|
207
|
+
line: imp.line,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
edgeCount++;
|
|
211
|
+
|
|
212
|
+
if (batch.length >= batchSize) {
|
|
213
|
+
insertMany(batch);
|
|
214
|
+
batch = [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (batch.length > 0) {
|
|
220
|
+
insertMany(batch);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return edgeCount;
|
|
224
|
+
}
|