@gethmy/mcp 1.0.0 → 2.1.0

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