@massu/core 0.8.0 → 0.9.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/dist/cli.js +479 -182
- package/dist/hooks/post-tool-use.js +9 -4
- package/package.json +1 -1
- package/src/claude-md-templates.ts +342 -0
- package/src/commands/doctor.ts +28 -0
- package/src/commands/init.ts +41 -15
- package/src/knowledge-indexer.ts +1 -1
- package/src/mcp-bridge-tools.ts +0 -1
- package/src/memory-file-ingest.ts +13 -6
|
@@ -1666,11 +1666,10 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
|
|
|
1666
1666
|
// src/hooks/post-tool-use.ts
|
|
1667
1667
|
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
1668
1668
|
import { join as join2 } from "path";
|
|
1669
|
-
import { parse as
|
|
1669
|
+
import { parse as parseYaml2 } from "yaml";
|
|
1670
1670
|
|
|
1671
1671
|
// src/memory-file-ingest.ts
|
|
1672
1672
|
import { readFileSync as readFileSync5, existsSync as existsSync6, readdirSync } from "fs";
|
|
1673
|
-
import { parse as parseYaml2 } from "yaml";
|
|
1674
1673
|
function ingestMemoryFile(db, sessionId, filePath) {
|
|
1675
1674
|
if (!existsSync6(filePath)) return "skipped";
|
|
1676
1675
|
const content = readFileSync5(filePath, "utf-8");
|
|
@@ -1682,7 +1681,13 @@ function ingestMemoryFile(db, sessionId, filePath) {
|
|
|
1682
1681
|
let confidence;
|
|
1683
1682
|
if (frontmatterMatch) {
|
|
1684
1683
|
try {
|
|
1685
|
-
const fm =
|
|
1684
|
+
const fm = {};
|
|
1685
|
+
for (const line of frontmatterMatch[1].split("\n")) {
|
|
1686
|
+
const sep = line.indexOf(":");
|
|
1687
|
+
if (sep > 0) {
|
|
1688
|
+
fm[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1686
1691
|
name = fm.name ?? basename2;
|
|
1687
1692
|
description = fm.description ?? "";
|
|
1688
1693
|
type = fm.type ?? "discovery";
|
|
@@ -1897,7 +1902,7 @@ function readConventions(cwd) {
|
|
|
1897
1902
|
const configPath = join2(projectRoot, "massu.config.yaml");
|
|
1898
1903
|
if (!existsSync7(configPath)) return defaults;
|
|
1899
1904
|
const content = readFileSync6(configPath, "utf-8");
|
|
1900
|
-
const parsed =
|
|
1905
|
+
const parsed = parseYaml2(content);
|
|
1901
1906
|
if (!parsed || typeof parsed !== "object") return defaults;
|
|
1902
1907
|
const conventions = parsed.conventions;
|
|
1903
1908
|
if (!conventions || typeof conventions !== "object") return defaults;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, auto-learning pipeline, tiered tooling (12 free / 72 total), 55+ workflow commands, 15 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLAUDE.md content generation — framework-aware templates and directory scanning.
|
|
6
|
+
*
|
|
7
|
+
* Used by `massu init` to generate a tailored CLAUDE.md for the detected project.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdirSync, statSync } from 'fs';
|
|
11
|
+
import { resolve, relative, basename } from 'path';
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// Types (re-used from init.ts but kept minimal to avoid circular deps)
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
|
+
interface FrameworkInfo {
|
|
18
|
+
type: string;
|
|
19
|
+
router: string;
|
|
20
|
+
orm: string;
|
|
21
|
+
ui: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PythonInfo {
|
|
25
|
+
detected: boolean;
|
|
26
|
+
root: string;
|
|
27
|
+
hasFastapi: boolean;
|
|
28
|
+
hasSqlalchemy: boolean;
|
|
29
|
+
hasAlembic: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================
|
|
33
|
+
// Directory Structure Scanner (P-A03)
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
const EXCLUDED_DIRS = new Set([
|
|
37
|
+
'node_modules', '.git', '.venv', 'venv', '__pycache__',
|
|
38
|
+
'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage',
|
|
39
|
+
'.massu', '.turbo', '.cache', '.output',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export function scanDirectoryStructure(projectRoot: string, maxDepth: number = 2): string {
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
const rootName = basename(projectRoot);
|
|
45
|
+
lines.push(`${rootName}/`);
|
|
46
|
+
scanLevel(projectRoot, '', maxDepth, 0, lines);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scanLevel(
|
|
51
|
+
dir: string,
|
|
52
|
+
prefix: string,
|
|
53
|
+
maxDepth: number,
|
|
54
|
+
currentDepth: number,
|
|
55
|
+
lines: string[],
|
|
56
|
+
): void {
|
|
57
|
+
if (currentDepth >= maxDepth) return;
|
|
58
|
+
|
|
59
|
+
let entries: string[];
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir).sort();
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Separate dirs and files, filter excluded
|
|
67
|
+
const dirs: string[] = [];
|
|
68
|
+
const files: string[] = [];
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.startsWith('.') && EXCLUDED_DIRS.has(entry)) continue;
|
|
71
|
+
if (EXCLUDED_DIRS.has(entry)) continue;
|
|
72
|
+
try {
|
|
73
|
+
const stat = statSync(resolve(dir, entry));
|
|
74
|
+
if (stat.isDirectory()) dirs.push(entry);
|
|
75
|
+
else files.push(entry);
|
|
76
|
+
} catch {
|
|
77
|
+
// Skip unreadable entries
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const allEntries = [...dirs, ...files];
|
|
82
|
+
for (let i = 0; i < allEntries.length; i++) {
|
|
83
|
+
const entry = allEntries[i];
|
|
84
|
+
const isLast = i === allEntries.length - 1;
|
|
85
|
+
const connector = isLast ? '\u2514\u2500\u2500 ' : '\u251c\u2500\u2500 ';
|
|
86
|
+
const childPrefix = isLast ? ' ' : '\u2502 ';
|
|
87
|
+
const isDir = dirs.includes(entry);
|
|
88
|
+
|
|
89
|
+
lines.push(`${prefix}${connector}${entry}${isDir ? '/' : ''}`);
|
|
90
|
+
|
|
91
|
+
if (isDir) {
|
|
92
|
+
scanLevel(resolve(dir, entry), prefix + childPrefix, maxDepth, currentDepth + 1, lines);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================
|
|
98
|
+
// Content Builder (P-A02)
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
export function buildClaudeMdContent(
|
|
102
|
+
projectName: string,
|
|
103
|
+
projectRoot: string,
|
|
104
|
+
framework: FrameworkInfo,
|
|
105
|
+
python: PythonInfo,
|
|
106
|
+
): string {
|
|
107
|
+
const sections: string[] = [];
|
|
108
|
+
|
|
109
|
+
// 1. Project Overview
|
|
110
|
+
sections.push(buildProjectOverview(projectName, framework, python));
|
|
111
|
+
|
|
112
|
+
// 2. Tech Stack
|
|
113
|
+
sections.push(buildTechStack(framework, python));
|
|
114
|
+
|
|
115
|
+
// 3. Directory Structure
|
|
116
|
+
sections.push(buildDirectorySection(projectRoot));
|
|
117
|
+
|
|
118
|
+
// 4. Coding Conventions
|
|
119
|
+
sections.push(buildCodingConventions(framework, python));
|
|
120
|
+
|
|
121
|
+
// 5. Testing
|
|
122
|
+
sections.push(buildTestingSection(framework, python));
|
|
123
|
+
|
|
124
|
+
// 6. Massu Workflow
|
|
125
|
+
sections.push(buildMassuWorkflow());
|
|
126
|
+
|
|
127
|
+
// 7. Memory System
|
|
128
|
+
sections.push(buildMemorySystem());
|
|
129
|
+
|
|
130
|
+
// 8. Critical Rules
|
|
131
|
+
sections.push(buildCriticalRules());
|
|
132
|
+
|
|
133
|
+
return sections.join('\n\n---\n\n') + '\n';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- Section Builders ----
|
|
137
|
+
|
|
138
|
+
function buildProjectOverview(
|
|
139
|
+
projectName: string,
|
|
140
|
+
framework: FrameworkInfo,
|
|
141
|
+
python: PythonInfo,
|
|
142
|
+
): string {
|
|
143
|
+
const stack: string[] = [];
|
|
144
|
+
if (framework.type !== 'javascript') stack.push(capitalize(framework.type));
|
|
145
|
+
if (framework.ui !== 'none') stack.push(formatUiName(framework.ui));
|
|
146
|
+
if (framework.router !== 'none') stack.push(framework.router.toUpperCase());
|
|
147
|
+
if (framework.orm !== 'none') stack.push(capitalize(framework.orm));
|
|
148
|
+
if (python.detected) {
|
|
149
|
+
stack.push('Python');
|
|
150
|
+
if (python.hasFastapi) stack.push('FastAPI');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const stackStr = stack.length > 0 ? stack.join(', ') : 'JavaScript';
|
|
154
|
+
|
|
155
|
+
return `# ${projectName}
|
|
156
|
+
|
|
157
|
+
## Project Overview
|
|
158
|
+
|
|
159
|
+
${projectName} is a ${stackStr} project.
|
|
160
|
+
|
|
161
|
+
<!-- Add a brief description of what this project does -->`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildTechStack(framework: FrameworkInfo, python: PythonInfo): string {
|
|
165
|
+
const rows: string[] = [];
|
|
166
|
+
rows.push('| Technology | Details |');
|
|
167
|
+
rows.push('|-----------|---------|');
|
|
168
|
+
|
|
169
|
+
rows.push(`| Language | ${capitalize(framework.type)} |`);
|
|
170
|
+
if (framework.ui !== 'none') rows.push(`| UI Framework | ${formatUiName(framework.ui)} |`);
|
|
171
|
+
if (framework.router !== 'none') rows.push(`| Router/API | ${framework.router.toUpperCase()} |`);
|
|
172
|
+
if (framework.orm !== 'none') rows.push(`| ORM | ${capitalize(framework.orm)} |`);
|
|
173
|
+
if (python.detected) {
|
|
174
|
+
rows.push('| Python | Yes |');
|
|
175
|
+
if (python.hasFastapi) rows.push('| Python Framework | FastAPI |');
|
|
176
|
+
if (python.hasSqlalchemy) rows.push('| Python ORM | SQLAlchemy |');
|
|
177
|
+
if (python.hasAlembic) rows.push('| Migrations | Alembic |');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `## Tech Stack\n\n${rows.join('\n')}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildDirectorySection(projectRoot: string): string {
|
|
184
|
+
const tree = scanDirectoryStructure(projectRoot);
|
|
185
|
+
return `## Directory Structure\n\n\`\`\`\n${tree}\n\`\`\``;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildCodingConventions(framework: FrameworkInfo, python: PythonInfo): string {
|
|
189
|
+
const rules: string[] = [];
|
|
190
|
+
|
|
191
|
+
// Language-level conventions
|
|
192
|
+
if (framework.type === 'typescript') {
|
|
193
|
+
rules.push('- Use ESM imports (`import`), not CommonJS (`require`)');
|
|
194
|
+
rules.push('- Enable strict TypeScript (`strict: true` in tsconfig.json)');
|
|
195
|
+
rules.push('- Prefer explicit types over `any`');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// UI framework conventions
|
|
199
|
+
switch (framework.ui) {
|
|
200
|
+
case 'nextjs':
|
|
201
|
+
rules.push('- Use App Router conventions (`app/` directory)');
|
|
202
|
+
rules.push('- Default to Server Components; add `"use client"` only when needed');
|
|
203
|
+
rules.push('- Use `next/image` for images, `next/link` for navigation');
|
|
204
|
+
rules.push('- API routes go in `app/api/` using Route Handlers');
|
|
205
|
+
break;
|
|
206
|
+
case 'sveltekit':
|
|
207
|
+
rules.push('- Use load functions for data fetching (`+page.server.ts`)');
|
|
208
|
+
rules.push('- Use form actions for mutations');
|
|
209
|
+
rules.push('- Server-only code in `+server.ts` files');
|
|
210
|
+
break;
|
|
211
|
+
case 'react':
|
|
212
|
+
rules.push('- Prefer functional components with hooks');
|
|
213
|
+
rules.push('- Colocate component, styles, and tests');
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Router conventions
|
|
218
|
+
if (framework.router === 'trpc') {
|
|
219
|
+
rules.push('- Define tRPC routers with Zod input validation');
|
|
220
|
+
rules.push('- Keep router files focused (one domain per router)');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ORM conventions
|
|
224
|
+
if (framework.orm === 'prisma') {
|
|
225
|
+
rules.push('- Define models in `prisma/schema.prisma`');
|
|
226
|
+
rules.push('- Run `npx prisma generate` after schema changes');
|
|
227
|
+
} else if (framework.orm === 'drizzle') {
|
|
228
|
+
rules.push('- Define schemas with Drizzle table builders');
|
|
229
|
+
rules.push('- Run migrations with `drizzle-kit`');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Python conventions
|
|
233
|
+
if (python.detected) {
|
|
234
|
+
rules.push('- Use type hints for function signatures');
|
|
235
|
+
rules.push('- Use `async def` for async endpoints');
|
|
236
|
+
if (python.hasFastapi) {
|
|
237
|
+
rules.push('- Use Pydantic models for request/response schemas');
|
|
238
|
+
rules.push('- Organize routes with `APIRouter`');
|
|
239
|
+
}
|
|
240
|
+
if (python.hasSqlalchemy) {
|
|
241
|
+
rules.push('- Use SQLAlchemy 2.0 style (select/insert builders)');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (rules.length === 0) {
|
|
246
|
+
rules.push('- Follow consistent naming conventions');
|
|
247
|
+
rules.push('- Keep functions small and focused');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return `## Coding Conventions\n\n${rules.join('\n')}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function buildTestingSection(framework: FrameworkInfo, python: PythonInfo): string {
|
|
254
|
+
const lines: string[] = [];
|
|
255
|
+
|
|
256
|
+
if (framework.type === 'typescript') {
|
|
257
|
+
lines.push('- Test framework: vitest (or jest)');
|
|
258
|
+
lines.push('- Test files: `__tests__/*.test.ts` or `*.test.ts` colocated');
|
|
259
|
+
lines.push('- Run tests: `npm test`');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (python.detected) {
|
|
263
|
+
lines.push('- Python tests: pytest');
|
|
264
|
+
lines.push('- Test files: `tests/` directory or `test_*.py` files');
|
|
265
|
+
lines.push('- Run: `pytest`');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (lines.length === 0) {
|
|
269
|
+
lines.push('- Configure a test framework for this project');
|
|
270
|
+
lines.push('- Run tests before committing changes');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return `## Testing\n\n${lines.join('\n')}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function buildMassuWorkflow(): string {
|
|
277
|
+
return `## Massu Workflow
|
|
278
|
+
|
|
279
|
+
This project uses [Massu AI](https://massu.ai) for development governance.
|
|
280
|
+
|
|
281
|
+
### Common Commands
|
|
282
|
+
|
|
283
|
+
| Command | Purpose |
|
|
284
|
+
|---------|---------|
|
|
285
|
+
| \`/massu-create-plan\` | Create an implementation plan |
|
|
286
|
+
| \`/massu-plan\` | Audit and improve a plan |
|
|
287
|
+
| \`/massu-golden-path\` | Full implementation flow (plan to push) |
|
|
288
|
+
| \`/massu-test\` | Run tests with failure analysis |
|
|
289
|
+
| \`/massu-commit\` | Pre-commit verification gate |
|
|
290
|
+
| \`/massu-push\` | Pre-push verification gate |
|
|
291
|
+
| \`/massu-status\` | Project health dashboard |
|
|
292
|
+
| \`/massu-debug\` | Systematic debugging |
|
|
293
|
+
|
|
294
|
+
### Workflow Flow
|
|
295
|
+
|
|
296
|
+
\`\`\`
|
|
297
|
+
/massu-create-plan -> /massu-plan (audit) -> /massu-golden-path (implement + push)
|
|
298
|
+
\`\`\``;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildMemorySystem(): string {
|
|
302
|
+
return `## Memory System
|
|
303
|
+
|
|
304
|
+
Massu maintains persistent memory across sessions in \`~/.claude/projects/.../memory/\`.
|
|
305
|
+
|
|
306
|
+
- **User memories**: Your role, preferences, and expertise
|
|
307
|
+
- **Feedback memories**: Corrections and validated approaches
|
|
308
|
+
- **Project memories**: Ongoing work, decisions, deadlines
|
|
309
|
+
- **Reference memories**: External resources and tools
|
|
310
|
+
|
|
311
|
+
Memory is automatically loaded at session start and updated as you work.`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildCriticalRules(): string {
|
|
315
|
+
return `## Critical Rules
|
|
316
|
+
|
|
317
|
+
1. **Never commit secrets** — no API keys, tokens, or credentials in code
|
|
318
|
+
2. **Run tests before committing** — all tests must pass
|
|
319
|
+
3. **Verify before claiming done** — use VR-* verification checks
|
|
320
|
+
4. **Fix all issues encountered** — pre-existing issues get fixed too
|
|
321
|
+
5. **Read before editing** — understand existing code before modifying
|
|
322
|
+
|
|
323
|
+
<!-- Add project-specific rules as you discover them -->`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---- Helpers ----
|
|
327
|
+
|
|
328
|
+
function capitalize(str: string): string {
|
|
329
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function formatUiName(name: string): string {
|
|
333
|
+
const names: Record<string, string> = {
|
|
334
|
+
nextjs: 'Next.js',
|
|
335
|
+
sveltekit: 'SvelteKit',
|
|
336
|
+
nuxt: 'Nuxt',
|
|
337
|
+
angular: 'Angular',
|
|
338
|
+
vue: 'Vue',
|
|
339
|
+
react: 'React',
|
|
340
|
+
};
|
|
341
|
+
return names[name] ?? capitalize(name);
|
|
342
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 8. better-sqlite3 native module loads
|
|
16
16
|
* 9. Node.js version >= 18
|
|
17
17
|
* 10. Git repository detected
|
|
18
|
+
* 11. CLAUDE.md exists with content
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
@@ -373,6 +374,32 @@ function checkPythonHealth(projectRoot: string): CheckResult | null {
|
|
|
373
374
|
};
|
|
374
375
|
}
|
|
375
376
|
|
|
377
|
+
function checkClaudeMd(projectRoot: string): CheckResult {
|
|
378
|
+
const claudeMdPath = resolve(projectRoot, 'CLAUDE.md');
|
|
379
|
+
if (!existsSync(claudeMdPath)) {
|
|
380
|
+
return {
|
|
381
|
+
name: 'CLAUDE.md',
|
|
382
|
+
status: 'warn',
|
|
383
|
+
detail: 'CLAUDE.md not found. Run: npx massu init (or create manually)',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
388
|
+
if (content.trim().length < 50) {
|
|
389
|
+
return {
|
|
390
|
+
name: 'CLAUDE.md',
|
|
391
|
+
status: 'warn',
|
|
392
|
+
detail: 'CLAUDE.md exists but appears empty or minimal',
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
name: 'CLAUDE.md',
|
|
398
|
+
status: 'pass',
|
|
399
|
+
detail: 'CLAUDE.md found and has content',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
376
403
|
// ============================================================
|
|
377
404
|
// Main Doctor Flow
|
|
378
405
|
// ============================================================
|
|
@@ -397,6 +424,7 @@ export async function runDoctor(): Promise<void> {
|
|
|
397
424
|
checkNodeVersion(),
|
|
398
425
|
await checkGitRepo(projectRoot),
|
|
399
426
|
await checkLicenseStatus(),
|
|
427
|
+
checkClaudeMd(projectRoot),
|
|
400
428
|
];
|
|
401
429
|
|
|
402
430
|
// Add Python health check if configured
|
package/src/commands/init.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { resolve, basename, dirname } from 'path';
|
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import { homedir } from 'os';
|
|
20
20
|
import { backfillMemoryFiles } from '../memory-file-ingest.ts';
|
|
21
|
+
import { buildClaudeMdContent } from '../claude-md-templates.ts';
|
|
21
22
|
|
|
22
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
24
|
const __dirname = dirname(__filename);
|
|
@@ -37,6 +38,8 @@ interface FrameworkDetection {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
interface InitResult {
|
|
41
|
+
claudeMdCreated: boolean;
|
|
42
|
+
claudeMdSkipped: boolean;
|
|
40
43
|
configCreated: boolean;
|
|
41
44
|
configSkipped: boolean;
|
|
42
45
|
mcpRegistered: boolean;
|
|
@@ -289,6 +292,28 @@ ${yamlStringify(config)}`;
|
|
|
289
292
|
return true;
|
|
290
293
|
}
|
|
291
294
|
|
|
295
|
+
// ============================================================
|
|
296
|
+
// CLAUDE.md Generation
|
|
297
|
+
// ============================================================
|
|
298
|
+
|
|
299
|
+
export function generateClaudeMd(
|
|
300
|
+
projectRoot: string,
|
|
301
|
+
framework: FrameworkDetection,
|
|
302
|
+
python: PythonDetection,
|
|
303
|
+
): { created: boolean; skipped: boolean } {
|
|
304
|
+
const claudeMdPath = resolve(projectRoot, 'CLAUDE.md');
|
|
305
|
+
|
|
306
|
+
// NEVER overwrite existing CLAUDE.md
|
|
307
|
+
if (existsSync(claudeMdPath)) {
|
|
308
|
+
return { created: false, skipped: true };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const projectName = basename(projectRoot);
|
|
312
|
+
const content = buildClaudeMdContent(projectName, projectRoot, framework, python);
|
|
313
|
+
writeFileSync(claudeMdPath, content, 'utf-8');
|
|
314
|
+
return { created: true, skipped: false };
|
|
315
|
+
}
|
|
316
|
+
|
|
292
317
|
// ============================================================
|
|
293
318
|
// MCP Server Registration
|
|
294
319
|
// ============================================================
|
|
@@ -346,25 +371,18 @@ type HooksConfig = Record<string, HookGroup[]>;
|
|
|
346
371
|
* Handles both local development and npm-installed scenarios.
|
|
347
372
|
*/
|
|
348
373
|
export function resolveHooksDir(): string {
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (existsSync(nodeModulesPath)) {
|
|
353
|
-
return 'node_modules/@massu/core/dist/hooks';
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Fall back to finding relative to this source file
|
|
357
|
-
const localPath = resolve(__dirname, '../dist/hooks');
|
|
358
|
-
if (existsSync(localPath)) {
|
|
359
|
-
return localPath;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Default to node_modules path (will be created on npm install)
|
|
374
|
+
// Always use node_modules/@massu/core/dist/hooks relative to project root.
|
|
375
|
+
// hookCmd() wraps each command with a parent-directory walk to find the
|
|
376
|
+
// project root, so hooks resolve correctly even from subdirectories.
|
|
363
377
|
return 'node_modules/@massu/core/dist/hooks';
|
|
364
378
|
}
|
|
365
379
|
|
|
366
380
|
function hookCmd(hooksDir: string, hookFile: string): string {
|
|
367
|
-
|
|
381
|
+
// Walk up from cwd to find the directory containing node_modules/@massu/core,
|
|
382
|
+
// then cd there before running the hook. This handles subdirectories like
|
|
383
|
+
// website/, packages/foo/, etc. where node_modules doesn't exist.
|
|
384
|
+
const hookPath = `${hooksDir}/${hookFile}`;
|
|
385
|
+
return `d="$PWD"; while [ "$d" != "/" ] && [ ! -f "$d/${hookPath}" ]; do d="$(dirname "$d")"; done; cd "$d" && node ${hookPath}`;
|
|
368
386
|
}
|
|
369
387
|
|
|
370
388
|
export function buildHooksConfig(hooksDir: string): HooksConfig {
|
|
@@ -559,6 +577,14 @@ export async function runInit(): Promise<void> {
|
|
|
559
577
|
console.log(` Detected: ${pyParts.join(', ')} (root: ${python.root})`);
|
|
560
578
|
}
|
|
561
579
|
|
|
580
|
+
// Step 1.5: Generate CLAUDE.md (MUST be first content step — incident 2026-04-13)
|
|
581
|
+
const claudeMdResult = generateClaudeMd(projectRoot, framework, python);
|
|
582
|
+
if (claudeMdResult.created) {
|
|
583
|
+
console.log(' Created CLAUDE.md (project instructions for Claude Code)');
|
|
584
|
+
} else {
|
|
585
|
+
console.log(' CLAUDE.md already exists (preserved)');
|
|
586
|
+
}
|
|
587
|
+
|
|
562
588
|
// Step 2: Create config
|
|
563
589
|
const configCreated = generateConfig(projectRoot, framework);
|
|
564
590
|
if (configCreated) {
|
package/src/knowledge-indexer.ts
CHANGED
|
@@ -599,7 +599,7 @@ export function indexAllKnowledge(db: Database.Database): IndexStats {
|
|
|
599
599
|
// Parse plan documents for structured metadata
|
|
600
600
|
if (category === 'plan') {
|
|
601
601
|
// Extract plan items (P1-001, P2-001, etc.)
|
|
602
|
-
const planItemRegex = /^###\s+(P
|
|
602
|
+
const planItemRegex = /^###\s+(P[-A-Z]*\d*-?\w+):\s+(.+)$/gm;
|
|
603
603
|
let planMatch;
|
|
604
604
|
while ((planMatch = planItemRegex.exec(content)) !== null) {
|
|
605
605
|
insertChunk.run(docId, 'pattern', planMatch[1], `${planMatch[1]}: ${planMatch[2]}`, null, null, JSON.stringify({ plan_item_id: planMatch[1] }));
|
package/src/mcp-bridge-tools.ts
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import type Database from 'better-sqlite3';
|
|
12
12
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
13
13
|
import { join } from 'path';
|
|
14
|
-
import { parse as parseYaml } from 'yaml';
|
|
15
14
|
import { addObservation } from './memory-db.ts';
|
|
16
15
|
|
|
17
16
|
export type IngestResult = 'inserted' | 'updated' | 'skipped';
|
|
@@ -42,13 +41,21 @@ export function ingestMemoryFile(
|
|
|
42
41
|
|
|
43
42
|
if (frontmatterMatch) {
|
|
44
43
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Simple key: value parser for memory file frontmatter
|
|
45
|
+
// (avoids importing yaml library — frontmatter is flat key-value pairs)
|
|
46
|
+
const fm: Record<string, string> = {};
|
|
47
|
+
for (const line of frontmatterMatch[1].split('\n')) {
|
|
48
|
+
const sep = line.indexOf(':');
|
|
49
|
+
if (sep > 0) {
|
|
50
|
+
fm[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
name = fm.name ?? basename;
|
|
54
|
+
description = fm.description ?? '';
|
|
55
|
+
type = fm.type ?? 'discovery';
|
|
49
56
|
confidence = fm.confidence != null ? Number(fm.confidence) : undefined;
|
|
50
57
|
} catch {
|
|
51
|
-
// Use defaults if
|
|
58
|
+
// Use defaults if frontmatter parsing fails
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|