@gethmy/mcp 2.3.1 → 2.3.3

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