@infinitedusky/indusk-mcp 0.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
8
+ const program = new Command();
9
+ program
10
+ .name("dev-system")
11
+ .description("InDusk development system — skills, MCP tools, and CLI")
12
+ .version(pkg.version);
13
+ program
14
+ .command("init")
15
+ .description("Initialize a project with InDusk dev system")
16
+ .action(async () => {
17
+ const { init } = await import("./commands/init.js");
18
+ await init(process.cwd());
19
+ });
20
+ program
21
+ .command("update")
22
+ .description("Update skills from package without touching project content")
23
+ .action(async () => {
24
+ const { update } = await import("./commands/update.js");
25
+ await update(process.cwd());
26
+ });
27
+ program
28
+ .command("serve")
29
+ .description("Start the MCP server (used by Claude Code via .mcp.json)")
30
+ .action(async () => {
31
+ const { startServer } = await import("../server/index.js");
32
+ await startServer();
33
+ });
34
+ program.parse();
@@ -0,0 +1 @@
1
+ export declare function init(projectRoot: string): Promise<void>;
@@ -0,0 +1,213 @@
1
+ import { execSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { globSync } from "glob";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const packageRoot = join(__dirname, "../../..");
8
+ function run(cmd, options) {
9
+ try {
10
+ return execSync(cmd, {
11
+ encoding: "utf-8",
12
+ timeout: 15000,
13
+ stdio: options?.stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"],
14
+ }).trim();
15
+ }
16
+ catch {
17
+ return "";
18
+ }
19
+ }
20
+ function ensureFalkorDB() {
21
+ // Check if FalkorDB container exists and is running
22
+ const status = run('docker ps --filter name=falkordb --format "{{.Status}}"');
23
+ if (status) {
24
+ console.info(" ok: FalkorDB container running");
25
+ return;
26
+ }
27
+ // Check if container exists but is stopped
28
+ const stopped = run('docker ps -a --filter name=falkordb --format "{{.Status}}"');
29
+ if (stopped) {
30
+ console.info(" start: FalkorDB container (was stopped)");
31
+ run("docker start falkordb");
32
+ return;
33
+ }
34
+ // Create and start the container
35
+ console.info(" create: FalkorDB container (global, persistent)");
36
+ run("docker run -d --name falkordb --restart unless-stopped -p 6379:6379 -v falkordb-global:/data falkordb/falkordb:latest");
37
+ }
38
+ function checkCGC() {
39
+ const cgcPaths = [join(process.env.HOME ?? "", ".local/bin/cgc"), "/usr/local/bin/cgc"];
40
+ const found = cgcPaths.find((p) => existsSync(p));
41
+ if (found) {
42
+ console.info(` ok: CGC found at ${found}`);
43
+ return true;
44
+ }
45
+ console.info(" missing: CGC — install via: pipx install codegraphcontext");
46
+ return false;
47
+ }
48
+ function createCgcIgnore(projectRoot) {
49
+ const ignorePath = join(projectRoot, ".cgcignore");
50
+ if (existsSync(ignorePath)) {
51
+ console.info(" skip: .cgcignore (already exists)");
52
+ return;
53
+ }
54
+ writeFileSync(ignorePath, [
55
+ "node_modules/",
56
+ ".next/",
57
+ "dist/",
58
+ "build/",
59
+ ".git/",
60
+ "*.png",
61
+ "*.jpg",
62
+ "*.svg",
63
+ "*.ico",
64
+ "*.woff",
65
+ "*.woff2",
66
+ "*.lock",
67
+ "pnpm-lock.yaml",
68
+ "package-lock.json",
69
+ "",
70
+ ].join("\n"));
71
+ console.info(" create: .cgcignore");
72
+ }
73
+ export async function init(projectRoot) {
74
+ const projectName = basename(projectRoot);
75
+ console.info("Initializing InDusk dev system...\n");
76
+ // 1. Copy skills
77
+ console.info("[Skills]");
78
+ const skillsSource = join(packageRoot, "skills");
79
+ const skillsTarget = join(projectRoot, ".claude/skills");
80
+ const skillFiles = globSync("*.md", { cwd: skillsSource });
81
+ for (const file of skillFiles) {
82
+ const skillName = file.replace(".md", "");
83
+ const targetDir = join(skillsTarget, skillName);
84
+ const targetFile = join(targetDir, "SKILL.md");
85
+ if (existsSync(targetFile)) {
86
+ console.info(` skip: .claude/skills/${skillName}/SKILL.md (already exists)`);
87
+ continue;
88
+ }
89
+ mkdirSync(targetDir, { recursive: true });
90
+ cpSync(join(skillsSource, file), targetFile);
91
+ console.info(` create: .claude/skills/${skillName}/SKILL.md`);
92
+ }
93
+ // 2. Create CLAUDE.md
94
+ console.info("\n[Project files]");
95
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
96
+ if (existsSync(claudeMdPath)) {
97
+ console.info(" skip: CLAUDE.md (already exists)");
98
+ }
99
+ else {
100
+ cpSync(join(packageRoot, "templates/CLAUDE.md"), claudeMdPath);
101
+ console.info(" create: CLAUDE.md");
102
+ }
103
+ // 3. Create planning directory
104
+ const planningDir = join(projectRoot, "planning");
105
+ if (existsSync(planningDir)) {
106
+ console.info(" skip: planning/ (already exists)");
107
+ }
108
+ else {
109
+ mkdirSync(planningDir, { recursive: true });
110
+ console.info(" create: planning/");
111
+ }
112
+ // 4. Set up .mcp.json with both indusk and codegraphcontext
113
+ console.info("\n[MCP config]");
114
+ const mcpJsonPath = join(projectRoot, ".mcp.json");
115
+ const induskEntry = {
116
+ command: "npx",
117
+ args: ["@infinitedusky/indusk-mcp", "serve"],
118
+ env: { PROJECT_ROOT: "." },
119
+ };
120
+ const cgcEntry = {
121
+ command: "cgc",
122
+ args: ["mcp", "start"],
123
+ env: {
124
+ DATABASE_TYPE: "falkordb-remote",
125
+ FALKORDB_HOST: "localhost",
126
+ FALKORDB_PORT: "6379",
127
+ FALKORDB_GRAPH_NAME: projectName,
128
+ },
129
+ };
130
+ if (existsSync(mcpJsonPath)) {
131
+ const existing = JSON.parse(readFileSync(mcpJsonPath, "utf-8"));
132
+ let updated = false;
133
+ existing.mcpServers = existing.mcpServers || {};
134
+ if (!existing.mcpServers.indusk) {
135
+ existing.mcpServers.indusk = induskEntry;
136
+ console.info(" update: .mcp.json (added indusk entry)");
137
+ updated = true;
138
+ }
139
+ else {
140
+ console.info(" skip: .mcp.json indusk entry (already exists)");
141
+ }
142
+ if (!existing.mcpServers.codegraphcontext) {
143
+ existing.mcpServers.codegraphcontext = cgcEntry;
144
+ console.info(` update: .mcp.json (added codegraphcontext, graph: ${projectName})`);
145
+ updated = true;
146
+ }
147
+ else {
148
+ console.info(" skip: .mcp.json codegraphcontext entry (already exists)");
149
+ }
150
+ if (updated) {
151
+ writeFileSync(mcpJsonPath, `${JSON.stringify(existing, null, "\t")}\n`);
152
+ }
153
+ }
154
+ else {
155
+ const mcpJson = {
156
+ mcpServers: {
157
+ indusk: induskEntry,
158
+ codegraphcontext: cgcEntry,
159
+ },
160
+ };
161
+ writeFileSync(mcpJsonPath, `${JSON.stringify(mcpJson, null, "\t")}\n`);
162
+ console.info(" create: .mcp.json (indusk + codegraphcontext)");
163
+ }
164
+ // 5. Generate .vscode/settings.json
165
+ console.info("\n[Editor]");
166
+ const vscodePath = join(projectRoot, ".vscode/settings.json");
167
+ if (existsSync(vscodePath)) {
168
+ console.info(" skip: .vscode/settings.json (already exists)");
169
+ }
170
+ else {
171
+ mkdirSync(join(projectRoot, ".vscode"), { recursive: true });
172
+ cpSync(join(packageRoot, "templates/vscode-settings.json"), vscodePath);
173
+ console.info(" create: .vscode/settings.json");
174
+ }
175
+ // 6. Create base biome.json
176
+ const biomePath = join(projectRoot, "biome.json");
177
+ if (existsSync(biomePath)) {
178
+ console.info(" skip: biome.json (already exists)");
179
+ }
180
+ else {
181
+ cpSync(join(packageRoot, "templates/biome.template.json"), biomePath);
182
+ console.info(" create: biome.json");
183
+ }
184
+ // 7. Create .cgcignore
185
+ createCgcIgnore(projectRoot);
186
+ // 8. Infrastructure: FalkorDB + CGC
187
+ console.info("\n[Infrastructure]");
188
+ const dockerAvailable = run("docker info") !== "";
189
+ if (dockerAvailable) {
190
+ ensureFalkorDB();
191
+ }
192
+ else {
193
+ console.info(" missing: Docker — install Docker or OrbStack to enable FalkorDB");
194
+ }
195
+ const cgcInstalled = checkCGC();
196
+ // Summary
197
+ console.info("\nDone!");
198
+ if (!cgcInstalled || !dockerAvailable) {
199
+ console.info("\nManual steps needed:");
200
+ if (!dockerAvailable) {
201
+ console.info(" 1. Install Docker or OrbStack");
202
+ console.info(" 2. Re-run init to set up FalkorDB");
203
+ }
204
+ if (!cgcInstalled) {
205
+ console.info(" - Install CGC: pipx install codegraphcontext");
206
+ }
207
+ }
208
+ console.info("\nNext steps:");
209
+ console.info(" 1. Edit CLAUDE.md with your project details");
210
+ console.info(" 2. Install Biome: pnpm add -D @biomejs/biome");
211
+ console.info(" 3. Start a Claude Code session — MCP tools will be available");
212
+ console.info(" 4. Start planning: /plan your-first-feature");
213
+ }
@@ -0,0 +1 @@
1
+ export declare function update(projectRoot: string): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { createHash } from "node:crypto";
2
+ import { cpSync, existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { globSync } from "glob";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const packageRoot = join(__dirname, "../../..");
8
+ function fileHash(path) {
9
+ return createHash("sha256").update(readFileSync(path)).digest("hex").slice(0, 12);
10
+ }
11
+ export async function update(projectRoot) {
12
+ console.info("Checking for skill updates...\n");
13
+ const skillsSource = join(packageRoot, "skills");
14
+ const skillsTarget = join(projectRoot, ".claude/skills");
15
+ const skillFiles = globSync("*.md", { cwd: skillsSource });
16
+ let updated = 0;
17
+ let skipped = 0;
18
+ for (const file of skillFiles) {
19
+ const skillName = file.replace(".md", "");
20
+ const sourceFile = join(skillsSource, file);
21
+ const targetFile = join(skillsTarget, skillName, "SKILL.md");
22
+ if (!existsSync(targetFile)) {
23
+ console.info(` skip: ${skillName} (not installed — run init first)`);
24
+ skipped++;
25
+ continue;
26
+ }
27
+ const sourceHash = fileHash(sourceFile);
28
+ const targetHash = fileHash(targetFile);
29
+ if (sourceHash === targetHash) {
30
+ console.info(` current: ${skillName}`);
31
+ skipped++;
32
+ }
33
+ else {
34
+ cpSync(sourceFile, targetFile);
35
+ console.info(` updated: ${skillName}`);
36
+ updated++;
37
+ }
38
+ }
39
+ console.info(`\n${updated} updated, ${skipped} current.`);
40
+ }
@@ -0,0 +1,26 @@
1
+ export declare const SECTION_NAMES: readonly ["What This Is", "Architecture", "Conventions", "Key Decisions", "Known Gotchas", "Current State"];
2
+ export type SectionName = (typeof SECTION_NAMES)[number];
3
+ export interface ContextSection {
4
+ name: SectionName;
5
+ content: string;
6
+ }
7
+ export interface ParsedContext {
8
+ title: string;
9
+ sections: ContextSection[];
10
+ }
11
+ export interface ContextValidation {
12
+ valid: boolean;
13
+ missing: SectionName[];
14
+ extra: string[];
15
+ }
16
+ /**
17
+ * Parse CLAUDE.md into its 6 canonical sections.
18
+ * Sections are split on `## ` headings. Content between the title (H1)
19
+ * and the first H2 is ignored (it's the intro line).
20
+ */
21
+ export declare function parseContext(filePath: string): ParsedContext;
22
+ export declare function parseContextString(raw: string): ParsedContext;
23
+ export declare function validateContext(parsed: ParsedContext): ContextValidation;
24
+ export declare function getSection(parsed: ParsedContext, name: SectionName): string | null;
25
+ export declare function updateSection(filePath: string, name: SectionName, newContent: string): void;
26
+ export declare function updateSectionString(raw: string, name: SectionName, newContent: string): string;
@@ -0,0 +1,86 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ export const SECTION_NAMES = [
3
+ "What This Is",
4
+ "Architecture",
5
+ "Conventions",
6
+ "Key Decisions",
7
+ "Known Gotchas",
8
+ "Current State",
9
+ ];
10
+ /**
11
+ * Parse CLAUDE.md into its 6 canonical sections.
12
+ * Sections are split on `## ` headings. Content between the title (H1)
13
+ * and the first H2 is ignored (it's the intro line).
14
+ */
15
+ export function parseContext(filePath) {
16
+ if (!existsSync(filePath)) {
17
+ return { title: "", sections: [] };
18
+ }
19
+ const raw = readFileSync(filePath, "utf-8");
20
+ return parseContextString(raw);
21
+ }
22
+ export function parseContextString(raw) {
23
+ const lines = raw.split("\n");
24
+ // Extract H1 title
25
+ const titleLine = lines.find((l) => l.startsWith("# "));
26
+ const title = titleLine ? titleLine.replace(/^# /, "").trim() : "";
27
+ // Split on ## headings
28
+ const sections = [];
29
+ let currentName = null;
30
+ let currentLines = [];
31
+ for (const line of lines) {
32
+ if (line.startsWith("## ")) {
33
+ if (currentName !== null) {
34
+ sections.push({
35
+ name: currentName,
36
+ content: currentLines.join("\n").trim(),
37
+ });
38
+ }
39
+ currentName = line.replace(/^## /, "").trim();
40
+ currentLines = [];
41
+ }
42
+ else if (currentName !== null) {
43
+ currentLines.push(line);
44
+ }
45
+ }
46
+ // Push last section
47
+ if (currentName !== null) {
48
+ sections.push({
49
+ name: currentName,
50
+ content: currentLines.join("\n").trim(),
51
+ });
52
+ }
53
+ return { title, sections };
54
+ }
55
+ export function validateContext(parsed) {
56
+ const found = new Set(parsed.sections.map((s) => s.name));
57
+ const missing = SECTION_NAMES.filter((n) => !found.has(n));
58
+ const canonical = new Set(SECTION_NAMES);
59
+ const extra = parsed.sections.map((s) => s.name).filter((n) => !canonical.has(n));
60
+ return {
61
+ valid: missing.length === 0 && extra.length === 0,
62
+ missing: missing,
63
+ extra,
64
+ };
65
+ }
66
+ export function getSection(parsed, name) {
67
+ const section = parsed.sections.find((s) => s.name === name);
68
+ return section?.content ?? null;
69
+ }
70
+ export function updateSection(filePath, name, newContent) {
71
+ const raw = readFileSync(filePath, "utf-8");
72
+ const updated = updateSectionString(raw, name, newContent);
73
+ writeFileSync(filePath, updated);
74
+ }
75
+ export function updateSectionString(raw, name, newContent) {
76
+ const header = `## ${name}`;
77
+ const headerIdx = raw.indexOf(header);
78
+ if (headerIdx === -1) {
79
+ throw new Error(`Section "${name}" not found in CLAUDE.md`);
80
+ }
81
+ // Find the end of this section (next ## or EOF)
82
+ const afterHeader = headerIdx + header.length;
83
+ const nextSectionMatch = raw.slice(afterHeader).search(/\n## /);
84
+ const endIdx = nextSectionMatch === -1 ? raw.length : afterHeader + nextSectionMatch;
85
+ return `${raw.slice(0, afterHeader)}\n\n${newContent.trim()}\n${raw.slice(endIdx)}`;
86
+ }
@@ -0,0 +1,31 @@
1
+ export type GateType = "implementation" | "verification" | "context" | "document";
2
+ export interface ChecklistItem {
3
+ checked: boolean;
4
+ text: string;
5
+ }
6
+ export interface PhaseGate {
7
+ type: GateType;
8
+ items: ChecklistItem[];
9
+ }
10
+ export interface ImplPhase {
11
+ number: number;
12
+ name: string;
13
+ gates: PhaseGate[];
14
+ }
15
+ export interface ParsedImpl {
16
+ title: string;
17
+ status: string;
18
+ phases: ImplPhase[];
19
+ }
20
+ export declare function parseImplString(raw: string): ParsedImpl;
21
+ export declare function parseImpl(filePath: string): ParsedImpl;
22
+ export interface PhaseCompletion {
23
+ phase: number;
24
+ name: string;
25
+ complete: boolean;
26
+ totalItems: number;
27
+ checkedItems: number;
28
+ uncheckedByGate: Record<GateType, string[]>;
29
+ }
30
+ export declare function getPhaseCompletion(phase: ImplPhase): PhaseCompletion;
31
+ export declare function getAllPhaseCompletions(parsed: ParsedImpl): PhaseCompletion[];
@@ -0,0 +1,107 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import matter from "gray-matter";
3
+ const GATE_SUFFIXES = {
4
+ Verification: "verification",
5
+ Context: "context",
6
+ Document: "document",
7
+ };
8
+ function parseChecklistItems(lines) {
9
+ const items = [];
10
+ for (const line of lines) {
11
+ const match = line.match(/^-\s+\[([ x])\]\s+(.*)/);
12
+ if (match) {
13
+ items.push({
14
+ checked: match[1] === "x",
15
+ text: match[2].trim(),
16
+ });
17
+ }
18
+ }
19
+ return items;
20
+ }
21
+ export function parseImplString(raw) {
22
+ const { data, content } = matter(raw);
23
+ const title = data.title ?? "";
24
+ const status = data.status ?? "";
25
+ const lines = content.split("\n");
26
+ const phases = [];
27
+ let currentPhase = null;
28
+ let currentGateType = "implementation";
29
+ let currentGateLines = [];
30
+ function flushGate() {
31
+ if (!currentPhase)
32
+ return;
33
+ const items = parseChecklistItems(currentGateLines);
34
+ if (items.length > 0) {
35
+ currentPhase.gates.push({ type: currentGateType, items });
36
+ }
37
+ currentGateLines = [];
38
+ }
39
+ for (const line of lines) {
40
+ // Phase header: ### Phase N: Name
41
+ const phaseMatch = line.match(/^###\s+Phase\s+(\d+)[:\s]+(.*)/);
42
+ if (phaseMatch) {
43
+ flushGate();
44
+ if (currentPhase)
45
+ phases.push(currentPhase);
46
+ currentPhase = {
47
+ number: Number.parseInt(phaseMatch[1], 10),
48
+ name: phaseMatch[2].trim(),
49
+ gates: [],
50
+ };
51
+ currentGateType = "implementation";
52
+ currentGateLines = [];
53
+ continue;
54
+ }
55
+ // Gate header: #### Phase N Verification|Context|Document
56
+ const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|Context|Document)\b/);
57
+ if (gateMatch) {
58
+ flushGate();
59
+ currentGateType = GATE_SUFFIXES[gateMatch[1]];
60
+ continue;
61
+ }
62
+ currentGateLines.push(line);
63
+ }
64
+ // Flush last gate and phase
65
+ flushGate();
66
+ if (currentPhase)
67
+ phases.push(currentPhase);
68
+ return { title, status, phases };
69
+ }
70
+ export function parseImpl(filePath) {
71
+ if (!existsSync(filePath)) {
72
+ return { title: "", status: "", phases: [] };
73
+ }
74
+ return parseImplString(readFileSync(filePath, "utf-8"));
75
+ }
76
+ export function getPhaseCompletion(phase) {
77
+ const uncheckedByGate = {
78
+ implementation: [],
79
+ verification: [],
80
+ context: [],
81
+ document: [],
82
+ };
83
+ let totalItems = 0;
84
+ let checkedItems = 0;
85
+ for (const gate of phase.gates) {
86
+ for (const item of gate.items) {
87
+ totalItems++;
88
+ if (item.checked) {
89
+ checkedItems++;
90
+ }
91
+ else {
92
+ uncheckedByGate[gate.type].push(item.text);
93
+ }
94
+ }
95
+ }
96
+ return {
97
+ phase: phase.number,
98
+ name: phase.name,
99
+ complete: checkedItems === totalItems,
100
+ totalItems,
101
+ checkedItems,
102
+ uncheckedByGate,
103
+ };
104
+ }
105
+ export function getAllPhaseCompletions(parsed) {
106
+ return parsed.phases.map(getPhaseCompletion);
107
+ }
@@ -0,0 +1,16 @@
1
+ export interface PlanFrontmatter {
2
+ title: string;
3
+ date: string;
4
+ status: string;
5
+ }
6
+ export type PlanStage = "research" | "brief" | "adr" | "impl" | "retrospective" | "unknown";
7
+ export interface PlanSummary {
8
+ name: string;
9
+ stage: PlanStage;
10
+ stageStatus: string;
11
+ nextStep: string;
12
+ dependencies: string[];
13
+ documents: string[];
14
+ }
15
+ export declare function parsePlan(planDir: string): PlanSummary;
16
+ export declare function parseAllPlans(projectRoot: string): PlanSummary[];
@@ -0,0 +1,82 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import matter from "gray-matter";
4
+ const STAGE_ORDER = ["research", "brief", "adr", "impl", "retrospective"];
5
+ function parseFrontmatter(filePath) {
6
+ if (!existsSync(filePath))
7
+ return null;
8
+ const raw = readFileSync(filePath, "utf-8");
9
+ const { data } = matter(raw);
10
+ return {
11
+ title: data.title ?? "",
12
+ date: data.date ?? "",
13
+ status: data.status ?? "",
14
+ };
15
+ }
16
+ function parseDependsOn(filePath) {
17
+ if (!existsSync(filePath))
18
+ return [];
19
+ const content = readFileSync(filePath, "utf-8");
20
+ const depsMatch = content.match(/## Depends On\s*\n([\s\S]*?)(?=\n## |\n$|$)/);
21
+ if (!depsMatch)
22
+ return [];
23
+ const deps = [];
24
+ for (const line of depsMatch[1].split("\n")) {
25
+ const match = line.match(/^-\s+`?planning\/([^/`]+)\/?`?/);
26
+ if (match) {
27
+ deps.push(match[1]);
28
+ }
29
+ }
30
+ return deps;
31
+ }
32
+ function determineStage(planDir, docs) {
33
+ // Walk stages in reverse to find the most advanced document
34
+ for (let i = STAGE_ORDER.length - 1; i >= 0; i--) {
35
+ const stage = STAGE_ORDER[i];
36
+ const file = `${stage}.md`;
37
+ if (docs.includes(file)) {
38
+ const fm = parseFrontmatter(join(planDir, file));
39
+ return { stage, stageStatus: fm?.status ?? "unknown" };
40
+ }
41
+ }
42
+ return { stage: "unknown", stageStatus: "unknown" };
43
+ }
44
+ function determineNextStep(stage, stageStatus) {
45
+ if (stage === "unknown")
46
+ return "Create a brief";
47
+ const idx = STAGE_ORDER.indexOf(stage);
48
+ if (stageStatus === "completed" || stageStatus === "accepted") {
49
+ const next = STAGE_ORDER[idx + 1];
50
+ if (next)
51
+ return `Create ${next}`;
52
+ return "Done";
53
+ }
54
+ if (stageStatus === "in-progress") {
55
+ return `Continue ${stage}`;
56
+ }
57
+ return `Review ${stage} (status: ${stageStatus})`;
58
+ }
59
+ export function parsePlan(planDir) {
60
+ const name = planDir.split("/").pop() ?? "";
61
+ const entries = readdirSync(planDir).filter((f) => f.endsWith(".md"));
62
+ const { stage, stageStatus } = determineStage(planDir, entries);
63
+ const dependencies = parseDependsOn(join(planDir, "brief.md"));
64
+ const nextStep = determineNextStep(stage, stageStatus);
65
+ return {
66
+ name,
67
+ stage,
68
+ stageStatus,
69
+ nextStep,
70
+ dependencies,
71
+ documents: entries,
72
+ };
73
+ }
74
+ export function parseAllPlans(projectRoot) {
75
+ const planningDir = join(projectRoot, "planning");
76
+ if (!existsSync(planningDir))
77
+ return [];
78
+ return readdirSync(planningDir, { withFileTypes: true })
79
+ .filter((d) => d.isDirectory())
80
+ .map((d) => parsePlan(join(planningDir, d.name)))
81
+ .sort((a, b) => a.name.localeCompare(b.name));
82
+ }
@@ -0,0 +1 @@
1
+ export declare function startServer(): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { resolve } from "node:path";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerContextTools } from "../tools/context-tools.js";
5
+ import { registerDocumentTools } from "../tools/document-tools.js";
6
+ import { registerPlanTools } from "../tools/plan-tools.js";
7
+ import { registerQualityTools } from "../tools/quality-tools.js";
8
+ import { registerSystemTools } from "../tools/system-tools.js";
9
+ export async function startServer() {
10
+ const projectRoot = resolve(process.env.PROJECT_ROOT ?? ".");
11
+ const server = new McpServer({
12
+ name: "indusk",
13
+ version: "0.1.0",
14
+ });
15
+ registerPlanTools(server, projectRoot);
16
+ registerContextTools(server, projectRoot);
17
+ registerQualityTools(server, projectRoot);
18
+ registerDocumentTools(server, projectRoot);
19
+ registerSystemTools(server, projectRoot);
20
+ const transport = new StdioServerTransport();
21
+ await server.connect(transport);
22
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerContextTools(server: McpServer, projectRoot: string): void;