@infinitedusky/indusk-mcp 0.4.0 → 0.6.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/bin/cli.js CHANGED
@@ -14,9 +14,15 @@ program
14
14
  .command("init")
15
15
  .description("Initialize a project with InDusk dev system")
16
16
  .option("-f, --force", "Overwrite existing files (except CLAUDE.md and planning/)")
17
+ .option("--skills <list>", "Comma-separated domain skills to install (e.g., nextjs,tailwind)")
18
+ .option("--no-domain-skills", "Skip domain skill detection and installation")
17
19
  .action(async (opts) => {
18
20
  const { init } = await import("./commands/init.js");
19
- await init(process.cwd(), { force: opts.force ?? false });
21
+ await init(process.cwd(), {
22
+ force: opts.force ?? false,
23
+ skills: opts.skills,
24
+ noDomainSkills: opts.domainSkills === false,
25
+ });
20
26
  });
21
27
  program
22
28
  .command("update")
@@ -25,6 +31,15 @@ program
25
31
  const { update } = await import("./commands/update.js");
26
32
  await update(process.cwd());
27
33
  });
34
+ program
35
+ .command("check-gates")
36
+ .description("Validate plan execution gates — reports incomplete verification, context, and document items")
37
+ .option("--file <path>", "Path to a specific impl.md file")
38
+ .option("--phase <number>", "Check a specific phase number", Number.parseInt)
39
+ .action(async (opts) => {
40
+ const { checkGates } = await import("./commands/check-gates.js");
41
+ await checkGates(process.cwd(), { file: opts.file, phase: opts.phase });
42
+ });
28
43
  program
29
44
  .command("serve")
30
45
  .description("Start the MCP server (used by Claude Code via .mcp.json)")
@@ -0,0 +1,4 @@
1
+ export declare function checkGates(projectRoot: string, options?: {
2
+ file?: string;
3
+ phase?: number;
4
+ }): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAllPhaseCompletions, parseImpl } from "../../lib/impl-parser.js";
4
+ export async function checkGates(projectRoot, options = {}) {
5
+ let implPath;
6
+ if (options.file) {
7
+ implPath = options.file;
8
+ }
9
+ else {
10
+ // Find active impl (in-progress status) in planning/
11
+ const planningDir = join(projectRoot, "planning");
12
+ if (!existsSync(planningDir)) {
13
+ console.error("No planning/ directory found");
14
+ process.exitCode = 1;
15
+ return;
16
+ }
17
+ const plans = readdirSync(planningDir, { withFileTypes: true })
18
+ .filter((d) => d.isDirectory() && d.name !== "archive")
19
+ .map((d) => d.name);
20
+ let found = null;
21
+ for (const plan of plans) {
22
+ const path = join(planningDir, plan, "impl.md");
23
+ if (!existsSync(path))
24
+ continue;
25
+ const content = readFileSync(path, "utf-8");
26
+ if (content.includes("status: in-progress")) {
27
+ found = path;
28
+ break;
29
+ }
30
+ }
31
+ if (!found) {
32
+ console.error("No in-progress impl found in planning/");
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ implPath = found;
37
+ }
38
+ if (!existsSync(implPath)) {
39
+ console.error(`File not found: ${implPath}`);
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ const parsed = parseImpl(implPath);
44
+ const completions = getAllPhaseCompletions(parsed);
45
+ if (options.phase) {
46
+ const phase = completions.find((c) => c.phase === options.phase);
47
+ if (!phase) {
48
+ console.error(`Phase ${options.phase} not found`);
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ printPhase(phase);
53
+ process.exitCode = phase.complete ? 0 : 1;
54
+ return;
55
+ }
56
+ // Report all phases
57
+ let allPass = true;
58
+ console.info(`Gate status: ${implPath}\n`);
59
+ for (const phase of completions) {
60
+ printPhase(phase);
61
+ if (!phase.complete)
62
+ allPass = false;
63
+ }
64
+ // Check for blockers
65
+ for (const phase of parsed.phases) {
66
+ if (phase.blocker) {
67
+ console.info(` BLOCKER in Phase ${phase.number}: ${phase.blocker}`);
68
+ allPass = false;
69
+ }
70
+ }
71
+ console.info(allPass ? "\nAll gates pass." : "\nSome gates incomplete.");
72
+ process.exitCode = allPass ? 0 : 1;
73
+ }
74
+ function printPhase(phase) {
75
+ const status = phase.complete ? "PASS" : "FAIL";
76
+ console.info(`Phase ${phase.phase}: ${phase.name} — ${status} (${phase.checkedItems}/${phase.totalItems})`);
77
+ if (!phase.complete) {
78
+ for (const [gate, items] of Object.entries(phase.uncheckedByGate)) {
79
+ for (const item of items) {
80
+ console.info(` [${gate}] ${item}`);
81
+ }
82
+ }
83
+ }
84
+ }
@@ -1,4 +1,6 @@
1
1
  export interface InitOptions {
2
2
  force?: boolean;
3
+ skills?: string;
4
+ noDomainSkills?: boolean;
3
5
  }
4
6
  export declare function init(projectRoot: string, options?: InitOptions): Promise<void>;
@@ -70,8 +70,56 @@ function createCgcIgnore(projectRoot) {
70
70
  ].join("\n"));
71
71
  console.info(" create: .cgcignore");
72
72
  }
73
+ const DOMAIN_DETECTION_MAP = [
74
+ { signal: "dependency", match: "next", skill: "nextjs" },
75
+ { signal: "dependency", match: "tailwindcss", skill: "tailwind" },
76
+ { signal: "dependency", match: "react", skill: "react" },
77
+ { signal: "devDependency", match: "typescript", skill: "typescript" },
78
+ { signal: "devDependency", match: "vitest", skill: "testing" },
79
+ { signal: "devDependency", match: "jest", skill: "testing" },
80
+ { signal: "dependency", match: "vitepress", skill: "vitepress" },
81
+ ];
82
+ const FILE_PATTERN_DETECTIONS = [
83
+ { pattern: "*.sol", skill: "solidity" },
84
+ { pattern: "Dockerfile*", skill: "docker" },
85
+ ];
86
+ function detectDomainSkills(projectRoot) {
87
+ const detections = [];
88
+ const seen = new Set();
89
+ // Check package.json dependencies
90
+ const pkgPath = join(projectRoot, "package.json");
91
+ if (existsSync(pkgPath)) {
92
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
93
+ const deps = pkg.dependencies ?? {};
94
+ const devDeps = pkg.devDependencies ?? {};
95
+ for (const rule of DOMAIN_DETECTION_MAP) {
96
+ if (seen.has(rule.skill))
97
+ continue;
98
+ const source = rule.signal === "dependency" ? deps : devDeps;
99
+ if (source[rule.match]) {
100
+ detections.push({
101
+ skill: rule.skill,
102
+ signal: rule.signal,
103
+ match: rule.match,
104
+ });
105
+ seen.add(rule.skill);
106
+ }
107
+ }
108
+ }
109
+ // Check file patterns
110
+ for (const rule of FILE_PATTERN_DETECTIONS) {
111
+ if (seen.has(rule.skill))
112
+ continue;
113
+ const matches = globSync(rule.pattern, { cwd: projectRoot, maxDepth: 3 });
114
+ if (matches.length > 0) {
115
+ detections.push({ skill: rule.skill, signal: "file-pattern", match: rule.pattern });
116
+ seen.add(rule.skill);
117
+ }
118
+ }
119
+ return detections;
120
+ }
73
121
  export async function init(projectRoot, options = {}) {
74
- const { force = false } = options;
122
+ const { force = false, skills, noDomainSkills = false } = options;
75
123
  const projectName = basename(projectRoot);
76
124
  console.info(`Initializing InDusk dev system...${force ? " (--force)" : ""}\n`);
77
125
  // 1. Copy skills
@@ -91,7 +139,66 @@ export async function init(projectRoot, options = {}) {
91
139
  cpSync(join(skillsSource, file), targetFile);
92
140
  console.info(` ${existsSync(targetFile) ? "overwrite" : "create"}: .claude/skills/${skillName}/SKILL.md`);
93
141
  }
94
- // 2. Create CLAUDE.md (never overwrite — write CLAUDE-NEW.md if exists)
142
+ // 2. Install domain skills
143
+ if (!noDomainSkills) {
144
+ console.info("\n[Domain Skills]");
145
+ const domainSource = join(packageRoot, "skills/domain");
146
+ let skillsToInstall;
147
+ if (skills) {
148
+ skillsToInstall = skills.split(",").map((s) => s.trim());
149
+ console.info(` manual: installing ${skillsToInstall.join(", ")}`);
150
+ }
151
+ else {
152
+ const detections = detectDomainSkills(projectRoot);
153
+ skillsToInstall = detections.map((d) => d.skill);
154
+ if (detections.length > 0) {
155
+ for (const d of detections) {
156
+ console.info(` detected: ${d.skill} (${d.signal}: ${d.match})`);
157
+ }
158
+ }
159
+ else {
160
+ console.info(" none detected");
161
+ }
162
+ }
163
+ for (const skillName of skillsToInstall) {
164
+ const sourceFile = join(domainSource, `${skillName}.md`);
165
+ if (!existsSync(sourceFile)) {
166
+ console.info(` skip: ${skillName} (not found in registry)`);
167
+ continue;
168
+ }
169
+ const targetDir = join(skillsTarget, skillName);
170
+ const targetFile = join(targetDir, "SKILL.md");
171
+ if (existsSync(targetFile) && !force) {
172
+ console.info(` skip: .claude/skills/${skillName}/SKILL.md (already exists)`);
173
+ continue;
174
+ }
175
+ mkdirSync(targetDir, { recursive: true });
176
+ cpSync(sourceFile, targetFile);
177
+ console.info(` install: .claude/skills/${skillName}/SKILL.md`);
178
+ }
179
+ }
180
+ else {
181
+ console.info("\n[Domain Skills]");
182
+ console.info(" skipped (--no-domain-skills)");
183
+ }
184
+ // 3. Copy community lessons
185
+ console.info("\n[Lessons]");
186
+ const lessonsSource = join(packageRoot, "lessons/community");
187
+ const lessonsTarget = join(projectRoot, ".claude/lessons");
188
+ mkdirSync(lessonsTarget, { recursive: true });
189
+ if (existsSync(lessonsSource)) {
190
+ const lessonFiles = globSync("community-*.md", { cwd: lessonsSource });
191
+ for (const file of lessonFiles) {
192
+ const targetFile = join(lessonsTarget, file);
193
+ if (existsSync(targetFile) && !force) {
194
+ console.info(` skip: .claude/lessons/${file} (already exists)`);
195
+ continue;
196
+ }
197
+ cpSync(join(lessonsSource, file), targetFile);
198
+ console.info(` ${existsSync(targetFile) ? "overwrite" : "create"}: .claude/lessons/${file}`);
199
+ }
200
+ }
201
+ // 3. Create CLAUDE.md (never overwrite — write CLAUDE-NEW.md if exists)
95
202
  console.info("\n[Project files]");
96
203
  const claudeMdPath = join(projectRoot, "CLAUDE.md");
97
204
  if (existsSync(claudeMdPath)) {
@@ -184,7 +291,74 @@ export async function init(projectRoot, options = {}) {
184
291
  cpSync(join(packageRoot, "templates/biome.template.json"), biomePath);
185
292
  console.info(` ${existsSync(biomePath) ? "overwrite" : "create"}: biome.json`);
186
293
  }
187
- // 7. Create .cgcignore (always overwrite — package-owned)
294
+ // 7. Install gate enforcement hooks
295
+ console.info("\n[Hooks]");
296
+ const hooksSource = join(packageRoot, "hooks");
297
+ const hooksTarget = join(projectRoot, ".claude/hooks");
298
+ const hookFiles = ["check-gates.js", "gate-reminder.js"];
299
+ if (existsSync(hooksSource)) {
300
+ mkdirSync(hooksTarget, { recursive: true });
301
+ for (const file of hookFiles) {
302
+ const sourceFile = join(hooksSource, file);
303
+ const targetFile = join(hooksTarget, file);
304
+ if (!existsSync(sourceFile))
305
+ continue;
306
+ if (existsSync(targetFile) && !force) {
307
+ console.info(` skip: .claude/hooks/${file} (already exists)`);
308
+ }
309
+ else {
310
+ cpSync(sourceFile, targetFile);
311
+ console.info(` ${existsSync(targetFile) ? "overwrite" : "create"}: .claude/hooks/${file}`);
312
+ }
313
+ }
314
+ }
315
+ // Merge hook config into .claude/settings.json
316
+ const claudeSettingsPath = join(projectRoot, ".claude/settings.json");
317
+ const hookConfig = {
318
+ PreToolUse: [
319
+ {
320
+ matcher: "Edit|Write",
321
+ hooks: [{ type: "command", command: "node .claude/hooks/check-gates.js" }],
322
+ },
323
+ ],
324
+ PostToolUse: [
325
+ {
326
+ matcher: "Edit|Write",
327
+ hooks: [{ type: "command", command: "node .claude/hooks/gate-reminder.js" }],
328
+ },
329
+ ],
330
+ };
331
+ if (existsSync(claudeSettingsPath)) {
332
+ const existing = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
333
+ existing.hooks = existing.hooks || {};
334
+ let hooksUpdated = false;
335
+ for (const [event, entries] of Object.entries(hookConfig)) {
336
+ const existingEntries = existing.hooks[event] || [];
337
+ // Check if our hook is already present
338
+ const hasOurHook = existingEntries.some((e) => e.hooks?.some((h) => h.command?.includes("check-gates") || h.command?.includes("gate-reminder")));
339
+ if (!hasOurHook || force) {
340
+ // Remove old entries if force, then add
341
+ if (force) {
342
+ existing.hooks[event] = existingEntries.filter((e) => !e.hooks?.some((h) => h.command?.includes("check-gates") || h.command?.includes("gate-reminder")));
343
+ }
344
+ existing.hooks[event] = [...(existing.hooks[event] || []), ...entries];
345
+ hooksUpdated = true;
346
+ }
347
+ }
348
+ if (hooksUpdated) {
349
+ writeFileSync(claudeSettingsPath, `${JSON.stringify(existing, null, "\t")}\n`);
350
+ console.info(" update: .claude/settings.json (added hook config)");
351
+ }
352
+ else {
353
+ console.info(" skip: .claude/settings.json hooks (already configured)");
354
+ }
355
+ }
356
+ else {
357
+ const settings = { hooks: hookConfig };
358
+ writeFileSync(claudeSettingsPath, `${JSON.stringify(settings, null, "\t")}\n`);
359
+ console.info(" create: .claude/settings.json (with hook config)");
360
+ }
361
+ // 8. Create .cgcignore (always overwrite — package-owned)
188
362
  createCgcIgnore(projectRoot);
189
363
  // 8. Infrastructure: FalkorDB + CGC
190
364
  console.info("\n[Infrastructure]");
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { cpSync, existsSync, readFileSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { globSync } from "glob";
@@ -14,21 +14,25 @@ export async function update(projectRoot) {
14
14
  const skillsTarget = join(projectRoot, ".claude/skills");
15
15
  const skillFiles = globSync("*.md", { cwd: skillsSource });
16
16
  let updated = 0;
17
- let skipped = 0;
17
+ let added = 0;
18
+ let current = 0;
18
19
  for (const file of skillFiles) {
19
20
  const skillName = file.replace(".md", "");
20
21
  const sourceFile = join(skillsSource, file);
21
- const targetFile = join(skillsTarget, skillName, "SKILL.md");
22
+ const targetDir = join(skillsTarget, skillName);
23
+ const targetFile = join(targetDir, "SKILL.md");
22
24
  if (!existsSync(targetFile)) {
23
- console.info(` skip: ${skillName} (not installed — run init first)`);
24
- skipped++;
25
+ mkdirSync(targetDir, { recursive: true });
26
+ cpSync(sourceFile, targetFile);
27
+ console.info(` added: ${skillName} (new skill)`);
28
+ added++;
25
29
  continue;
26
30
  }
27
31
  const sourceHash = fileHash(sourceFile);
28
32
  const targetHash = fileHash(targetFile);
29
33
  if (sourceHash === targetHash) {
30
34
  console.info(` current: ${skillName}`);
31
- skipped++;
35
+ current++;
32
36
  }
33
37
  else {
34
38
  cpSync(sourceFile, targetFile);
@@ -36,5 +40,101 @@ export async function update(projectRoot) {
36
40
  updated++;
37
41
  }
38
42
  }
39
- console.info(`\n${updated} updated, ${skipped} current.`);
43
+ console.info(`\n${added} added, ${updated} updated, ${current} current.`);
44
+ // Sync community lessons (only community- prefixed files)
45
+ console.info("\nChecking for lesson updates...\n");
46
+ const lessonsSource = join(packageRoot, "lessons/community");
47
+ const lessonsTarget = join(projectRoot, ".claude/lessons");
48
+ let lessonsAdded = 0;
49
+ let lessonsUpdated = 0;
50
+ let lessonsCurrent = 0;
51
+ if (existsSync(lessonsSource)) {
52
+ mkdirSync(lessonsTarget, { recursive: true });
53
+ const lessonFiles = globSync("community-*.md", { cwd: lessonsSource });
54
+ for (const file of lessonFiles) {
55
+ const sourceFile = join(lessonsSource, file);
56
+ const targetFile = join(lessonsTarget, file);
57
+ if (!existsSync(targetFile)) {
58
+ cpSync(sourceFile, targetFile);
59
+ console.info(` added: ${file}`);
60
+ lessonsAdded++;
61
+ continue;
62
+ }
63
+ const sourceH = fileHash(sourceFile);
64
+ const targetH = fileHash(targetFile);
65
+ if (sourceH === targetH) {
66
+ console.info(` current: ${file}`);
67
+ lessonsCurrent++;
68
+ }
69
+ else {
70
+ cpSync(sourceFile, targetFile);
71
+ console.info(` updated: ${file}`);
72
+ lessonsUpdated++;
73
+ }
74
+ }
75
+ }
76
+ console.info(`\n${lessonsAdded} added, ${lessonsUpdated} updated, ${lessonsCurrent} current.`);
77
+ // Sync installed domain skills (only update ones already installed)
78
+ console.info("\nChecking for domain skill updates...\n");
79
+ const domainSource = join(packageRoot, "skills/domain");
80
+ let domainUpdated = 0;
81
+ let domainCurrent = 0;
82
+ if (existsSync(domainSource)) {
83
+ const domainFiles = globSync("*.md", { cwd: domainSource });
84
+ for (const file of domainFiles) {
85
+ const skillName = file.replace(".md", "");
86
+ const sourceFile = join(domainSource, file);
87
+ const targetFile = join(skillsTarget, skillName, "SKILL.md");
88
+ // Only update if already installed — don't install new domain skills during update
89
+ if (!existsSync(targetFile))
90
+ continue;
91
+ const sourceH = fileHash(sourceFile);
92
+ const targetH = fileHash(targetFile);
93
+ if (sourceH === targetH) {
94
+ console.info(` current: ${skillName}`);
95
+ domainCurrent++;
96
+ }
97
+ else {
98
+ cpSync(sourceFile, targetFile);
99
+ console.info(` updated: ${skillName}`);
100
+ domainUpdated++;
101
+ }
102
+ }
103
+ }
104
+ if (domainUpdated + domainCurrent > 0) {
105
+ console.info(`\n${domainUpdated} updated, ${domainCurrent} current.`);
106
+ }
107
+ else {
108
+ console.info(" no domain skills installed");
109
+ }
110
+ // Sync hook scripts (only if hooks directory exists in target)
111
+ console.info("\nChecking for hook updates...\n");
112
+ const hooksSource = join(packageRoot, "hooks");
113
+ const hooksTarget = join(projectRoot, ".claude/hooks");
114
+ let hooksUpdated = 0;
115
+ let hooksCurrent = 0;
116
+ if (existsSync(hooksSource) && existsSync(hooksTarget)) {
117
+ const hookFiles = ["check-gates.js", "gate-reminder.js"];
118
+ for (const file of hookFiles) {
119
+ const sourceFile = join(hooksSource, file);
120
+ const targetFile = join(hooksTarget, file);
121
+ if (!existsSync(sourceFile) || !existsSync(targetFile))
122
+ continue;
123
+ const sourceH = fileHash(sourceFile);
124
+ const targetH = fileHash(targetFile);
125
+ if (sourceH === targetH) {
126
+ console.info(` current: ${file}`);
127
+ hooksCurrent++;
128
+ }
129
+ else {
130
+ cpSync(sourceFile, targetFile);
131
+ console.info(` updated: ${file}`);
132
+ hooksUpdated++;
133
+ }
134
+ }
135
+ console.info(`\n${hooksUpdated} updated, ${hooksCurrent} current.`);
136
+ }
137
+ else {
138
+ console.info(" hooks not installed (run init to install)");
139
+ }
40
140
  }
@@ -11,6 +11,8 @@ export interface ImplPhase {
11
11
  number: number;
12
12
  name: string;
13
13
  gates: PhaseGate[];
14
+ blocker: string | null;
15
+ forwardIntelligence: string | null;
14
16
  }
15
17
  export interface ParsedImpl {
16
18
  title: string;
@@ -27,6 +27,8 @@ export function parseImplString(raw) {
27
27
  let currentPhase = null;
28
28
  let currentGateType = "implementation";
29
29
  let currentGateLines = [];
30
+ let inForwardIntelligence = false;
31
+ let forwardIntelligenceLines = [];
30
32
  function flushGate() {
31
33
  if (!currentPhase)
32
34
  return;
@@ -47,21 +49,51 @@ export function parseImplString(raw) {
47
49
  number: Number.parseInt(phaseMatch[1], 10),
48
50
  name: phaseMatch[2].trim(),
49
51
  gates: [],
52
+ blocker: null,
53
+ forwardIntelligence: null,
50
54
  };
51
55
  currentGateType = "implementation";
52
56
  currentGateLines = [];
53
57
  continue;
54
58
  }
59
+ // Forward Intelligence header: #### Phase N Forward Intelligence
60
+ const fiMatch = line.match(/^####\s+Phase\s+\d+\s+Forward Intelligence\b/);
61
+ if (fiMatch) {
62
+ flushGate();
63
+ inForwardIntelligence = true;
64
+ forwardIntelligenceLines = [];
65
+ continue;
66
+ }
55
67
  // Gate header: #### Phase N Verification|Context|Document
56
68
  const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|Context|Document)\b/);
57
69
  if (gateMatch) {
70
+ if (inForwardIntelligence && currentPhase) {
71
+ currentPhase.forwardIntelligence = forwardIntelligenceLines.join("\n").trim() || null;
72
+ inForwardIntelligence = false;
73
+ }
58
74
  flushGate();
59
75
  currentGateType = GATE_SUFFIXES[gateMatch[1]];
60
76
  continue;
61
77
  }
62
- currentGateLines.push(line);
78
+ // Blocker line: blocker: description
79
+ if (currentPhase) {
80
+ const blockerMatch = line.match(/^blocker:\s+(.*)/);
81
+ if (blockerMatch) {
82
+ currentPhase.blocker = blockerMatch[1].trim();
83
+ continue;
84
+ }
85
+ }
86
+ if (inForwardIntelligence) {
87
+ forwardIntelligenceLines.push(line);
88
+ }
89
+ else {
90
+ currentGateLines.push(line);
91
+ }
92
+ }
93
+ // Flush last forward intelligence, gate, and phase
94
+ if (inForwardIntelligence && currentPhase) {
95
+ currentPhase.forwardIntelligence = forwardIntelligenceLines.join("\n").trim() || null;
63
96
  }
64
- // Flush last gate and phase
65
97
  flushGate();
66
98
  if (currentPhase)
67
99
  phases.push(currentPhase);
@@ -0,0 +1,6 @@
1
+ export interface DiscoveredCheck {
2
+ name: string;
3
+ command: string;
4
+ source: "package.json" | "config-file";
5
+ }
6
+ export declare function discoverVerificationCommands(projectRoot: string): DiscoveredCheck[];
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { globSync } from "glob";
4
+ const SCRIPT_PATTERNS = {
5
+ typecheck: "typecheck",
6
+ "type-check": "type-check",
7
+ lint: "lint",
8
+ test: "test",
9
+ build: "build",
10
+ check: "check",
11
+ "check:fix": "check:fix",
12
+ };
13
+ export function discoverVerificationCommands(projectRoot) {
14
+ const checks = [];
15
+ // 1. Read package.json scripts
16
+ const pkgPath = join(projectRoot, "package.json");
17
+ if (existsSync(pkgPath)) {
18
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
19
+ const scripts = pkg.scripts ?? {};
20
+ for (const [pattern, name] of Object.entries(SCRIPT_PATTERNS)) {
21
+ if (scripts[pattern]) {
22
+ const runner = existsSync(join(projectRoot, "pnpm-lock.yaml"))
23
+ ? "pnpm"
24
+ : existsSync(join(projectRoot, "yarn.lock"))
25
+ ? "yarn"
26
+ : "npm";
27
+ checks.push({
28
+ name,
29
+ command: `${runner} run ${pattern}`,
30
+ source: "package.json",
31
+ });
32
+ }
33
+ }
34
+ // Check for tsc script variants
35
+ if (!checks.some((c) => c.name === "typecheck") && scripts.tsc) {
36
+ checks.push({ name: "typecheck", command: "npm run tsc", source: "package.json" });
37
+ }
38
+ }
39
+ // 2. Detect tool configs
40
+ if (existsSync(join(projectRoot, "biome.json")) && !checks.some((c) => c.name === "check")) {
41
+ checks.push({ name: "biome", command: "npx biome check", source: "config-file" });
42
+ }
43
+ if (existsSync(join(projectRoot, "tsconfig.json")) &&
44
+ !checks.some((c) => c.name === "typecheck")) {
45
+ checks.push({ name: "typecheck", command: "npx tsc --noEmit", source: "config-file" });
46
+ }
47
+ const vitestConfigs = globSync("vitest.config.*", { cwd: projectRoot });
48
+ if (vitestConfigs.length > 0 && !checks.some((c) => c.name === "test")) {
49
+ checks.push({ name: "test", command: "npx vitest run", source: "config-file" });
50
+ }
51
+ const jestConfigs = globSync("jest.config.*", { cwd: projectRoot });
52
+ if (jestConfigs.length > 0 && !checks.some((c) => c.name === "test")) {
53
+ checks.push({ name: "test", command: "npx jest", source: "config-file" });
54
+ }
55
+ return checks;
56
+ }
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { registerContextTools } from "../tools/context-tools.js";
5
5
  import { registerDocumentTools } from "../tools/document-tools.js";
6
6
  import { registerGraphTools } from "../tools/graph-tools.js";
7
+ import { registerLessonTools } from "../tools/lesson-tools.js";
7
8
  import { registerPlanTools } from "../tools/plan-tools.js";
8
9
  import { registerQualityTools } from "../tools/quality-tools.js";
9
10
  import { registerSystemTools } from "../tools/system-tools.js";
@@ -19,6 +20,7 @@ export async function startServer() {
19
20
  registerDocumentTools(server, projectRoot);
20
21
  registerSystemTools(server, projectRoot);
21
22
  registerGraphTools(server, projectRoot);
23
+ registerLessonTools(server, projectRoot);
22
24
  const transport = new StdioServerTransport();
23
25
  await server.connect(transport);
24
26
  }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerLessonTools(server: McpServer, projectRoot: string): void;