@massu/core 0.1.1 → 0.4.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 (151) hide show
  1. package/commands/_shared-preamble.md +76 -0
  2. package/commands/massu-audit-deps.md +211 -0
  3. package/commands/massu-changelog.md +174 -0
  4. package/commands/massu-cleanup.md +315 -0
  5. package/commands/massu-commit.md +481 -0
  6. package/commands/massu-create-plan.md +752 -0
  7. package/commands/massu-dead-code.md +131 -0
  8. package/commands/massu-debug.md +484 -0
  9. package/commands/massu-deploy.md +91 -0
  10. package/commands/massu-deps.md +374 -0
  11. package/commands/massu-doc-gen.md +279 -0
  12. package/commands/massu-docs.md +364 -0
  13. package/commands/massu-estimate.md +313 -0
  14. package/commands/massu-golden-path.md +973 -0
  15. package/commands/massu-guide.md +167 -0
  16. package/commands/massu-hotfix.md +480 -0
  17. package/commands/massu-loop-playwright.md +837 -0
  18. package/commands/massu-loop.md +775 -0
  19. package/commands/massu-new-feature.md +511 -0
  20. package/commands/massu-parity.md +214 -0
  21. package/commands/massu-plan.md +456 -0
  22. package/commands/massu-push-light.md +207 -0
  23. package/commands/massu-push.md +434 -0
  24. package/commands/massu-refactor.md +410 -0
  25. package/commands/massu-release.md +363 -0
  26. package/commands/massu-review.md +238 -0
  27. package/commands/massu-simplify.md +281 -0
  28. package/commands/massu-status.md +278 -0
  29. package/commands/massu-tdd.md +201 -0
  30. package/commands/massu-test.md +516 -0
  31. package/commands/massu-verify-playwright.md +281 -0
  32. package/commands/massu-verify.md +667 -0
  33. package/dist/cli.js +7772 -3140
  34. package/dist/hooks/cost-tracker.js +103 -40
  35. package/dist/hooks/post-edit-context.js +74 -8
  36. package/dist/hooks/post-tool-use.js +268 -106
  37. package/dist/hooks/pre-compact.js +167 -43
  38. package/dist/hooks/pre-delete-check.js +159 -42
  39. package/dist/hooks/quality-event.js +103 -40
  40. package/dist/hooks/security-gate.js +29 -0
  41. package/dist/hooks/session-end.js +143 -84
  42. package/dist/hooks/session-start.js +186 -49
  43. package/dist/hooks/user-prompt.js +189 -43
  44. package/package.json +10 -15
  45. package/src/adr-generator.ts +9 -2
  46. package/src/analytics.ts +9 -3
  47. package/src/audit-trail.ts +10 -3
  48. package/src/backfill-sessions.ts +5 -4
  49. package/src/cli.ts +6 -0
  50. package/src/cloud-sync.ts +14 -18
  51. package/src/commands/doctor.ts +193 -6
  52. package/src/commands/init.ts +230 -5
  53. package/src/commands/install-commands.ts +137 -0
  54. package/src/config.ts +68 -2
  55. package/src/cost-tracker.ts +11 -6
  56. package/src/db.ts +115 -2
  57. package/src/dependency-scorer.ts +9 -2
  58. package/src/docs-tools.ts +21 -16
  59. package/src/hooks/post-edit-context.ts +4 -4
  60. package/src/hooks/post-tool-use.ts +130 -0
  61. package/src/hooks/pre-compact.ts +23 -1
  62. package/src/hooks/pre-delete-check.ts +92 -4
  63. package/src/hooks/security-gate.ts +32 -0
  64. package/src/hooks/session-end.ts +3 -3
  65. package/src/hooks/session-start.ts +99 -6
  66. package/src/hooks/user-prompt.ts +46 -1
  67. package/src/import-resolver.ts +2 -1
  68. package/src/knowledge-db.ts +169 -0
  69. package/src/knowledge-indexer.ts +704 -0
  70. package/src/knowledge-tools.ts +1413 -0
  71. package/src/license.ts +482 -0
  72. package/src/memory-db.ts +1364 -23
  73. package/src/memory-tools.ts +14 -15
  74. package/src/observability-tools.ts +13 -2
  75. package/src/observation-extractor.ts +11 -4
  76. package/src/page-deps.ts +3 -2
  77. package/src/prompt-analyzer.ts +9 -2
  78. package/src/python/coupling-detector.ts +124 -0
  79. package/src/python/domain-enforcer.ts +83 -0
  80. package/src/python/impact-analyzer.ts +95 -0
  81. package/src/python/import-parser.ts +244 -0
  82. package/src/python/import-resolver.ts +135 -0
  83. package/src/python/migration-indexer.ts +115 -0
  84. package/src/python/migration-parser.ts +332 -0
  85. package/src/python/model-indexer.ts +70 -0
  86. package/src/python/model-parser.ts +279 -0
  87. package/src/python/route-indexer.ts +58 -0
  88. package/src/python/route-parser.ts +317 -0
  89. package/src/python-tools.ts +629 -0
  90. package/src/regression-detector.ts +9 -3
  91. package/src/security-scorer.ts +9 -2
  92. package/src/sentinel-db.ts +45 -89
  93. package/src/sentinel-tools.ts +8 -11
  94. package/src/server.ts +29 -7
  95. package/src/session-archiver.ts +4 -5
  96. package/src/team-knowledge.ts +9 -2
  97. package/src/tools.ts +1032 -44
  98. package/src/validate-features-runner.ts +0 -1
  99. package/src/validation-engine.ts +9 -2
  100. package/README.md +0 -40
  101. package/dist/server.js +0 -7008
  102. package/src/__tests__/adr-generator.test.ts +0 -260
  103. package/src/__tests__/analytics.test.ts +0 -282
  104. package/src/__tests__/audit-trail.test.ts +0 -382
  105. package/src/__tests__/backfill-sessions.test.ts +0 -690
  106. package/src/__tests__/cli.test.ts +0 -290
  107. package/src/__tests__/cloud-sync.test.ts +0 -261
  108. package/src/__tests__/config-sections.test.ts +0 -359
  109. package/src/__tests__/config.test.ts +0 -732
  110. package/src/__tests__/cost-tracker.test.ts +0 -348
  111. package/src/__tests__/db.test.ts +0 -177
  112. package/src/__tests__/dependency-scorer.test.ts +0 -325
  113. package/src/__tests__/docs-integration.test.ts +0 -178
  114. package/src/__tests__/docs-tools.test.ts +0 -199
  115. package/src/__tests__/domains.test.ts +0 -236
  116. package/src/__tests__/hooks.test.ts +0 -221
  117. package/src/__tests__/import-resolver.test.ts +0 -95
  118. package/src/__tests__/integration/path-traversal.test.ts +0 -134
  119. package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
  120. package/src/__tests__/integration/tool-registration.test.ts +0 -146
  121. package/src/__tests__/memory-db.test.ts +0 -404
  122. package/src/__tests__/memory-enhancements.test.ts +0 -316
  123. package/src/__tests__/memory-tools.test.ts +0 -199
  124. package/src/__tests__/middleware-tree.test.ts +0 -177
  125. package/src/__tests__/observability-tools.test.ts +0 -595
  126. package/src/__tests__/observability.test.ts +0 -437
  127. package/src/__tests__/observation-extractor.test.ts +0 -167
  128. package/src/__tests__/page-deps.test.ts +0 -60
  129. package/src/__tests__/prompt-analyzer.test.ts +0 -298
  130. package/src/__tests__/regression-detector.test.ts +0 -295
  131. package/src/__tests__/rules.test.ts +0 -87
  132. package/src/__tests__/schema-mapper.test.ts +0 -29
  133. package/src/__tests__/security-scorer.test.ts +0 -238
  134. package/src/__tests__/security-utils.test.ts +0 -175
  135. package/src/__tests__/sentinel-db.test.ts +0 -491
  136. package/src/__tests__/sentinel-scanner.test.ts +0 -750
  137. package/src/__tests__/sentinel-tools.test.ts +0 -324
  138. package/src/__tests__/sentinel-types.test.ts +0 -750
  139. package/src/__tests__/server.test.ts +0 -452
  140. package/src/__tests__/session-archiver.test.ts +0 -524
  141. package/src/__tests__/session-state-generator.test.ts +0 -900
  142. package/src/__tests__/team-knowledge.test.ts +0 -327
  143. package/src/__tests__/tools.test.ts +0 -340
  144. package/src/__tests__/transcript-parser.test.ts +0 -195
  145. package/src/__tests__/trpc-index.test.ts +0 -25
  146. package/src/__tests__/validate-features-runner.test.ts +0 -517
  147. package/src/__tests__/validation-engine.test.ts +0 -300
  148. package/src/core-tools.ts +0 -685
  149. package/src/memory-queries.ts +0 -804
  150. package/src/memory-schema.ts +0 -546
  151. package/src/tool-helpers.ts +0 -41
@@ -9,15 +9,20 @@
9
9
  * 2. .mcp.json has massu entry
10
10
  * 3. .claude/settings.local.json has hooks config
11
11
  * 4. All 11 compiled hook files exist
12
- * 5. better-sqlite3 native module loads
13
- * 6. Node.js version >= 18
14
- * 7. Git repository detected
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
15
18
  */
16
19
 
17
- import { existsSync, readFileSync } from 'fs';
20
+ import { existsSync, readFileSync, readdirSync } from 'fs';
18
21
  import { resolve, dirname } from 'path';
19
22
  import { fileURLToPath } from 'url';
20
23
  import { parse as parseYaml } from 'yaml';
24
+ import { getConfig, getResolvedPaths } from '../config.ts';
25
+ import { getCurrentTier, getLicenseInfo, daysUntilExpiry } from '../license.ts';
21
26
 
22
27
  const __filename = fileURLToPath(import.meta.url);
23
28
  const __dirname = dirname(__filename);
@@ -73,7 +78,7 @@ function checkConfig(projectRoot: string): CheckResult {
73
78
  }
74
79
 
75
80
  function checkMcpServer(projectRoot: string): CheckResult {
76
- const mcpPath = resolve(projectRoot, '.mcp.json');
81
+ const mcpPath = getResolvedPaths().mcpJsonPath;
77
82
  if (!existsSync(mcpPath)) {
78
83
  return { name: 'MCP Server', status: 'fail', detail: '.mcp.json not found. Run: npx massu init' };
79
84
  }
@@ -91,7 +96,7 @@ function checkMcpServer(projectRoot: string): CheckResult {
91
96
  }
92
97
 
93
98
  function checkHooksConfig(projectRoot: string): CheckResult {
94
- const settingsPath = resolve(projectRoot, '.claude/settings.local.json');
99
+ const settingsPath = getResolvedPaths().settingsLocalPath;
95
100
  if (!existsSync(settingsPath)) {
96
101
  return { name: 'Hooks Config', status: 'fail', detail: '.claude/settings.local.json not found. Run: npx massu init' };
97
102
  }
@@ -194,6 +199,180 @@ async function checkGitRepo(projectRoot: string): Promise<CheckResult> {
194
199
  }
195
200
  }
196
201
 
202
+ function checkKnowledgeDb(projectRoot: string): CheckResult {
203
+ // Knowledge DB is the memory DB
204
+ const knowledgeDbPath = getResolvedPaths().memoryDbPath;
205
+ if (!existsSync(knowledgeDbPath)) {
206
+ return {
207
+ name: 'Knowledge DB',
208
+ status: 'warn',
209
+ detail: '.massu/memory.db not found (will auto-create on first session)',
210
+ };
211
+ }
212
+ return { name: 'Knowledge DB', status: 'pass', detail: '.massu/memory.db exists' };
213
+ }
214
+
215
+ function checkMemoryDir(_projectRoot: string): CheckResult {
216
+ // Memory dir: ~/.claude/projects/-<encoded-root>/memory/ (resolved via config)
217
+ const memoryDir = getResolvedPaths().memoryDir;
218
+ if (!existsSync(memoryDir)) {
219
+ return {
220
+ name: 'Memory Directory',
221
+ status: 'warn',
222
+ detail: 'Memory directory not found. Run: npx massu init',
223
+ };
224
+ }
225
+ return { name: 'Memory Directory', status: 'pass', detail: `Memory directory exists` };
226
+ }
227
+
228
+ function checkShellHooksWired(_projectRoot: string): CheckResult {
229
+ // Verify that .claude/settings.local.json has hooks configured (shell hooks are wired)
230
+ const settingsPath = getResolvedPaths().settingsLocalPath;
231
+ if (!existsSync(settingsPath)) {
232
+ return {
233
+ name: 'Shell Hooks',
234
+ status: 'fail',
235
+ detail: 'settings.local.json not found. Run: npx massu install-hooks',
236
+ };
237
+ }
238
+
239
+ try {
240
+ const content = JSON.parse(readFileSync(settingsPath, 'utf-8'));
241
+ const hooks = content.hooks ?? {};
242
+ const hasSessionStart = Array.isArray(hooks.SessionStart) && hooks.SessionStart.length > 0;
243
+ const hasPreToolUse = Array.isArray(hooks.PreToolUse) && hooks.PreToolUse.length > 0;
244
+ if (!hasSessionStart && !hasPreToolUse) {
245
+ return {
246
+ name: 'Shell Hooks',
247
+ status: 'fail',
248
+ detail: 'No lifecycle hooks wired. Run: npx massu install-hooks',
249
+ };
250
+ }
251
+ return { name: 'Shell Hooks', status: 'pass', detail: 'Lifecycle hooks wired in settings.local.json' };
252
+ } catch (err) {
253
+ return {
254
+ name: 'Shell Hooks',
255
+ status: 'fail',
256
+ detail: `settings.local.json parse error: ${err instanceof Error ? err.message : String(err)}`,
257
+ };
258
+ }
259
+ }
260
+
261
+ async function checkLicenseStatus(): Promise<CheckResult> {
262
+ try {
263
+ const tier = await getCurrentTier();
264
+ const info = await getLicenseInfo();
265
+
266
+ if (tier === 'free' && !info.validUntil) {
267
+ return { name: 'License', status: 'pass', detail: 'Free (no API key configured)' };
268
+ }
269
+
270
+ const days = await daysUntilExpiry();
271
+ if (days >= 0 && info.validUntil) {
272
+ return {
273
+ name: 'License',
274
+ status: 'pass',
275
+ detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid until ${info.validUntil})`,
276
+ };
277
+ }
278
+
279
+ return {
280
+ name: 'License',
281
+ status: 'pass',
282
+ detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid)`,
283
+ };
284
+ } catch (err) {
285
+ return {
286
+ name: 'License',
287
+ status: 'warn',
288
+ detail: `Could not check license: ${err instanceof Error ? err.message : String(err)}`,
289
+ };
290
+ }
291
+ }
292
+
293
+ function checkPythonHealth(projectRoot: string): CheckResult | null {
294
+ const config = getConfig();
295
+ if (!config.python?.root) return null;
296
+
297
+ const pythonRoot = resolve(projectRoot, config.python.root);
298
+ if (!existsSync(pythonRoot)) {
299
+ return {
300
+ name: 'Python',
301
+ status: 'fail',
302
+ detail: `Python root directory not found: ${config.python.root}`,
303
+ };
304
+ }
305
+
306
+ // Count .py files recursively (shallow scan for performance)
307
+ let pyFileCount = 0;
308
+ let routeCount = 0;
309
+ let modelCount = 0;
310
+ const initPyMissing: string[] = [];
311
+
312
+ function scanDir(dir: string, depth: number): void {
313
+ if (depth > 5) return;
314
+ try {
315
+ const entries = readdirSync(dir, { withFileTypes: true });
316
+ for (const entry of entries) {
317
+ if (entry.isDirectory()) {
318
+ const excludeDirs = config.python?.exclude_dirs || ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'];
319
+ if (!excludeDirs.includes(entry.name)) {
320
+ const subdir = resolve(dir, entry.name);
321
+ // Check for __init__.py in package directories
322
+ if (depth <= 2 && !existsSync(resolve(subdir, '__init__.py'))) {
323
+ // Only flag directories that contain .py files
324
+ try {
325
+ const subEntries = readdirSync(subdir);
326
+ if (subEntries.some(f => f.endsWith('.py') && f !== '__init__.py')) {
327
+ initPyMissing.push(entry.name);
328
+ }
329
+ } catch { /* skip */ }
330
+ }
331
+ scanDir(subdir, depth + 1);
332
+ }
333
+ } else if (entry.name.endsWith('.py')) {
334
+ pyFileCount++;
335
+ // Rough heuristic for routes and models
336
+ if (entry.name === 'routes.py' || entry.name === 'router.py' || entry.name === 'endpoints.py') {
337
+ routeCount++;
338
+ }
339
+ if (entry.name === 'models.py' || entry.name === 'model.py' || entry.name === 'schemas.py') {
340
+ modelCount++;
341
+ }
342
+ }
343
+ }
344
+ } catch { /* skip unreadable dirs */ }
345
+ }
346
+
347
+ scanDir(pythonRoot, 0);
348
+
349
+ if (pyFileCount === 0) {
350
+ return {
351
+ name: 'Python',
352
+ status: 'warn',
353
+ detail: `Python root ${config.python.root} exists but no .py files found`,
354
+ };
355
+ }
356
+
357
+ const parts: string[] = [`${pyFileCount} .py files`];
358
+ if (routeCount > 0) parts.push(`${routeCount} route files`);
359
+ if (modelCount > 0) parts.push(`${modelCount} model files`);
360
+ if (initPyMissing.length > 0) {
361
+ parts.push(`missing __init__.py: ${initPyMissing.slice(0, 3).join(', ')}${initPyMissing.length > 3 ? '...' : ''}`);
362
+ return {
363
+ name: 'Python',
364
+ status: 'warn',
365
+ detail: parts.join(', '),
366
+ };
367
+ }
368
+
369
+ return {
370
+ name: 'Python',
371
+ status: 'pass',
372
+ detail: parts.join(', '),
373
+ };
374
+ }
375
+
197
376
  // ============================================================
198
377
  // Main Doctor Flow
199
378
  // ============================================================
@@ -211,11 +390,19 @@ export async function runDoctor(): Promise<void> {
211
390
  checkMcpServer(projectRoot),
212
391
  checkHooksConfig(projectRoot),
213
392
  checkHookFiles(projectRoot),
393
+ checkKnowledgeDb(projectRoot),
394
+ checkMemoryDir(projectRoot),
395
+ checkShellHooksWired(projectRoot),
214
396
  await checkNativeModules(),
215
397
  checkNodeVersion(),
216
398
  await checkGitRepo(projectRoot),
399
+ await checkLicenseStatus(),
217
400
  ];
218
401
 
402
+ // Add Python health check if configured
403
+ const pythonCheck = checkPythonHealth(projectRoot);
404
+ if (pythonCheck) checks.push(pythonCheck);
405
+
219
406
  let passed = 0;
220
407
  let failed = 0;
221
408
  let warned = 0;
@@ -8,16 +8,21 @@
8
8
  * 2. Generates massu.config.yaml (or preserves existing)
9
9
  * 3. Registers MCP server in .mcp.json (creates or merges)
10
10
  * 4. Installs all 11 hooks in .claude/settings.local.json
11
- * 5. Prints success summary
11
+ * 5. Installs slash commands into .claude/commands/
12
+ * 6. Initializes memory directory
13
+ * 7. Prints success summary
12
14
  */
13
15
 
14
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
15
17
  import { resolve, basename, dirname } from 'path';
16
18
  import { fileURLToPath } from 'url';
19
+ import { homedir } from 'os';
17
20
 
18
21
  const __filename = fileURLToPath(import.meta.url);
19
22
  const __dirname = dirname(__filename);
20
23
  import { stringify as yamlStringify } from 'yaml';
24
+ import { getConfig } from '../config.ts';
25
+ import { installCommands } from './install-commands.ts';
21
26
 
22
27
  // ============================================================
23
28
  // Types
@@ -91,6 +96,135 @@ export function detectFramework(projectRoot: string): FrameworkDetection {
91
96
  return result;
92
97
  }
93
98
 
99
+ // ============================================================
100
+ // Python Project Detection
101
+ // ============================================================
102
+
103
+ interface PythonDetection {
104
+ detected: boolean;
105
+ root: string;
106
+ hasFastapi: boolean;
107
+ hasSqlalchemy: boolean;
108
+ hasAlembic: boolean;
109
+ alembicDir: string | null;
110
+ }
111
+
112
+ export function detectPython(projectRoot: string): PythonDetection {
113
+ const result: PythonDetection = {
114
+ detected: false,
115
+ root: '',
116
+ hasFastapi: false,
117
+ hasSqlalchemy: false,
118
+ hasAlembic: false,
119
+ alembicDir: null,
120
+ };
121
+
122
+ // Check for Python project markers
123
+ const markers = ['pyproject.toml', 'setup.py', 'requirements.txt', 'Pipfile'];
124
+ const hasMarker = markers.some(m => existsSync(resolve(projectRoot, m)));
125
+ if (!hasMarker) return result;
126
+
127
+ result.detected = true;
128
+
129
+ // Scan dependencies for FastAPI and SQLAlchemy
130
+ const depFiles = [
131
+ { file: 'pyproject.toml', parser: parsePyprojectDeps },
132
+ { file: 'requirements.txt', parser: parseRequirementsDeps },
133
+ { file: 'setup.py', parser: parseSetupPyDeps },
134
+ { file: 'Pipfile', parser: parsePipfileDeps },
135
+ ];
136
+
137
+ for (const { file, parser } of depFiles) {
138
+ const filePath = resolve(projectRoot, file);
139
+ if (existsSync(filePath)) {
140
+ try {
141
+ const content = readFileSync(filePath, 'utf-8');
142
+ const deps = parser(content);
143
+ if (deps.includes('fastapi')) result.hasFastapi = true;
144
+ if (deps.includes('sqlalchemy')) result.hasSqlalchemy = true;
145
+ } catch {
146
+ // Best effort
147
+ }
148
+ }
149
+ }
150
+
151
+ // Check for Alembic
152
+ if (existsSync(resolve(projectRoot, 'alembic.ini'))) {
153
+ result.hasAlembic = true;
154
+ // Try to find the alembic versions directory
155
+ if (existsSync(resolve(projectRoot, 'alembic'))) {
156
+ result.alembicDir = 'alembic';
157
+ }
158
+ } else if (existsSync(resolve(projectRoot, 'alembic'))) {
159
+ result.hasAlembic = true;
160
+ result.alembicDir = 'alembic';
161
+ }
162
+
163
+ // Auto-detect Python source root
164
+ const candidateRoots = ['app', 'src', 'backend', 'api'];
165
+ for (const candidate of candidateRoots) {
166
+ const candidatePath = resolve(projectRoot, candidate);
167
+ if (existsSync(candidatePath) && existsSync(resolve(candidatePath, '__init__.py'))) {
168
+ result.root = candidate;
169
+ break;
170
+ }
171
+ // Also check for .py files directly (some projects use app/ without __init__.py)
172
+ if (existsSync(candidatePath)) {
173
+ try {
174
+ const files = readdirSync(candidatePath);
175
+ if (files.some(f => f.endsWith('.py'))) {
176
+ result.root = candidate;
177
+ break;
178
+ }
179
+ } catch {
180
+ // Best effort
181
+ }
182
+ }
183
+ }
184
+
185
+ // Fallback: use '.' if no candidate root found
186
+ if (!result.root) {
187
+ result.root = '.';
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ function parsePyprojectDeps(content: string): string[] {
194
+ const deps: string[] = [];
195
+ const lower = content.toLowerCase();
196
+ if (lower.includes('fastapi')) deps.push('fastapi');
197
+ if (lower.includes('sqlalchemy')) deps.push('sqlalchemy');
198
+ return deps;
199
+ }
200
+
201
+ function parseRequirementsDeps(content: string): string[] {
202
+ const deps: string[] = [];
203
+ const lower = content.toLowerCase();
204
+ for (const line of lower.split('\n')) {
205
+ const trimmed = line.trim();
206
+ if (trimmed.startsWith('fastapi')) deps.push('fastapi');
207
+ if (trimmed.startsWith('sqlalchemy')) deps.push('sqlalchemy');
208
+ }
209
+ return deps;
210
+ }
211
+
212
+ function parseSetupPyDeps(content: string): string[] {
213
+ const deps: string[] = [];
214
+ const lower = content.toLowerCase();
215
+ if (lower.includes('fastapi')) deps.push('fastapi');
216
+ if (lower.includes('sqlalchemy')) deps.push('sqlalchemy');
217
+ return deps;
218
+ }
219
+
220
+ function parsePipfileDeps(content: string): string[] {
221
+ const deps: string[] = [];
222
+ const lower = content.toLowerCase();
223
+ if (lower.includes('fastapi')) deps.push('fastapi');
224
+ if (lower.includes('sqlalchemy')) deps.push('sqlalchemy');
225
+ return deps;
226
+ }
227
+
94
228
  // ============================================================
95
229
  // Config File Generation
96
230
  // ============================================================
@@ -104,7 +238,7 @@ export function generateConfig(projectRoot: string, framework: FrameworkDetectio
104
238
 
105
239
  const projectName = basename(projectRoot);
106
240
 
107
- const config = {
241
+ const config: Record<string, unknown> = {
108
242
  project: {
109
243
  name: projectName,
110
244
  root: 'auto',
@@ -129,6 +263,21 @@ export function generateConfig(projectRoot: string, framework: FrameworkDetectio
129
263
  ],
130
264
  };
131
265
 
266
+ // Detect and add Python configuration
267
+ const python = detectPython(projectRoot);
268
+ if (python.detected) {
269
+ const pythonConfig: Record<string, unknown> = {
270
+ root: python.root,
271
+ exclude_dirs: ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'],
272
+ };
273
+ if (python.hasFastapi) pythonConfig.framework = 'fastapi';
274
+ if (python.hasSqlalchemy) pythonConfig.orm = 'sqlalchemy';
275
+ if (python.hasAlembic && python.alembicDir) {
276
+ pythonConfig.alembic_dir = python.alembicDir;
277
+ }
278
+ config.python = pythonConfig;
279
+ }
280
+
132
281
  const yamlContent = `# Massu AI Configuration
133
282
  # Generated by: npx massu init
134
283
  # Documentation: https://massu.ai/docs/getting-started/configuration
@@ -281,7 +430,8 @@ export function buildHooksConfig(hooksDir: string): HooksConfig {
281
430
  }
282
431
 
283
432
  export function installHooks(projectRoot: string): { installed: boolean; count: number } {
284
- const claudeDir = resolve(projectRoot, '.claude');
433
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
434
+ const claudeDir = resolve(projectRoot, claudeDirName);
285
435
  const settingsPath = resolve(claudeDir, 'settings.local.json');
286
436
 
287
437
  // Ensure .claude directory exists
@@ -321,6 +471,51 @@ export function installHooks(projectRoot: string): { installed: boolean; count:
321
471
  return { installed: true, count: hookCount };
322
472
  }
323
473
 
474
+ // ============================================================
475
+ // Memory Directory Initialization
476
+ // ============================================================
477
+
478
+ /**
479
+ * Initialize the memory directory and create an initial MEMORY.md if absent.
480
+ * The memory directory lives in ~/.claude/projects/<encoded-root>/memory/
481
+ * matching the path used by memory-db.ts / knowledge-tools.ts.
482
+ */
483
+ export function initMemoryDir(projectRoot: string): { created: boolean; memoryMdCreated: boolean } {
484
+ // Encode the project root the same way as getResolvedPaths() in config.ts
485
+ const encodedRoot = '-' + projectRoot.replace(/\//g, '-');
486
+ const memoryDir = resolve(homedir(), `.claude/projects/${encodedRoot}/memory`);
487
+
488
+ let created = false;
489
+ if (!existsSync(memoryDir)) {
490
+ mkdirSync(memoryDir, { recursive: true });
491
+ created = true;
492
+ }
493
+
494
+ const memoryMdPath = resolve(memoryDir, 'MEMORY.md');
495
+ let memoryMdCreated = false;
496
+ if (!existsSync(memoryMdPath)) {
497
+ const projectName = basename(projectRoot);
498
+ const memoryContent = `# ${projectName} - Massu Memory
499
+
500
+ ## Key Learnings
501
+ <!-- Important patterns and conventions discovered during development -->
502
+
503
+ ## Common Gotchas
504
+ <!-- Non-obvious issues and how to avoid them -->
505
+
506
+ ## Corrections
507
+ <!-- Wrong behaviors that were corrected and how to prevent them -->
508
+
509
+ ## File Index
510
+ <!-- Significant files and directories -->
511
+ `;
512
+ writeFileSync(memoryMdPath, memoryContent, 'utf-8');
513
+ memoryMdCreated = true;
514
+ }
515
+
516
+ return { created, memoryMdCreated };
517
+ }
518
+
324
519
  // ============================================================
325
520
  // Main Init Flow
326
521
  // ============================================================
@@ -343,6 +538,16 @@ export async function runInit(): Promise<void> {
343
538
  const detected = frameworkParts.length > 0 ? frameworkParts.join(', ') : 'JavaScript';
344
539
  console.log(` Detected: ${detected}`);
345
540
 
541
+ // Step 1b: Detect Python
542
+ const python = detectPython(projectRoot);
543
+ if (python.detected) {
544
+ const pyParts: string[] = ['Python'];
545
+ if (python.hasFastapi) pyParts.push('FastAPI');
546
+ if (python.hasSqlalchemy) pyParts.push('SQLAlchemy');
547
+ if (python.hasAlembic) pyParts.push('Alembic');
548
+ console.log(` Detected: ${pyParts.join(', ')} (root: ${python.root})`);
549
+ }
550
+
346
551
  // Step 2: Create config
347
552
  const configCreated = generateConfig(projectRoot, framework);
348
553
  if (configCreated) {
@@ -363,7 +568,27 @@ export async function runInit(): Promise<void> {
363
568
  const { count: hooksCount } = installHooks(projectRoot);
364
569
  console.log(` Installed ${hooksCount} hooks in .claude/settings.local.json`);
365
570
 
366
- // Step 5: Databases info
571
+ // Step 5: Install slash commands
572
+ const cmdResult = installCommands(projectRoot);
573
+ const cmdTotal = cmdResult.installed + cmdResult.updated + cmdResult.skipped;
574
+ if (cmdResult.installed > 0 || cmdResult.updated > 0) {
575
+ console.log(` Installed ${cmdTotal} slash commands (${cmdResult.installed} new, ${cmdResult.updated} updated)`);
576
+ } else {
577
+ console.log(` ${cmdTotal} slash commands already up to date`);
578
+ }
579
+
580
+ // Step 6: Initialize memory directory
581
+ const { created: memDirCreated, memoryMdCreated } = initMemoryDir(projectRoot);
582
+ if (memDirCreated) {
583
+ console.log(' Created memory directory (~/.claude/projects/.../memory/)');
584
+ } else {
585
+ console.log(' Memory directory already exists');
586
+ }
587
+ if (memoryMdCreated) {
588
+ console.log(' Created initial MEMORY.md');
589
+ }
590
+
591
+ // Step 7: Databases info
367
592
  console.log(' Databases will auto-create on first session');
368
593
 
369
594
  // Summary
@@ -0,0 +1,137 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * `massu install-commands` — Install massu slash commands into a project.
6
+ *
7
+ * Copies all massu command .md files from the package's commands/ directory
8
+ * into the project's .claude/commands/ directory. Existing massu commands
9
+ * are updated; non-massu commands are preserved.
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
13
+ import { resolve, dirname } from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ import { getConfig } from '../config.ts';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+
20
+ // ============================================================
21
+ // Command Installation
22
+ // ============================================================
23
+
24
+ /**
25
+ * Resolve the path to the bundled commands directory.
26
+ * Handles both npm-installed and local development scenarios.
27
+ */
28
+ export function resolveCommandsDir(): string | null {
29
+ const cwd = process.cwd();
30
+
31
+ // 1. npm-installed: node_modules/@massu/core/commands
32
+ const nodeModulesPath = resolve(cwd, 'node_modules/@massu/core/commands');
33
+ if (existsSync(nodeModulesPath)) {
34
+ return nodeModulesPath;
35
+ }
36
+
37
+ // 2. Relative to compiled dist/cli.js → ../commands
38
+ const distRelPath = resolve(__dirname, '../commands');
39
+ if (existsSync(distRelPath)) {
40
+ return distRelPath;
41
+ }
42
+
43
+ // 3. Relative to source src/commands/ → ../../commands
44
+ const srcRelPath = resolve(__dirname, '../../commands');
45
+ if (existsSync(srcRelPath)) {
46
+ return srcRelPath;
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ export interface InstallCommandsResult {
53
+ installed: number;
54
+ updated: number;
55
+ skipped: number;
56
+ commandsDir: string;
57
+ }
58
+
59
+ export function installCommands(projectRoot: string): InstallCommandsResult {
60
+ const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
61
+ const targetDir = resolve(projectRoot, claudeDirName, 'commands');
62
+
63
+ // Ensure .claude/commands directory exists
64
+ if (!existsSync(targetDir)) {
65
+ mkdirSync(targetDir, { recursive: true });
66
+ }
67
+
68
+ // Find source commands
69
+ const sourceDir = resolveCommandsDir();
70
+ if (!sourceDir) {
71
+ console.error(' ERROR: Could not find massu commands directory.');
72
+ console.error(' Try reinstalling: npm install @massu/core');
73
+ return { installed: 0, updated: 0, skipped: 0, commandsDir: targetDir };
74
+ }
75
+
76
+ // Read all command files from source
77
+ const sourceFiles = readdirSync(sourceDir).filter(f => f.endsWith('.md'));
78
+
79
+ let installed = 0;
80
+ let updated = 0;
81
+ let skipped = 0;
82
+
83
+ for (const file of sourceFiles) {
84
+ const sourcePath = resolve(sourceDir, file);
85
+ const targetPath = resolve(targetDir, file);
86
+ const sourceContent = readFileSync(sourcePath, 'utf-8');
87
+
88
+ if (existsSync(targetPath)) {
89
+ const existingContent = readFileSync(targetPath, 'utf-8');
90
+ if (existingContent === sourceContent) {
91
+ skipped++;
92
+ continue;
93
+ }
94
+ // Update existing command
95
+ writeFileSync(targetPath, sourceContent, 'utf-8');
96
+ updated++;
97
+ } else {
98
+ // Install new command
99
+ writeFileSync(targetPath, sourceContent, 'utf-8');
100
+ installed++;
101
+ }
102
+ }
103
+
104
+ return { installed, updated, skipped, commandsDir: targetDir };
105
+ }
106
+
107
+ // ============================================================
108
+ // Standalone CLI Runner
109
+ // ============================================================
110
+
111
+ export async function runInstallCommands(): Promise<void> {
112
+ const projectRoot = process.cwd();
113
+
114
+ console.log('');
115
+ console.log('Massu AI - Install Slash Commands');
116
+ console.log('==================================');
117
+ console.log('');
118
+
119
+ const result = installCommands(projectRoot);
120
+
121
+ if (result.installed > 0) {
122
+ console.log(` Installed ${result.installed} new commands`);
123
+ }
124
+ if (result.updated > 0) {
125
+ console.log(` Updated ${result.updated} existing commands`);
126
+ }
127
+ if (result.skipped > 0) {
128
+ console.log(` ${result.skipped} commands already up to date`);
129
+ }
130
+
131
+ const total = result.installed + result.updated + result.skipped;
132
+ console.log('');
133
+ console.log(` ${total} slash commands available in ${result.commandsDir}`);
134
+ console.log('');
135
+ console.log(' Restart your Claude Code session to use them.');
136
+ console.log('');
137
+ }