@massu/core 1.10.7 → 1.12.0

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 (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
@@ -1,127 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * `massu config upgrade` — migrate a v1 `massu.config.yaml` to schema_version=2.
6
- *
7
- * Flags:
8
- * --rollback Restore massu.config.yaml from massu.config.yaml.bak.
9
- * --ci / --yes Non-interactive; no prompts; detector wins on conflicts.
10
- *
11
- * Safety:
12
- * - Writes .bak of the original before overwriting.
13
- * - Atomic write via writeConfigAtomic (tmp + rename).
14
- * - Idempotent: running on a schema_version=2 config is a no-op.
15
- *
16
- * Exit codes:
17
- * 0 success (migrated, rolled back, or already current)
18
- * 1 config missing / rollback source missing
19
- * 2 parse or write failure
20
- */
21
-
22
- import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'fs';
23
- import { resolve } from 'path';
24
- import { parse as parseYaml } from 'yaml';
25
- import { runDetection } from '../detect/index.ts';
26
- import { computeFingerprint } from '../detect/drift.ts';
27
- import { migrateV1ToV2, type AnyConfig } from '../detect/migrate.ts';
28
- import { renderConfigYaml, writeConfigAtomic } from './init.ts';
29
-
30
- export interface ConfigUpgradeOptions {
31
- rollback?: boolean;
32
- ci?: boolean;
33
- cwd?: string;
34
- silent?: boolean;
35
- }
36
-
37
- export interface ConfigUpgradeResult {
38
- exitCode: 0 | 1 | 2;
39
- action: 'migrated' | 'already-current' | 'rolled-back' | 'none';
40
- message?: string;
41
- }
42
-
43
- export async function runConfigUpgrade(opts: ConfigUpgradeOptions = {}): Promise<ConfigUpgradeResult> {
44
- const cwd = opts.cwd ?? process.cwd();
45
- const configPath = resolve(cwd, 'massu.config.yaml');
46
- const bakPath = `${configPath}.bak`;
47
- const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
48
- const err = opts.silent ? () => {} : (s: string) => process.stderr.write(s);
49
-
50
- if (opts.rollback) {
51
- if (!existsSync(bakPath)) {
52
- const message = `No backup found at ${bakPath}`;
53
- err(message + '\n');
54
- return { exitCode: 1, action: 'none', message };
55
- }
56
- try {
57
- copyFileSync(bakPath, configPath);
58
- unlinkSync(bakPath);
59
- log('Config restored from backup.\n');
60
- return { exitCode: 0, action: 'rolled-back' };
61
- } catch (e) {
62
- const message = `Rollback failed: ${e instanceof Error ? e.message : String(e)}`;
63
- err(message + '\n');
64
- return { exitCode: 2, action: 'none', message };
65
- }
66
- }
67
-
68
- if (!existsSync(configPath)) {
69
- const message = 'massu.config.yaml not found. Run: npx massu init';
70
- err(message + '\n');
71
- return { exitCode: 1, action: 'none', message };
72
- }
73
-
74
- let existing: AnyConfig;
75
- try {
76
- const content = readFileSync(configPath, 'utf-8');
77
- // pattern-scanner-allow: yaml-parse — reason: v1.1.0 config lifecycle command. Same pre-migration constraint as config-refresh: `massu config upgrade` operates on v1 configs that getConfig() Zod-rejects, so raw parse is required.
78
- const parsed = parseYaml(content);
79
- if (!parsed || typeof parsed !== 'object') {
80
- throw new Error('config is not a YAML object');
81
- }
82
- existing = parsed as AnyConfig;
83
- } catch (e) {
84
- const message = `Failed to parse massu.config.yaml: ${e instanceof Error ? e.message : String(e)}`;
85
- err(message + '\n');
86
- return { exitCode: 2, action: 'none', message };
87
- }
88
-
89
- const schemaVersion = existing.schema_version;
90
- if (schemaVersion === 2) {
91
- log('Config is already at schema_version=2; nothing to do.\n');
92
- return { exitCode: 0, action: 'already-current' };
93
- }
94
-
95
- const detection = await runDetection(cwd);
96
- const v2 = migrateV1ToV2(existing, detection);
97
- v2.detection = {
98
- ...(v2.detection as Record<string, unknown> | undefined ?? {}),
99
- fingerprint: computeFingerprint(detection),
100
- };
101
-
102
- // Back up original before any write.
103
- try {
104
- const original = readFileSync(configPath, 'utf-8');
105
- writeFileSync(bakPath, original, 'utf-8');
106
- } catch (e) {
107
- const message = `Failed to write backup: ${e instanceof Error ? e.message : String(e)}`;
108
- err(message + '\n');
109
- return { exitCode: 2, action: 'none', message };
110
- }
111
-
112
- const yamlContent = renderConfigYaml(v2);
113
- const writeRes = writeConfigAtomic(configPath, yamlContent);
114
- if (!writeRes.validated) {
115
- const message = `Failed to write upgraded config: ${writeRes.error}`;
116
- err(message + '\n');
117
- return { exitCode: 2, action: 'none', message };
118
- }
119
-
120
- // Non-interactive mode just proceeds; interactive mode currently has no
121
- // prompt on migrate (the migrator is deterministic and always user-preserving).
122
- // --ci / --yes remain accepted for script-pipeline safety.
123
- void opts.ci;
124
-
125
- log(`Config upgraded to schema_version=2. Backup saved at ${bakPath}\n`);
126
- return { exitCode: 0, action: 'migrated' };
127
- }
@@ -1,485 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * `massu doctor` — Installation health check.
6
- *
7
- * Verifies all components of a Massu AI installation are working:
8
- * 1. massu.config.yaml exists and parses correctly
9
- * 2. .mcp.json has massu entry
10
- * 3. .claude/settings.local.json has hooks config
11
- * 4. All compiled hook files exist (count sourced from lib/hook-registry.ts SoT)
12
- * 5. Knowledge DB exists (.massu/memory.db)
13
- * 6. Memory directory exists (~/.claude/projects/.../memory/)
14
- * 7. Shell hooks wired in settings.local.json
15
- * 8. better-sqlite3 native module loads
16
- * 9. Node.js version >= 18
17
- * 10. Git repository detected
18
- */
19
-
20
- import { existsSync, readFileSync, readdirSync } from 'fs';
21
- import { resolve, dirname } from 'path';
22
- import { fileURLToPath } from 'url';
23
- import { parse as parseYaml } from 'yaml';
24
- import { getConfig, getResolvedPaths } from '../config.ts';
25
- import { getCurrentTier, getLicenseInfo, daysUntilExpiry } from '../license.ts';
26
- import { readSettingsAtPath } from '../lib/settings-local.ts';
27
- import { getExpectedHookFiles } from '../lib/hook-registry.ts';
28
-
29
- const __filename = fileURLToPath(import.meta.url);
30
- const __dirname = dirname(__filename);
31
-
32
- // ============================================================
33
- // Types
34
- // ============================================================
35
-
36
- interface CheckResult {
37
- name: string;
38
- status: 'pass' | 'fail' | 'warn';
39
- detail: string;
40
- }
41
-
42
- // ============================================================
43
- // Hook Files
44
- // ============================================================
45
-
46
- // P-H001 (plan-stage-c-high-batch): drift-fix. EXPECTED_HOOKS now consumes
47
- // the lib/hook-registry.ts SoT; the registry-parity drift-guard test asserts
48
- // this list matches `src/hooks/*.ts` AND `buildHooksConfig()`. Previously
49
- // a hand-maintained list of 11 entries silently drifted from the 16 hooks
50
- // actually registered by installHooks(), letting hook files go missing
51
- // without doctor noticing.
52
- const EXPECTED_HOOKS: readonly string[] = getExpectedHookFiles();
53
-
54
- // ============================================================
55
- // Individual Checks
56
- // ============================================================
57
-
58
- function checkConfig(projectRoot: string): CheckResult {
59
- const configPath = resolve(projectRoot, 'massu.config.yaml');
60
- if (!existsSync(configPath)) {
61
- return { name: 'Configuration', status: 'fail', detail: 'massu.config.yaml not found. Run: npx massu init' };
62
- }
63
-
64
- try {
65
- const content = readFileSync(configPath, 'utf-8');
66
- // pattern-scanner-allow: yaml-parse — reason: `massu doctor` config-integrity diagnostic; runs BEFORE getConfig() to verify the file is well-formed YAML so the user sees a parse error here, not a Zod-validation surprise downstream.
67
- const parsed = parseYaml(content);
68
- if (!parsed || typeof parsed !== 'object') {
69
- return { name: 'Configuration', status: 'fail', detail: 'massu.config.yaml is empty or invalid YAML' };
70
- }
71
- return { name: 'Configuration', status: 'pass', detail: 'massu.config.yaml found and valid' };
72
- } catch (err) {
73
- return { name: 'Configuration', status: 'fail', detail: `massu.config.yaml parse error: ${err instanceof Error ? err.message : String(err)}` };
74
- }
75
- }
76
-
77
- function checkMcpServer(projectRoot: string): CheckResult {
78
- const mcpPath = getResolvedPaths().mcpJsonPath;
79
- if (!existsSync(mcpPath)) {
80
- return { name: 'MCP Server', status: 'fail', detail: '.mcp.json not found. Run: npx massu init' };
81
- }
82
-
83
- try {
84
- const content = JSON.parse(readFileSync(mcpPath, 'utf-8'));
85
- const servers = content.mcpServers ?? {};
86
- if (!servers.massu) {
87
- return { name: 'MCP Server', status: 'fail', detail: 'massu not registered in .mcp.json. Run: npx massu init' };
88
- }
89
- return { name: 'MCP Server', status: 'pass', detail: 'Registered in .mcp.json' };
90
- } catch (err) {
91
- return { name: 'MCP Server', status: 'fail', detail: `.mcp.json parse error: ${err instanceof Error ? err.message : String(err)}` };
92
- }
93
- }
94
-
95
- function checkHooksConfig(projectRoot: string): CheckResult {
96
- const settingsPath = getResolvedPaths().settingsLocalPath;
97
- if (!existsSync(settingsPath)) {
98
- return { name: 'Hooks Config', status: 'fail', detail: '.claude/settings.local.json not found. Run: npx massu init' };
99
- }
100
-
101
- try {
102
- const content = readSettingsAtPath(settingsPath);
103
- if (!content.hooks) {
104
- return { name: 'Hooks Config', status: 'fail', detail: 'No hooks configured. Run: npx massu install-hooks' };
105
- }
106
-
107
- // Count configured hooks
108
- let hookCount = 0;
109
- for (const groups of Object.values(content.hooks)) {
110
- if (Array.isArray(groups)) {
111
- for (const group of groups) {
112
- const g = group as { hooks?: unknown[] };
113
- if (Array.isArray(g.hooks)) {
114
- hookCount += g.hooks.length;
115
- }
116
- }
117
- }
118
- }
119
-
120
- if (hookCount === 0) {
121
- return { name: 'Hooks Config', status: 'fail', detail: 'Hooks section exists but no hooks configured' };
122
- }
123
-
124
- return { name: 'Hooks Config', status: 'pass', detail: `${hookCount} hooks configured` };
125
- } catch (err) {
126
- return { name: 'Hooks Config', status: 'fail', detail: `settings.local.json parse error: ${err instanceof Error ? err.message : String(err)}` };
127
- }
128
- }
129
-
130
- function checkHookFiles(projectRoot: string): CheckResult {
131
- // Check node_modules path first
132
- const nodeModulesHooksDir = resolve(projectRoot, 'node_modules/@massu/core/dist/hooks');
133
- let hooksDir = nodeModulesHooksDir;
134
-
135
- if (!existsSync(nodeModulesHooksDir)) {
136
- // Check relative to this file (development mode)
137
- const devHooksDir = resolve(__dirname, '../../dist/hooks');
138
- if (existsSync(devHooksDir)) {
139
- hooksDir = devHooksDir;
140
- } else {
141
- return { name: 'Hook Files', status: 'fail', detail: 'Compiled hooks not found. Run: npm install @massu/core' };
142
- }
143
- }
144
-
145
- const missing: string[] = [];
146
- for (const hookFile of EXPECTED_HOOKS) {
147
- if (!existsSync(resolve(hooksDir, hookFile))) {
148
- missing.push(hookFile);
149
- }
150
- }
151
-
152
- if (missing.length > 0) {
153
- return { name: 'Hook Files', status: 'fail', detail: `Missing hooks: ${missing.join(', ')}` };
154
- }
155
-
156
- return { name: 'Hook Files', status: 'pass', detail: `${EXPECTED_HOOKS.length}/${EXPECTED_HOOKS.length} compiled hooks present` };
157
- }
158
-
159
- async function checkNativeModules(): Promise<CheckResult> {
160
- try {
161
- await import('better-sqlite3');
162
- return { name: 'Native Modules', status: 'pass', detail: 'better-sqlite3 loads correctly' };
163
- } catch (err) {
164
- return { name: 'Native Modules', status: 'fail', detail: `better-sqlite3 failed: ${err instanceof Error ? err.message : String(err)}. Try: npm rebuild better-sqlite3` };
165
- }
166
- }
167
-
168
- function checkNodeVersion(): CheckResult {
169
- const version = process.versions.node;
170
- const major = parseInt(version.split('.')[0], 10);
171
-
172
- if (major >= 18) {
173
- return { name: 'Node.js', status: 'pass', detail: `v${version} (>= 18 required)` };
174
- }
175
-
176
- return { name: 'Node.js', status: 'fail', detail: `v${version} — Node.js 18+ is required` };
177
- }
178
-
179
- async function checkGitRepo(projectRoot: string): Promise<CheckResult> {
180
- const gitDir = resolve(projectRoot, '.git');
181
- if (!existsSync(gitDir)) {
182
- return { name: 'Git Repository', status: 'warn', detail: 'Not a git repository (optional but recommended)' };
183
- }
184
-
185
- try {
186
- const { spawnSync } = await import('child_process');
187
- const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
188
- encoding: 'utf-8',
189
- timeout: 5000,
190
- cwd: projectRoot,
191
- });
192
- const branch = result.stdout?.trim() ?? 'unknown';
193
- return { name: 'Git Repository', status: 'pass', detail: `Detected (branch: ${branch})` };
194
- } catch {
195
- return { name: 'Git Repository', status: 'pass', detail: 'Detected' };
196
- }
197
- }
198
-
199
- function checkKnowledgeDb(projectRoot: string): CheckResult {
200
- // Knowledge DB is the memory DB
201
- const knowledgeDbPath = getResolvedPaths().memoryDbPath;
202
- if (!existsSync(knowledgeDbPath)) {
203
- return {
204
- name: 'Knowledge DB',
205
- status: 'warn',
206
- detail: '.massu/memory.db not found (will auto-create on first session)',
207
- };
208
- }
209
- return { name: 'Knowledge DB', status: 'pass', detail: '.massu/memory.db exists' };
210
- }
211
-
212
- function checkMemoryDir(_projectRoot: string): CheckResult {
213
- // Memory dir: ~/.claude/projects/-<encoded-root>/memory/ (resolved via config)
214
- const memoryDir = getResolvedPaths().memoryDir;
215
- if (!existsSync(memoryDir)) {
216
- return {
217
- name: 'Memory Directory',
218
- status: 'warn',
219
- detail: 'Memory directory not found. Run: npx massu init',
220
- };
221
- }
222
- return { name: 'Memory Directory', status: 'pass', detail: `Memory directory exists` };
223
- }
224
-
225
- function checkShellHooksWired(_projectRoot: string): CheckResult {
226
- // Verify that .claude/settings.local.json has hooks configured (shell hooks are wired)
227
- const settingsPath = getResolvedPaths().settingsLocalPath;
228
- if (!existsSync(settingsPath)) {
229
- return {
230
- name: 'Shell Hooks',
231
- status: 'fail',
232
- detail: 'settings.local.json not found. Run: npx massu install-hooks',
233
- };
234
- }
235
-
236
- try {
237
- const content = readSettingsAtPath(settingsPath);
238
- const hooks = (content.hooks ?? {}) as Record<string, unknown>;
239
- const hasSessionStart = Array.isArray(hooks.SessionStart) && hooks.SessionStart.length > 0;
240
- const hasPreToolUse = Array.isArray(hooks.PreToolUse) && hooks.PreToolUse.length > 0;
241
- if (!hasSessionStart && !hasPreToolUse) {
242
- return {
243
- name: 'Shell Hooks',
244
- status: 'fail',
245
- detail: 'No lifecycle hooks wired. Run: npx massu install-hooks',
246
- };
247
- }
248
- return { name: 'Shell Hooks', status: 'pass', detail: 'Lifecycle hooks wired in settings.local.json' };
249
- } catch (err) {
250
- return {
251
- name: 'Shell Hooks',
252
- status: 'fail',
253
- detail: `settings.local.json parse error: ${err instanceof Error ? err.message : String(err)}`,
254
- };
255
- }
256
- }
257
-
258
- async function checkLicenseStatus(): Promise<CheckResult> {
259
- try {
260
- const tier = await getCurrentTier();
261
- const info = await getLicenseInfo();
262
-
263
- if (tier === 'free' && !info.validUntil) {
264
- return { name: 'License', status: 'pass', detail: 'Free (no API key configured)' };
265
- }
266
-
267
- const days = await daysUntilExpiry();
268
- if (days >= 0 && info.validUntil) {
269
- return {
270
- name: 'License',
271
- status: 'pass',
272
- detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid until ${info.validUntil})`,
273
- };
274
- }
275
-
276
- return {
277
- name: 'License',
278
- status: 'pass',
279
- detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid)`,
280
- };
281
- } catch (err) {
282
- return {
283
- name: 'License',
284
- status: 'warn',
285
- detail: `Could not check license: ${err instanceof Error ? err.message : String(err)}`,
286
- };
287
- }
288
- }
289
-
290
- function checkPythonHealth(projectRoot: string): CheckResult | null {
291
- const config = getConfig();
292
- if (!config.python?.root) return null;
293
-
294
- const pythonRoot = resolve(projectRoot, config.python.root);
295
- if (!existsSync(pythonRoot)) {
296
- return {
297
- name: 'Python',
298
- status: 'fail',
299
- detail: `Python root directory not found: ${config.python.root}`,
300
- };
301
- }
302
-
303
- // Count .py files recursively (shallow scan for performance)
304
- let pyFileCount = 0;
305
- let routeCount = 0;
306
- let modelCount = 0;
307
- const initPyMissing: string[] = [];
308
-
309
- function scanDir(dir: string, depth: number): void {
310
- if (depth > 5) return;
311
- try {
312
- const entries = readdirSync(dir, { withFileTypes: true });
313
- for (const entry of entries) {
314
- if (entry.isDirectory()) {
315
- const excludeDirs = config.python?.exclude_dirs || ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'];
316
- if (!excludeDirs.includes(entry.name)) {
317
- const subdir = resolve(dir, entry.name);
318
- // Check for __init__.py in package directories
319
- if (depth <= 2 && !existsSync(resolve(subdir, '__init__.py'))) {
320
- // Only flag directories that contain .py files
321
- try {
322
- const subEntries = readdirSync(subdir);
323
- if (subEntries.some(f => f.endsWith('.py') && f !== '__init__.py')) {
324
- initPyMissing.push(entry.name);
325
- }
326
- } catch { /* skip */ }
327
- }
328
- scanDir(subdir, depth + 1);
329
- }
330
- } else if (entry.name.endsWith('.py')) {
331
- pyFileCount++;
332
- // Rough heuristic for routes and models
333
- if (entry.name === 'routes.py' || entry.name === 'router.py' || entry.name === 'endpoints.py') {
334
- routeCount++;
335
- }
336
- if (entry.name === 'models.py' || entry.name === 'model.py' || entry.name === 'schemas.py') {
337
- modelCount++;
338
- }
339
- }
340
- }
341
- } catch { /* skip unreadable dirs */ }
342
- }
343
-
344
- scanDir(pythonRoot, 0);
345
-
346
- if (pyFileCount === 0) {
347
- return {
348
- name: 'Python',
349
- status: 'warn',
350
- detail: `Python root ${config.python.root} exists but no .py files found`,
351
- };
352
- }
353
-
354
- const parts: string[] = [`${pyFileCount} .py files`];
355
- if (routeCount > 0) parts.push(`${routeCount} route files`);
356
- if (modelCount > 0) parts.push(`${modelCount} model files`);
357
- if (initPyMissing.length > 0) {
358
- parts.push(`missing __init__.py: ${initPyMissing.slice(0, 3).join(', ')}${initPyMissing.length > 3 ? '...' : ''}`);
359
- return {
360
- name: 'Python',
361
- status: 'warn',
362
- detail: parts.join(', '),
363
- };
364
- }
365
-
366
- return {
367
- name: 'Python',
368
- status: 'pass',
369
- detail: parts.join(', '),
370
- };
371
- }
372
-
373
- // ============================================================
374
- // Main Doctor Flow
375
- // ============================================================
376
-
377
- export async function runDoctor(): Promise<void> {
378
- const projectRoot = process.cwd();
379
-
380
- console.log('');
381
- console.log('Massu AI Health Check');
382
- console.log('=====================');
383
- console.log('');
384
-
385
- const checks: CheckResult[] = [
386
- checkConfig(projectRoot),
387
- checkMcpServer(projectRoot),
388
- checkHooksConfig(projectRoot),
389
- checkHookFiles(projectRoot),
390
- checkKnowledgeDb(projectRoot),
391
- checkMemoryDir(projectRoot),
392
- checkShellHooksWired(projectRoot),
393
- await checkNativeModules(),
394
- checkNodeVersion(),
395
- await checkGitRepo(projectRoot),
396
- await checkLicenseStatus(),
397
- ];
398
-
399
- // Add Python health check if configured
400
- const pythonCheck = checkPythonHealth(projectRoot);
401
- if (pythonCheck) checks.push(pythonCheck);
402
-
403
- let passed = 0;
404
- let failed = 0;
405
- let warned = 0;
406
-
407
- for (const check of checks) {
408
- const icon = check.status === 'pass' ? '\u2713' : check.status === 'warn' ? '!' : '\u2717';
409
- const pad = check.name.padEnd(20);
410
- console.log(` ${icon} ${pad} ${check.detail}`);
411
-
412
- if (check.status === 'pass') passed++;
413
- else if (check.status === 'fail') failed++;
414
- else warned++;
415
- }
416
-
417
- console.log('');
418
-
419
- if (failed === 0) {
420
- const total = passed + warned;
421
- console.log(`Status: HEALTHY (${passed}/${total} checks passed${warned > 0 ? `, ${warned} warnings` : ''})`);
422
- } else {
423
- console.log(`Status: UNHEALTHY (${failed} check${failed > 1 ? 's' : ''} failed)`);
424
- console.log('');
425
- console.log('Fix issues above, then run: npx massu doctor');
426
- }
427
- console.log('');
428
-
429
- process.exit(failed > 0 ? 1 : 0);
430
- }
431
-
432
- // ============================================================
433
- // Validate Config Command
434
- // ============================================================
435
-
436
- export async function runValidateConfig(): Promise<void> {
437
- const projectRoot = process.cwd();
438
- const configPath = resolve(projectRoot, 'massu.config.yaml');
439
-
440
- if (!existsSync(configPath)) {
441
- console.error('Error: massu.config.yaml not found in current directory');
442
- console.error('Run: npx massu init');
443
- process.exit(1);
444
- return;
445
- }
446
-
447
- try {
448
- const content = readFileSync(configPath, 'utf-8');
449
- // pattern-scanner-allow: yaml-parse — reason: `massu doctor` deep-validation pass; same diagnostic-mode reasoning as the upper checkConfig() — surfaces YAML errors with operator-friendly stderr before any getConfig() call.
450
- const parsed = parseYaml(content);
451
-
452
- if (!parsed || typeof parsed !== 'object') {
453
- console.error('Error: massu.config.yaml is empty or not a valid YAML object');
454
- process.exit(1);
455
- return;
456
- }
457
-
458
- // Check required fields
459
- const warnings: string[] = [];
460
-
461
- if (!parsed.project?.name) {
462
- warnings.push('Missing project.name (will default to "my-project")');
463
- }
464
- if (!parsed.toolPrefix) {
465
- warnings.push('Missing toolPrefix (will default to "massu")');
466
- }
467
- if (!parsed.framework?.type) {
468
- warnings.push('Missing framework.type (will default to "typescript")');
469
- }
470
-
471
- console.log('');
472
- if (warnings.length === 0) {
473
- console.log('massu.config.yaml is valid');
474
- } else {
475
- console.log('massu.config.yaml parsed successfully with warnings:');
476
- for (const w of warnings) {
477
- console.log(` ! ${w}`);
478
- }
479
- }
480
- console.log('');
481
- } catch (err) {
482
- console.error(`Error parsing massu.config.yaml: ${err instanceof Error ? err.message : String(err)}`);
483
- process.exit(1);
484
- }
485
- }