@massu/core 0.1.2 → 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.
- package/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +12522 -0
- package/dist/hooks/cost-tracker.js +80 -5
- package/dist/hooks/post-edit-context.js +72 -6
- package/dist/hooks/post-tool-use.js +234 -57
- package/dist/hooks/pre-compact.js +144 -5
- package/dist/hooks/pre-delete-check.js +141 -11
- package/dist/hooks/quality-event.js +80 -5
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +83 -8
- package/dist/hooks/session-start.js +153 -7
- package/dist/hooks/user-prompt.js +166 -5
- package/package.json +6 -5
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +235 -6
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/db.ts +115 -2
- package/src/docs-tools.ts +8 -6
- package/src/hooks/post-edit-context.ts +1 -1
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-start.ts +97 -4
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +14 -1
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/sentinel-db.ts +2 -1
- package/src/server.ts +29 -6
- package/src/session-archiver.ts +4 -5
- package/src/tools.ts +283 -31
- package/README.md +0 -40
package/src/backfill-sessions.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { resolve, basename } from 'path';
|
|
|
13
13
|
import { getMemoryDb, createSession, addObservation, addSummary, addUserPrompt, deduplicateFailedAttempt } from './memory-db.ts';
|
|
14
14
|
import { parseTranscript, extractUserMessages, getLastAssistantMessage } from './transcript-parser.ts';
|
|
15
15
|
import { extractObservationsFromEntries } from './observation-extractor.ts';
|
|
16
|
-
import { getProjectRoot } from './config.ts';
|
|
16
|
+
import { getProjectRoot, getConfig } from './config.ts';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Auto-detect the Claude Code project transcript directory.
|
|
@@ -22,12 +22,13 @@ import { getProjectRoot } from './config.ts';
|
|
|
22
22
|
function findTranscriptDir(): string {
|
|
23
23
|
const home = process.env.HOME ?? '~';
|
|
24
24
|
const projectRoot = getProjectRoot();
|
|
25
|
+
const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
|
|
25
26
|
// Claude Code escapes the path by replacing / with -
|
|
26
27
|
const escapedPath = projectRoot.replace(/\//g, '-');
|
|
27
|
-
const candidate = resolve(home,
|
|
28
|
+
const candidate = resolve(home, `${claudeDirName}/projects`, escapedPath);
|
|
28
29
|
if (existsSync(candidate)) return candidate;
|
|
29
|
-
// Fallback: scan
|
|
30
|
-
const projectsDir = resolve(home,
|
|
30
|
+
// Fallback: scan projects dir for directories matching the project name
|
|
31
|
+
const projectsDir = resolve(home, `${claudeDirName}/projects`);
|
|
31
32
|
if (existsSync(projectsDir)) {
|
|
32
33
|
try {
|
|
33
34
|
const entries = readdirSync(projectsDir);
|
package/src/cli.ts
CHANGED
|
@@ -43,6 +43,11 @@ async function main(): Promise<void> {
|
|
|
43
43
|
await runInstallHooks();
|
|
44
44
|
break;
|
|
45
45
|
}
|
|
46
|
+
case 'install-commands': {
|
|
47
|
+
const { runInstallCommands } = await import('./commands/install-commands.ts');
|
|
48
|
+
await runInstallCommands();
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
46
51
|
case 'validate-config': {
|
|
47
52
|
const { runValidateConfig } = await import('./commands/doctor.ts');
|
|
48
53
|
await runValidateConfig();
|
|
@@ -77,6 +82,7 @@ Commands:
|
|
|
77
82
|
init Set up Massu AI in your project (one command, full setup)
|
|
78
83
|
doctor Check installation health
|
|
79
84
|
install-hooks Install/update Claude Code hooks
|
|
85
|
+
install-commands Install/update slash commands
|
|
80
86
|
validate-config Validate massu.config.yaml
|
|
81
87
|
|
|
82
88
|
Options:
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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.
|
|
13
|
-
* 6.
|
|
14
|
-
* 7.
|
|
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 =
|
|
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 =
|
|
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;
|
package/src/commands/init.ts
CHANGED
|
@@ -8,12 +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.
|
|
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';
|
|
15
|
-
import { resolve, basename } from 'path';
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
17
|
+
import { resolve, basename, dirname } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { homedir } from 'os';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
16
23
|
import { stringify as yamlStringify } from 'yaml';
|
|
24
|
+
import { getConfig } from '../config.ts';
|
|
25
|
+
import { installCommands } from './install-commands.ts';
|
|
17
26
|
|
|
18
27
|
// ============================================================
|
|
19
28
|
// Types
|
|
@@ -87,6 +96,135 @@ export function detectFramework(projectRoot: string): FrameworkDetection {
|
|
|
87
96
|
return result;
|
|
88
97
|
}
|
|
89
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
|
+
|
|
90
228
|
// ============================================================
|
|
91
229
|
// Config File Generation
|
|
92
230
|
// ============================================================
|
|
@@ -100,7 +238,7 @@ export function generateConfig(projectRoot: string, framework: FrameworkDetectio
|
|
|
100
238
|
|
|
101
239
|
const projectName = basename(projectRoot);
|
|
102
240
|
|
|
103
|
-
const config = {
|
|
241
|
+
const config: Record<string, unknown> = {
|
|
104
242
|
project: {
|
|
105
243
|
name: projectName,
|
|
106
244
|
root: 'auto',
|
|
@@ -125,6 +263,21 @@ export function generateConfig(projectRoot: string, framework: FrameworkDetectio
|
|
|
125
263
|
],
|
|
126
264
|
};
|
|
127
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
|
+
|
|
128
281
|
const yamlContent = `# Massu AI Configuration
|
|
129
282
|
# Generated by: npx massu init
|
|
130
283
|
# Documentation: https://massu.ai/docs/getting-started/configuration
|
|
@@ -277,7 +430,8 @@ export function buildHooksConfig(hooksDir: string): HooksConfig {
|
|
|
277
430
|
}
|
|
278
431
|
|
|
279
432
|
export function installHooks(projectRoot: string): { installed: boolean; count: number } {
|
|
280
|
-
const
|
|
433
|
+
const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
|
|
434
|
+
const claudeDir = resolve(projectRoot, claudeDirName);
|
|
281
435
|
const settingsPath = resolve(claudeDir, 'settings.local.json');
|
|
282
436
|
|
|
283
437
|
// Ensure .claude directory exists
|
|
@@ -317,6 +471,51 @@ export function installHooks(projectRoot: string): { installed: boolean; count:
|
|
|
317
471
|
return { installed: true, count: hookCount };
|
|
318
472
|
}
|
|
319
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
|
+
|
|
320
519
|
// ============================================================
|
|
321
520
|
// Main Init Flow
|
|
322
521
|
// ============================================================
|
|
@@ -339,6 +538,16 @@ export async function runInit(): Promise<void> {
|
|
|
339
538
|
const detected = frameworkParts.length > 0 ? frameworkParts.join(', ') : 'JavaScript';
|
|
340
539
|
console.log(` Detected: ${detected}`);
|
|
341
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
|
+
|
|
342
551
|
// Step 2: Create config
|
|
343
552
|
const configCreated = generateConfig(projectRoot, framework);
|
|
344
553
|
if (configCreated) {
|
|
@@ -359,7 +568,27 @@ export async function runInit(): Promise<void> {
|
|
|
359
568
|
const { count: hooksCount } = installHooks(projectRoot);
|
|
360
569
|
console.log(` Installed ${hooksCount} hooks in .claude/settings.local.json`);
|
|
361
570
|
|
|
362
|
-
// Step 5:
|
|
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
|
|
363
592
|
console.log(' Databases will auto-create on first session');
|
|
364
593
|
|
|
365
594
|
// Summary
|