@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
@@ -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
+ }