@gethmy/mcp 2.0.0 → 2.1.1
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/README.md +6 -1
- package/dist/cli.js +711 -59
- package/dist/index.js +5 -3
- package/dist/lib/__tests__/active-learning.test.js +386 -0
- package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
- package/dist/lib/__tests__/auto-session.test.js +661 -0
- package/dist/lib/__tests__/context-assembly.test.js +362 -0
- package/dist/lib/__tests__/graph-expansion.test.js +150 -0
- package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
- package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
- package/dist/lib/__tests__/pattern-detection.test.js +295 -0
- package/dist/lib/__tests__/prompt-builder.test.js +418 -0
- package/dist/lib/active-learning.js +878 -0
- package/dist/lib/api-client.js +550 -0
- package/dist/lib/auto-session.js +173 -0
- package/dist/lib/cli.js +127 -0
- package/dist/lib/config.js +205 -0
- package/dist/lib/consolidation.js +243 -0
- package/dist/lib/context-assembly.js +606 -0
- package/dist/lib/graph-expansion.js +163 -0
- package/dist/lib/http.js +174 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/lifecycle-maintenance.js +88 -0
- package/dist/lib/prompt-builder.js +483 -0
- package/dist/lib/remote.js +166 -0
- package/dist/lib/server.js +3132 -0
- package/dist/lib/tui/agents.js +116 -0
- package/dist/lib/tui/docs.js +744 -0
- package/dist/lib/tui/setup.js +1068 -0
- package/dist/lib/tui/theme.js +95 -0
- package/dist/lib/tui/writer.js +200 -0
- package/package.json +15 -6
- package/src/__tests__/active-learning.test.ts +483 -0
- package/src/__tests__/agent-performance-profiles.test.ts +468 -0
- package/src/__tests__/auto-session.test.ts +912 -0
- package/src/__tests__/context-assembly.test.ts +506 -0
- package/src/__tests__/graph-expansion.test.ts +285 -0
- package/src/__tests__/integration-memory-crud.test.ts +948 -0
- package/src/__tests__/integration-memory-system.test.ts +321 -0
- package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
- package/src/__tests__/pattern-detection.test.ts +438 -0
- package/src/__tests__/prompt-builder.test.ts +505 -0
- package/src/active-learning.ts +1227 -0
- package/src/api-client.ts +969 -0
- package/src/auto-session.ts +218 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +285 -0
- package/src/consolidation.ts +314 -0
- package/src/context-assembly.ts +842 -0
- package/src/graph-expansion.ts +234 -0
- package/src/http.ts +265 -0
- package/src/index.ts +8 -0
- package/src/lifecycle-maintenance.ts +120 -0
- package/src/prompt-builder.ts +681 -0
- package/src/remote.ts +227 -0
- package/src/server.ts +3858 -0
- package/src/tui/agents.ts +154 -0
- package/src/tui/docs.ts +863 -0
- package/src/tui/setup.ts +1281 -0
- package/src/tui/theme.ts +114 -0
- package/src/tui/writer.ts +260 -0
package/src/tui/docs.ts
ADDED
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { colors, symbols } from "./theme.js";
|
|
5
|
+
|
|
6
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface ProjectInfo {
|
|
9
|
+
packageManager: "bun" | "pnpm" | "yarn" | "npm" | null;
|
|
10
|
+
scripts: Record<string, string>;
|
|
11
|
+
language: "typescript" | "javascript" | "go" | "rust" | "python" | "unknown";
|
|
12
|
+
framework: string | null;
|
|
13
|
+
linter: string | null;
|
|
14
|
+
indentStyle: { type: "space" | "tab"; width: number } | null;
|
|
15
|
+
dirs: string[];
|
|
16
|
+
srcDirs: string[];
|
|
17
|
+
monorepo: boolean;
|
|
18
|
+
existingDocs: {
|
|
19
|
+
agentsMd: boolean;
|
|
20
|
+
claudeMd: boolean;
|
|
21
|
+
docsDir: boolean;
|
|
22
|
+
architectureMd: boolean;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DocsIssue {
|
|
27
|
+
severity: "error" | "warning";
|
|
28
|
+
file: string;
|
|
29
|
+
message: string;
|
|
30
|
+
fix?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DocsStepResult {
|
|
34
|
+
files: Array<{ path: string; content: string; type: "text" }>;
|
|
35
|
+
issues: DocsIssue[];
|
|
36
|
+
skipped: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const IGNORED_DIRS = new Set([
|
|
42
|
+
"node_modules",
|
|
43
|
+
".git",
|
|
44
|
+
"dist",
|
|
45
|
+
"build",
|
|
46
|
+
".next",
|
|
47
|
+
".nuxt",
|
|
48
|
+
".output",
|
|
49
|
+
".vercel",
|
|
50
|
+
".turbo",
|
|
51
|
+
".cache",
|
|
52
|
+
"coverage",
|
|
53
|
+
".harmony-worktrees",
|
|
54
|
+
"__pycache__",
|
|
55
|
+
"target",
|
|
56
|
+
"vendor",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** Read a JSON file safely, returning null on failure. */
|
|
60
|
+
function readJson(filePath: string): Record<string, unknown> | null {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read a text file safely, returning null on failure. */
|
|
69
|
+
function readText(filePath: string): string | null {
|
|
70
|
+
try {
|
|
71
|
+
return readFileSync(filePath, "utf-8");
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** List immediate subdirectories, excluding ignored names. */
|
|
78
|
+
function listDirs(dirPath: string): string[] {
|
|
79
|
+
try {
|
|
80
|
+
return readdirSync(dirPath).filter((entry) => {
|
|
81
|
+
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
82
|
+
try {
|
|
83
|
+
return statSync(join(dirPath, entry)).isDirectory();
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Map of common directory names to short descriptions. */
|
|
94
|
+
const DIR_DESCRIPTIONS: Record<string, string> = {
|
|
95
|
+
components: "UI components",
|
|
96
|
+
pages: "Route-level pages",
|
|
97
|
+
routes: "Route-level pages",
|
|
98
|
+
views: "Route-level pages",
|
|
99
|
+
hooks: "Custom hooks",
|
|
100
|
+
lib: "Utilities",
|
|
101
|
+
utils: "Utilities",
|
|
102
|
+
api: "API / server code",
|
|
103
|
+
server: "API / server code",
|
|
104
|
+
contexts: "State management",
|
|
105
|
+
store: "State management",
|
|
106
|
+
stores: "State management",
|
|
107
|
+
types: "Type definitions",
|
|
108
|
+
styles: "Stylesheets",
|
|
109
|
+
public: "Static assets",
|
|
110
|
+
static: "Static assets",
|
|
111
|
+
assets: "Static assets",
|
|
112
|
+
supabase: "Supabase backend",
|
|
113
|
+
functions: "Edge functions",
|
|
114
|
+
packages: "Monorepo packages",
|
|
115
|
+
apps: "Monorepo applications",
|
|
116
|
+
src: "Source code",
|
|
117
|
+
test: "Tests",
|
|
118
|
+
tests: "Tests",
|
|
119
|
+
__tests__: "Tests",
|
|
120
|
+
scripts: "Build / utility scripts",
|
|
121
|
+
config: "Configuration",
|
|
122
|
+
docs: "Documentation",
|
|
123
|
+
migrations: "Database migrations",
|
|
124
|
+
prisma: "Prisma schema & migrations",
|
|
125
|
+
e2e: "End-to-end tests",
|
|
126
|
+
cypress: "Cypress tests",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function describeDir(name: string): string {
|
|
130
|
+
return DIR_DESCRIPTIONS[name.toLowerCase()] ?? name;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Core functions ──────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scan the project directory and return structured metadata.
|
|
137
|
+
* Pure static analysis — no AI, no network calls.
|
|
138
|
+
*/
|
|
139
|
+
export function scanProject(cwd: string): ProjectInfo {
|
|
140
|
+
// Package manager
|
|
141
|
+
let packageManager: ProjectInfo["packageManager"] = null;
|
|
142
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) {
|
|
143
|
+
packageManager = "bun";
|
|
144
|
+
} else if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
145
|
+
packageManager = "pnpm";
|
|
146
|
+
} else if (existsSync(join(cwd, "yarn.lock"))) {
|
|
147
|
+
packageManager = "yarn";
|
|
148
|
+
} else if (existsSync(join(cwd, "package.json"))) {
|
|
149
|
+
packageManager = "npm";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Scripts
|
|
153
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
154
|
+
const scripts: Record<string, string> =
|
|
155
|
+
pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
156
|
+
? (pkg.scripts as Record<string, string>)
|
|
157
|
+
: {};
|
|
158
|
+
|
|
159
|
+
// Language
|
|
160
|
+
let language: ProjectInfo["language"] = "unknown";
|
|
161
|
+
if (existsSync(join(cwd, "tsconfig.json"))) {
|
|
162
|
+
language = "typescript";
|
|
163
|
+
} else if (existsSync(join(cwd, "go.mod"))) {
|
|
164
|
+
language = "go";
|
|
165
|
+
} else if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
166
|
+
language = "rust";
|
|
167
|
+
} else if (
|
|
168
|
+
existsSync(join(cwd, "setup.py")) ||
|
|
169
|
+
existsSync(join(cwd, "pyproject.toml"))
|
|
170
|
+
) {
|
|
171
|
+
language = "python";
|
|
172
|
+
} else if (pkg) {
|
|
173
|
+
language = "javascript";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Framework detection (from package.json dependencies)
|
|
177
|
+
let framework: string | null = null;
|
|
178
|
+
if (pkg) {
|
|
179
|
+
const deps: Record<string, string> = {
|
|
180
|
+
...(typeof pkg.dependencies === "object"
|
|
181
|
+
? (pkg.dependencies as Record<string, string>)
|
|
182
|
+
: {}),
|
|
183
|
+
...(typeof pkg.devDependencies === "object"
|
|
184
|
+
? (pkg.devDependencies as Record<string, string>)
|
|
185
|
+
: {}),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (deps.next) {
|
|
189
|
+
framework = "next";
|
|
190
|
+
} else if (deps.react && deps.vite) {
|
|
191
|
+
framework = "react+vite";
|
|
192
|
+
} else if (deps.react) {
|
|
193
|
+
framework = "react";
|
|
194
|
+
} else if (deps.vue && deps.vite) {
|
|
195
|
+
framework = "vue+vite";
|
|
196
|
+
} else if (deps.vue && deps.nuxt) {
|
|
197
|
+
framework = "nuxt";
|
|
198
|
+
} else if (deps.vue) {
|
|
199
|
+
framework = "vue";
|
|
200
|
+
} else if (deps.astro) {
|
|
201
|
+
framework = "astro";
|
|
202
|
+
} else if (deps.svelte) {
|
|
203
|
+
framework = "svelte";
|
|
204
|
+
} else if (deps.express) {
|
|
205
|
+
framework = "express";
|
|
206
|
+
} else if (deps.fastify) {
|
|
207
|
+
framework = "fastify";
|
|
208
|
+
} else if (deps.hono) {
|
|
209
|
+
framework = "hono";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Linter
|
|
214
|
+
let linter: string | null = null;
|
|
215
|
+
if (
|
|
216
|
+
existsSync(join(cwd, "biome.json")) ||
|
|
217
|
+
existsSync(join(cwd, "biome.jsonc"))
|
|
218
|
+
) {
|
|
219
|
+
linter = "biome";
|
|
220
|
+
} else {
|
|
221
|
+
const eslintFiles = [
|
|
222
|
+
".eslintrc",
|
|
223
|
+
".eslintrc.js",
|
|
224
|
+
".eslintrc.cjs",
|
|
225
|
+
".eslintrc.json",
|
|
226
|
+
".eslintrc.yml",
|
|
227
|
+
".eslintrc.yaml",
|
|
228
|
+
"eslint.config.js",
|
|
229
|
+
"eslint.config.mjs",
|
|
230
|
+
"eslint.config.cjs",
|
|
231
|
+
"eslint.config.ts",
|
|
232
|
+
];
|
|
233
|
+
if (eslintFiles.some((f) => existsSync(join(cwd, f)))) {
|
|
234
|
+
linter = "eslint";
|
|
235
|
+
} else {
|
|
236
|
+
const prettierFiles = [
|
|
237
|
+
".prettierrc",
|
|
238
|
+
".prettierrc.js",
|
|
239
|
+
".prettierrc.json",
|
|
240
|
+
".prettierrc.yml",
|
|
241
|
+
".prettierrc.yaml",
|
|
242
|
+
"prettier.config.js",
|
|
243
|
+
"prettier.config.mjs",
|
|
244
|
+
];
|
|
245
|
+
if (prettierFiles.some((f) => existsSync(join(cwd, f)))) {
|
|
246
|
+
linter = "prettier";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Indent style
|
|
252
|
+
let indentStyle: ProjectInfo["indentStyle"] = null;
|
|
253
|
+
const biome =
|
|
254
|
+
readJson(join(cwd, "biome.json")) ?? readJson(join(cwd, "biome.jsonc"));
|
|
255
|
+
if (biome) {
|
|
256
|
+
const formatter = biome.formatter as Record<string, unknown> | undefined;
|
|
257
|
+
if (formatter) {
|
|
258
|
+
const type = formatter.indentStyle === "tab" ? "tab" : "space";
|
|
259
|
+
const width =
|
|
260
|
+
typeof formatter.indentWidth === "number" ? formatter.indentWidth : 2;
|
|
261
|
+
indentStyle = { type, width };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!indentStyle) {
|
|
265
|
+
const editorConfig = readText(join(cwd, ".editorconfig"));
|
|
266
|
+
if (editorConfig) {
|
|
267
|
+
const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
|
|
268
|
+
const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
|
|
269
|
+
if (styleMatch) {
|
|
270
|
+
indentStyle = {
|
|
271
|
+
type: styleMatch[1] as "space" | "tab",
|
|
272
|
+
width: sizeMatch ? Number.parseInt(sizeMatch[1], 10) : 2,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Directories
|
|
279
|
+
const dirs = listDirs(cwd);
|
|
280
|
+
const srcDirs = existsSync(join(cwd, "src"))
|
|
281
|
+
? listDirs(join(cwd, "src"))
|
|
282
|
+
: [];
|
|
283
|
+
|
|
284
|
+
// Monorepo
|
|
285
|
+
const monorepo =
|
|
286
|
+
existsSync(join(cwd, "packages")) || existsSync(join(cwd, "apps"));
|
|
287
|
+
|
|
288
|
+
// Existing docs
|
|
289
|
+
const existingDocs = {
|
|
290
|
+
agentsMd: existsSync(join(cwd, "AGENTS.md")),
|
|
291
|
+
claudeMd: existsSync(join(cwd, "CLAUDE.md")),
|
|
292
|
+
docsDir: existsSync(join(cwd, "docs")),
|
|
293
|
+
architectureMd: existsSync(join(cwd, "docs", "architecture.md")),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
packageManager,
|
|
298
|
+
scripts,
|
|
299
|
+
language,
|
|
300
|
+
framework,
|
|
301
|
+
linter,
|
|
302
|
+
indentStyle,
|
|
303
|
+
dirs,
|
|
304
|
+
srcDirs,
|
|
305
|
+
monorepo,
|
|
306
|
+
existingDocs,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Generators ──────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/** Build a human-friendly run command prefix. */
|
|
313
|
+
function runCmd(pm: ProjectInfo["packageManager"]): string {
|
|
314
|
+
if (pm === "bun") return "bun run";
|
|
315
|
+
if (pm === "pnpm") return "pnpm run";
|
|
316
|
+
if (pm === "yarn") return "yarn";
|
|
317
|
+
return "npm run";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Guess a short description for a package.json script based on its name. */
|
|
321
|
+
function describeScript(name: string): string {
|
|
322
|
+
const map: Record<string, string> = {
|
|
323
|
+
dev: "Dev server",
|
|
324
|
+
start: "Start server",
|
|
325
|
+
build: "Production build",
|
|
326
|
+
lint: "Lint",
|
|
327
|
+
"lint:fix": "Lint + autofix",
|
|
328
|
+
format: "Format code",
|
|
329
|
+
test: "Run tests",
|
|
330
|
+
"test:watch": "Run tests (watch)",
|
|
331
|
+
"test:e2e": "End-to-end tests",
|
|
332
|
+
typecheck: "Type-check",
|
|
333
|
+
"type-check": "Type-check",
|
|
334
|
+
preview: "Preview production build",
|
|
335
|
+
deploy: "Deploy",
|
|
336
|
+
generate: "Code generation",
|
|
337
|
+
migrate: "Run migrations",
|
|
338
|
+
seed: "Seed database",
|
|
339
|
+
clean: "Clean build artifacts",
|
|
340
|
+
prepare: "Prepare (husky, etc.)",
|
|
341
|
+
};
|
|
342
|
+
return map[name] ?? "";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Generate a scaffold AGENTS.md from project metadata.
|
|
347
|
+
*/
|
|
348
|
+
export function generateAgentsMd(info: ProjectInfo, _cwd: string): string {
|
|
349
|
+
const lang =
|
|
350
|
+
info.language === "typescript"
|
|
351
|
+
? "TypeScript"
|
|
352
|
+
: info.language === "javascript"
|
|
353
|
+
? "JavaScript"
|
|
354
|
+
: info.language;
|
|
355
|
+
const frameworkLabel = info.framework ? `${info.framework} ` : "";
|
|
356
|
+
const monoLabel = info.monorepo ? " (monorepo)" : "";
|
|
357
|
+
|
|
358
|
+
const lines: string[] = [];
|
|
359
|
+
lines.push("# AGENTS.md");
|
|
360
|
+
lines.push("");
|
|
361
|
+
lines.push(`${frameworkLabel}${lang} project${monoLabel}.`);
|
|
362
|
+
lines.push("");
|
|
363
|
+
|
|
364
|
+
// Commands
|
|
365
|
+
const scriptEntries = Object.entries(info.scripts);
|
|
366
|
+
if (scriptEntries.length > 0 && info.packageManager) {
|
|
367
|
+
const prefix = runCmd(info.packageManager);
|
|
368
|
+
lines.push("## Commands");
|
|
369
|
+
lines.push("");
|
|
370
|
+
lines.push("```bash");
|
|
371
|
+
|
|
372
|
+
// Calculate padding for alignment
|
|
373
|
+
const commands = scriptEntries.map(([name]) => `${prefix} ${name}`);
|
|
374
|
+
const maxLen = Math.max(...commands.map((c) => c.length));
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < scriptEntries.length; i++) {
|
|
377
|
+
const [name] = scriptEntries[i];
|
|
378
|
+
const cmd = commands[i];
|
|
379
|
+
const desc = describeScript(name);
|
|
380
|
+
if (desc) {
|
|
381
|
+
lines.push(`${cmd}${" ".repeat(maxLen - cmd.length + 4)}# ${desc}`);
|
|
382
|
+
} else {
|
|
383
|
+
lines.push(cmd);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
lines.push("```");
|
|
388
|
+
lines.push("");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Code standards
|
|
392
|
+
lines.push("## Code Standards");
|
|
393
|
+
lines.push("");
|
|
394
|
+
|
|
395
|
+
const langLabel =
|
|
396
|
+
info.language === "typescript" ? "TypeScript" : "JavaScript";
|
|
397
|
+
if (info.language === "typescript" || info.language === "javascript") {
|
|
398
|
+
lines.push(`- ${langLabel} with ES modules`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (info.indentStyle) {
|
|
402
|
+
const unit = info.indentStyle.type === "tab" ? "tab" : "space";
|
|
403
|
+
lines.push(`- ${info.indentStyle.width}-${unit} indentation`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (info.linter) {
|
|
407
|
+
lines.push(`- Linted with ${info.linter}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
lines.push("");
|
|
411
|
+
|
|
412
|
+
// Architecture
|
|
413
|
+
lines.push("## Architecture");
|
|
414
|
+
lines.push("");
|
|
415
|
+
|
|
416
|
+
for (const dir of info.dirs) {
|
|
417
|
+
lines.push(`- \`${dir}/\` — ${describeDir(dir)}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (info.srcDirs.length > 0) {
|
|
421
|
+
for (const sub of info.srcDirs) {
|
|
422
|
+
lines.push(` - \`src/${sub}/\` — ${describeDir(sub)}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
lines.push("");
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Generate a lean CLAUDE.md pointer file.
|
|
432
|
+
*/
|
|
433
|
+
export function generateClaudeMd(info: ProjectInfo): string {
|
|
434
|
+
const lines: string[] = [];
|
|
435
|
+
lines.push("# CLAUDE.md");
|
|
436
|
+
lines.push("");
|
|
437
|
+
lines.push("@AGENTS.md");
|
|
438
|
+
|
|
439
|
+
if (info.existingDocs.architectureMd || info.dirs.includes("docs")) {
|
|
440
|
+
lines.push("@docs/architecture.md");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
lines.push("");
|
|
444
|
+
return lines.join("\n");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Generate an architecture.md scaffold.
|
|
449
|
+
*/
|
|
450
|
+
export function generateArchitectureMd(
|
|
451
|
+
info: ProjectInfo,
|
|
452
|
+
_cwd: string,
|
|
453
|
+
): string {
|
|
454
|
+
const lines: string[] = [];
|
|
455
|
+
lines.push("# Architecture");
|
|
456
|
+
lines.push("");
|
|
457
|
+
lines.push("## Directory Structure");
|
|
458
|
+
lines.push("");
|
|
459
|
+
|
|
460
|
+
for (const dir of info.dirs) {
|
|
461
|
+
lines.push(`- \`${dir}/\` — ${describeDir(dir)}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (info.srcDirs.length > 0) {
|
|
465
|
+
lines.push("");
|
|
466
|
+
lines.push("### `src/`");
|
|
467
|
+
lines.push("");
|
|
468
|
+
for (const sub of info.srcDirs) {
|
|
469
|
+
lines.push(`- \`src/${sub}/\` — ${describeDir(sub)}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
lines.push("");
|
|
474
|
+
return lines.join("\n");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── Verification ────────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
/** Vague phrases that provide no actionable guidance to agents. */
|
|
480
|
+
const VAGUE_STANDARDS = [
|
|
481
|
+
"follow best practices",
|
|
482
|
+
"use best practices",
|
|
483
|
+
"keep it clean",
|
|
484
|
+
"write clean code",
|
|
485
|
+
"maintain code quality",
|
|
486
|
+
"ensure quality",
|
|
487
|
+
"use proper naming",
|
|
488
|
+
"follow conventions",
|
|
489
|
+
"be consistent",
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Verify existing docs for broken references, stale commands, dead paths,
|
|
494
|
+
* and structural quality issues from the setup-agent-docs quality checks.
|
|
495
|
+
*/
|
|
496
|
+
export function verifyDocs(cwd: string): DocsIssue[] {
|
|
497
|
+
const issues: DocsIssue[] = [];
|
|
498
|
+
|
|
499
|
+
const claudeMd = readText(join(cwd, "CLAUDE.md"));
|
|
500
|
+
const agentsMd = readText(join(cwd, "AGENTS.md"));
|
|
501
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
502
|
+
const pkgScripts =
|
|
503
|
+
pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
504
|
+
? (pkg.scripts as Record<string, string>)
|
|
505
|
+
: {};
|
|
506
|
+
|
|
507
|
+
// ── CLAUDE.md checks ──────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
const projectRoot = resolve(cwd);
|
|
510
|
+
|
|
511
|
+
if (claudeMd) {
|
|
512
|
+
// Check @-references exist on disk (with path traversal protection)
|
|
513
|
+
const importedFiles: { ref: string; resolved: string }[] = [];
|
|
514
|
+
for (const line of claudeMd.split("\n")) {
|
|
515
|
+
const match = line.match(/^@(.+)$/);
|
|
516
|
+
if (match) {
|
|
517
|
+
const refPath = match[1].trim();
|
|
518
|
+
|
|
519
|
+
// Reject absolute paths
|
|
520
|
+
if (isAbsolute(refPath)) {
|
|
521
|
+
issues.push({
|
|
522
|
+
severity: "error",
|
|
523
|
+
file: "CLAUDE.md",
|
|
524
|
+
message: `@ reference uses an absolute path: ${refPath}`,
|
|
525
|
+
fix: "Use a project-relative path under the repository root",
|
|
526
|
+
});
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Resolve and ensure the path stays inside the project root
|
|
531
|
+
const resolvedPath = resolve(projectRoot, refPath);
|
|
532
|
+
if (
|
|
533
|
+
resolvedPath !== projectRoot &&
|
|
534
|
+
!resolvedPath.startsWith(projectRoot + sep)
|
|
535
|
+
) {
|
|
536
|
+
issues.push({
|
|
537
|
+
severity: "error",
|
|
538
|
+
file: "CLAUDE.md",
|
|
539
|
+
message: `@ reference escapes project root: ${refPath}`,
|
|
540
|
+
fix: "Remove traversal segments (../) and keep references inside the repo",
|
|
541
|
+
});
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
importedFiles.push({ ref: refPath, resolved: resolvedPath });
|
|
546
|
+
if (!existsSync(resolvedPath)) {
|
|
547
|
+
issues.push({
|
|
548
|
+
severity: "error",
|
|
549
|
+
file: "CLAUDE.md",
|
|
550
|
+
message: `Referenced file does not exist: ${refPath}`,
|
|
551
|
+
fix: `Remove the @${refPath} line or create the file`,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Line count — CLAUDE.md should be under 100 lines (lean, @imports only)
|
|
558
|
+
const claudeLines = claudeMd.split("\n").length;
|
|
559
|
+
if (claudeLines > 100) {
|
|
560
|
+
issues.push({
|
|
561
|
+
severity: "warning",
|
|
562
|
+
file: "CLAUDE.md",
|
|
563
|
+
message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
|
|
564
|
+
fix: "Move detailed content to AGENTS.md or docs/ files and use @imports",
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Duplication — check if CLAUDE.md duplicates content from imported files
|
|
569
|
+
if (importedFiles.length > 0) {
|
|
570
|
+
const claudeHeadings = extractHeadings(claudeMd);
|
|
571
|
+
for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
|
|
572
|
+
const refContent = readText(resolvedPath);
|
|
573
|
+
if (!refContent) continue;
|
|
574
|
+
const refHeadings = extractHeadings(refContent);
|
|
575
|
+
// Flag if CLAUDE.md repeats section headings from imported files
|
|
576
|
+
for (const heading of claudeHeadings) {
|
|
577
|
+
if (refHeadings.has(heading)) {
|
|
578
|
+
issues.push({
|
|
579
|
+
severity: "warning",
|
|
580
|
+
file: "CLAUDE.md",
|
|
581
|
+
message: `Section "${heading}" duplicates content from @${refPath}`,
|
|
582
|
+
fix: `Remove the "${heading}" section — it's already included via @import`,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── AGENTS.md checks ──────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
if (agentsMd) {
|
|
593
|
+
// Project Context — first non-heading, non-blank line should be exactly one line
|
|
594
|
+
const agentsLines = agentsMd.split("\n");
|
|
595
|
+
const contextLines: string[] = [];
|
|
596
|
+
let pastFirstHeading = false;
|
|
597
|
+
let hitNextSection = false;
|
|
598
|
+
for (const line of agentsLines) {
|
|
599
|
+
if (!pastFirstHeading) {
|
|
600
|
+
if (line.startsWith("# ")) {
|
|
601
|
+
pastFirstHeading = true;
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
// Stop at next ## heading
|
|
606
|
+
if (line.startsWith("## ")) {
|
|
607
|
+
hitNextSection = true;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
const trimmed = line.trim();
|
|
611
|
+
if (trimmed) contextLines.push(trimmed);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
|
|
615
|
+
issues.push({
|
|
616
|
+
severity: "warning",
|
|
617
|
+
file: "AGENTS.md",
|
|
618
|
+
message: "Missing project context line after the title heading",
|
|
619
|
+
fix: "Add a single-line description: stack + what the project does",
|
|
620
|
+
});
|
|
621
|
+
} else if (contextLines.length > 1) {
|
|
622
|
+
issues.push({
|
|
623
|
+
severity: "warning",
|
|
624
|
+
file: "AGENTS.md",
|
|
625
|
+
message: `Project context should be exactly 1 line, found ${contextLines.length}`,
|
|
626
|
+
fix: "Condense to a single line: stack + what the project does",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Commands — check against package.json scripts
|
|
631
|
+
const codeBlockRe = /```[\s\S]*?```/g;
|
|
632
|
+
let blockMatch: RegExpExecArray | null;
|
|
633
|
+
while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
|
|
634
|
+
const block = blockMatch[0];
|
|
635
|
+
const cmdRe = /(?:bun|npm|pnpm|yarn)\s+(?:run\s+)?(\S+)/g;
|
|
636
|
+
let cmdMatch: RegExpExecArray | null;
|
|
637
|
+
while ((cmdMatch = cmdRe.exec(block)) !== null) {
|
|
638
|
+
const scriptName = cmdMatch[1];
|
|
639
|
+
const builtins = new Set([
|
|
640
|
+
"install",
|
|
641
|
+
"init",
|
|
642
|
+
"create",
|
|
643
|
+
"exec",
|
|
644
|
+
"dlx",
|
|
645
|
+
"x",
|
|
646
|
+
"test",
|
|
647
|
+
"start",
|
|
648
|
+
]);
|
|
649
|
+
if (builtins.has(scriptName)) continue;
|
|
650
|
+
if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
|
|
651
|
+
issues.push({
|
|
652
|
+
severity: "warning",
|
|
653
|
+
file: "AGENTS.md",
|
|
654
|
+
message: `Command references script "${scriptName}" which is not in package.json`,
|
|
655
|
+
fix: `Update the command or add "${scriptName}" to package.json scripts`,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Code Standards — flag vague, non-actionable phrases
|
|
662
|
+
const standardsSection = extractSection(agentsMd, "Code Standards");
|
|
663
|
+
if (standardsSection) {
|
|
664
|
+
const lower = standardsSection.toLowerCase();
|
|
665
|
+
for (const phrase of VAGUE_STANDARDS) {
|
|
666
|
+
if (lower.includes(phrase)) {
|
|
667
|
+
issues.push({
|
|
668
|
+
severity: "warning",
|
|
669
|
+
file: "AGENTS.md",
|
|
670
|
+
message: `Code Standards contains vague phrase: "${phrase}"`,
|
|
671
|
+
fix: "Replace with specific, verifiable conventions derived from config files",
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Missing test command — if no test script exists, AGENTS.md should say so
|
|
678
|
+
if (Object.keys(pkgScripts).length > 0) {
|
|
679
|
+
const hasTestScript = Object.keys(pkgScripts).some(
|
|
680
|
+
(k) => k === "test" || k.startsWith("test:"),
|
|
681
|
+
);
|
|
682
|
+
if (!hasTestScript) {
|
|
683
|
+
const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
|
|
684
|
+
if (!mentionsNoTest) {
|
|
685
|
+
issues.push({
|
|
686
|
+
severity: "warning",
|
|
687
|
+
file: "AGENTS.md",
|
|
688
|
+
message:
|
|
689
|
+
"No test script in package.json and AGENTS.md doesn't mention it",
|
|
690
|
+
fix: 'Add a note like "No test framework. Verify changes with `bun run build`."',
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Check backtick-quoted paths
|
|
697
|
+
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ── docs/architecture.md checks ───────────────────────────────────────
|
|
701
|
+
|
|
702
|
+
const archMd = readText(join(cwd, "docs", "architecture.md"));
|
|
703
|
+
if (archMd) {
|
|
704
|
+
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return issues;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Extract ## headings from markdown content. */
|
|
711
|
+
function extractHeadings(content: string): Set<string> {
|
|
712
|
+
const headings = new Set<string>();
|
|
713
|
+
for (const line of content.split("\n")) {
|
|
714
|
+
const match = line.match(/^#{2,3}\s+(.+)$/);
|
|
715
|
+
if (match) {
|
|
716
|
+
headings.add(match[1].trim());
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return headings;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** Extract content under a specific ## section heading. */
|
|
723
|
+
function extractSection(content: string, heading: string): string | null {
|
|
724
|
+
const lines = content.split("\n");
|
|
725
|
+
let capturing = false;
|
|
726
|
+
const result: string[] = [];
|
|
727
|
+
|
|
728
|
+
for (const line of lines) {
|
|
729
|
+
if (capturing) {
|
|
730
|
+
// Stop at next ## heading
|
|
731
|
+
if (line.match(/^#{1,2}\s/)) break;
|
|
732
|
+
result.push(line);
|
|
733
|
+
} else if (
|
|
734
|
+
line.match(
|
|
735
|
+
new RegExp(
|
|
736
|
+
`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
|
|
737
|
+
"i",
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
) {
|
|
741
|
+
capturing = true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return result.length > 0 ? result.join("\n") : null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** Scan markdown for backtick-quoted paths and check they exist. */
|
|
749
|
+
function checkBacktickPaths(
|
|
750
|
+
content: string,
|
|
751
|
+
file: string,
|
|
752
|
+
cwd: string,
|
|
753
|
+
issues: DocsIssue[],
|
|
754
|
+
): void {
|
|
755
|
+
const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
|
|
756
|
+
let match: RegExpExecArray | null;
|
|
757
|
+
const checked = new Set<string>();
|
|
758
|
+
|
|
759
|
+
const root = resolve(cwd);
|
|
760
|
+
|
|
761
|
+
while ((match = pathRe.exec(content)) !== null) {
|
|
762
|
+
const refPath = match[1].replace(/\/$/, ""); // strip trailing slash
|
|
763
|
+
if (checked.has(refPath)) continue;
|
|
764
|
+
checked.add(refPath);
|
|
765
|
+
|
|
766
|
+
// Skip paths that escape the project root (e.g. src/../../etc/hosts)
|
|
767
|
+
const resolvedRef = resolve(root, refPath);
|
|
768
|
+
if (resolvedRef !== root && !resolvedRef.startsWith(root + sep)) continue;
|
|
769
|
+
|
|
770
|
+
if (!existsSync(resolvedRef)) {
|
|
771
|
+
issues.push({
|
|
772
|
+
severity: "warning",
|
|
773
|
+
file,
|
|
774
|
+
message: `Referenced path does not exist: ${refPath}`,
|
|
775
|
+
fix: `Update or remove the \`${refPath}\` reference`,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ── TUI Entry Point ─────────────────────────────────────────────────────────
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Run the docs step of the setup wizard.
|
|
785
|
+
* Scans the project, then either scaffolds new docs or verifies existing ones.
|
|
786
|
+
*/
|
|
787
|
+
export async function runDocsStep(cwd: string): Promise<DocsStepResult> {
|
|
788
|
+
const info = scanProject(cwd);
|
|
789
|
+
const hasDocs = info.existingDocs.agentsMd || info.existingDocs.claudeMd;
|
|
790
|
+
|
|
791
|
+
if (!hasDocs) {
|
|
792
|
+
// No docs — offer to generate
|
|
793
|
+
const shouldGenerate = await p.confirm({
|
|
794
|
+
message: "No project docs found. Generate AGENTS.md and CLAUDE.md?",
|
|
795
|
+
initialValue: true,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
if (p.isCancel(shouldGenerate) || !shouldGenerate) {
|
|
799
|
+
return { files: [], issues: [], skipped: true };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const files: DocsStepResult["files"] = [];
|
|
803
|
+
|
|
804
|
+
files.push({
|
|
805
|
+
path: join(cwd, "AGENTS.md"),
|
|
806
|
+
content: generateAgentsMd(info, cwd),
|
|
807
|
+
type: "text",
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
files.push({
|
|
811
|
+
path: join(cwd, "CLAUDE.md"),
|
|
812
|
+
content: generateClaudeMd(info),
|
|
813
|
+
type: "text",
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Generate architecture.md if docs/ exists or we referenced it
|
|
817
|
+
if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
|
|
818
|
+
files.push({
|
|
819
|
+
path: join(cwd, "docs", "architecture.md"),
|
|
820
|
+
content: generateArchitectureMd(info, cwd),
|
|
821
|
+
type: "text",
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
p.log.success(
|
|
826
|
+
`Generated ${files.length} doc file(s): ${files.map((f) => f.path.replace(cwd + "/", "")).join(", ")}`,
|
|
827
|
+
);
|
|
828
|
+
return { files, issues: [], skipped: false };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Docs exist — offer to verify
|
|
832
|
+
const shouldVerify = await p.confirm({
|
|
833
|
+
message: "Project docs found. Verify for issues?",
|
|
834
|
+
initialValue: false,
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
if (p.isCancel(shouldVerify) || !shouldVerify) {
|
|
838
|
+
return { files: [], issues: [], skipped: true };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const issues = verifyDocs(cwd);
|
|
842
|
+
|
|
843
|
+
if (issues.length === 0) {
|
|
844
|
+
p.log.success("No issues found in project docs.");
|
|
845
|
+
} else {
|
|
846
|
+
for (const issue of issues) {
|
|
847
|
+
const prefix = `${colors.bold(issue.file)}:`;
|
|
848
|
+
if (issue.severity === "error") {
|
|
849
|
+
p.log.error(`${prefix} ${issue.message}`);
|
|
850
|
+
} else {
|
|
851
|
+
p.log.warning(`${prefix} ${issue.message}`);
|
|
852
|
+
}
|
|
853
|
+
if (issue.fix) {
|
|
854
|
+
p.log.message(` ${symbols.arrow} ${colors.dim(issue.fix)}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
p.log.info(
|
|
858
|
+
`Found ${issues.length} issue(s) (${issues.filter((i) => i.severity === "error").length} errors, ${issues.filter((i) => i.severity === "warning").length} warnings)`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return { files: [], issues, skipped: false };
|
|
863
|
+
}
|