@infinitedusky/indusk-mcp 0.6.0 → 0.6.2

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
@@ -16,12 +16,14 @@ program
16
16
  .option("-f, --force", "Overwrite existing files (except CLAUDE.md and planning/)")
17
17
  .option("--skills <list>", "Comma-separated domain skills to install (e.g., nextjs,tailwind)")
18
18
  .option("--no-domain-skills", "Skip domain skill detection and installation")
19
+ .option("--no-index", "Skip code graph indexing")
19
20
  .action(async (opts) => {
20
21
  const { init } = await import("./commands/init.js");
21
22
  await init(process.cwd(), {
22
23
  force: opts.force ?? false,
23
24
  skills: opts.skills,
24
25
  noDomainSkills: opts.domainSkills === false,
26
+ noIndex: opts.index === false,
25
27
  });
26
28
  });
27
29
  program
@@ -2,5 +2,6 @@ export interface InitOptions {
2
2
  force?: boolean;
3
3
  skills?: string;
4
4
  noDomainSkills?: boolean;
5
+ noIndex?: boolean;
5
6
  }
6
7
  export declare function init(projectRoot: string, options?: InitOptions): Promise<void>;
@@ -119,7 +119,7 @@ function detectDomainSkills(projectRoot) {
119
119
  return detections;
120
120
  }
121
121
  export async function init(projectRoot, options = {}) {
122
- const { force = false, skills, noDomainSkills = false } = options;
122
+ const { force = false, skills, noDomainSkills = false, noIndex = false } = options;
123
123
  const projectName = basename(projectRoot);
124
124
  console.info(`Initializing InDusk dev system...${force ? " (--force)" : ""}\n`);
125
125
  // 1. Copy skills
@@ -371,7 +371,11 @@ export async function init(projectRoot, options = {}) {
371
371
  }
372
372
  const cgcInstalled = checkCGC();
373
373
  // 9. Auto-index the codebase into the graph
374
- if (dockerAvailable && cgcInstalled) {
374
+ if (noIndex) {
375
+ console.info("\n[Code Graph]");
376
+ console.info(" skipped (--no-index)");
377
+ }
378
+ else if (dockerAvailable && cgcInstalled) {
375
379
  console.info("\n[Code Graph]");
376
380
  console.info(" indexing: scanning codebase...");
377
381
  const { indexProject } = await import("../../tools/graph-tools.js");
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse hook: blocks phase transitions in impl.md when gates are incomplete.
4
+ *
5
+ * Exit 0 = allow the edit
6
+ * Exit 2 = block the edit (stderr sent to agent as feedback)
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+
11
+ // Read hook input from stdin
12
+ let input = "";
13
+ for await (const chunk of process.stdin) {
14
+ input += chunk;
15
+ }
16
+
17
+ const event = JSON.parse(input);
18
+ const toolInput = event.tool_input ?? {};
19
+
20
+ // Determine file path based on tool type
21
+ const filePath = toolInput.file_path ?? "";
22
+
23
+ // Fast path: not an impl.md file
24
+ if (!filePath.endsWith("/impl.md") && !filePath.endsWith("\\impl.md")) {
25
+ process.exit(0);
26
+ }
27
+
28
+ // Check for skip-gates escape hatch
29
+ const newContent = toolInput.new_string ?? toolInput.content ?? "";
30
+ if (newContent.includes("<!-- skip-gates -->")) {
31
+ process.exit(0);
32
+ }
33
+
34
+ // Detect checkbox transition: - [ ] → - [x]
35
+ const oldContent = toolInput.old_string ?? "";
36
+
37
+ // For Edit tool: check if old_string has unchecked and new_string has checked
38
+ // For Write tool: we need to compare with the file on disk
39
+ let hasCheckboxTransition = false;
40
+
41
+ if (event.tool_name === "Edit" && oldContent && newContent) {
42
+ const oldUnchecked = (oldContent.match(/- \[ \]/g) || []).length;
43
+ const newUnchecked = (newContent.match(/- \[ \]/g) || []).length;
44
+ const oldChecked = (oldContent.match(/- \[x\]/g) || []).length;
45
+ const newChecked = (newContent.match(/- \[x\]/g) || []).length;
46
+ hasCheckboxTransition = newChecked > oldChecked || newUnchecked < oldUnchecked;
47
+ } else if (event.tool_name === "Write") {
48
+ // For Write, compare with file on disk
49
+ try {
50
+ const diskContent = readFileSync(filePath, "utf-8");
51
+ const diskChecked = (diskContent.match(/- \[x\]/g) || []).length;
52
+ const writeChecked = (newContent.match(/- \[x\]/g) || []).length;
53
+ hasCheckboxTransition = writeChecked > diskChecked;
54
+ } catch {
55
+ // File doesn't exist yet — new impl, allow
56
+ process.exit(0);
57
+ }
58
+ }
59
+
60
+ if (!hasCheckboxTransition) {
61
+ process.exit(0);
62
+ }
63
+
64
+ // Parse the impl file to understand phase structure
65
+ // Read the full file to get current state, then apply the edit mentally
66
+ let fullContent;
67
+ try {
68
+ fullContent = readFileSync(filePath, "utf-8");
69
+ } catch {
70
+ process.exit(0);
71
+ }
72
+
73
+ // For Edit, apply the edit to get the new full content
74
+ let newFullContent;
75
+ if (event.tool_name === "Edit" && oldContent) {
76
+ newFullContent = fullContent.replace(oldContent, newContent);
77
+ } else if (event.tool_name === "Write") {
78
+ newFullContent = newContent;
79
+ } else {
80
+ process.exit(0);
81
+ }
82
+
83
+ // Parse phases from the NEW content (after edit) and OLD content (before edit)
84
+ function parsePhases(content) {
85
+ // Strip frontmatter
86
+ const fmMatch = content.match(/^---\n[\s\S]*?\n---\n/);
87
+ const body = fmMatch ? content.slice(fmMatch[0].length) : content;
88
+
89
+ const lines = body.split("\n");
90
+ const phases = [];
91
+ let currentPhase = null;
92
+ let currentGateType = "implementation";
93
+
94
+ for (const line of lines) {
95
+ const phaseMatch = line.match(/^###\s+Phase\s+(\d+)[:\s]+(.*)/);
96
+ if (phaseMatch) {
97
+ if (currentPhase) phases.push(currentPhase);
98
+ currentPhase = {
99
+ number: parseInt(phaseMatch[1], 10),
100
+ name: phaseMatch[2].trim(),
101
+ items: [],
102
+ };
103
+ currentGateType = "implementation";
104
+ continue;
105
+ }
106
+
107
+ const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|Context|Document)\b/);
108
+ if (gateMatch) {
109
+ currentGateType = gateMatch[1].toLowerCase();
110
+ continue;
111
+ }
112
+
113
+ // Forward intelligence — skip
114
+ if (line.match(/^####\s+Phase\s+\d+\s+Forward Intelligence\b/)) {
115
+ currentGateType = "_fi";
116
+ continue;
117
+ }
118
+
119
+ if (currentPhase && currentGateType !== "_fi") {
120
+ const itemMatch = line.match(/^-\s+\[([ x])\]\s+(.*)/);
121
+ if (itemMatch) {
122
+ currentPhase.items.push({
123
+ checked: itemMatch[1] === "x",
124
+ text: itemMatch[2].trim(),
125
+ gate: currentGateType,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ if (currentPhase) phases.push(currentPhase);
131
+ return phases;
132
+ }
133
+
134
+ const oldPhases = parsePhases(fullContent);
135
+ const newPhases = parsePhases(newFullContent);
136
+
137
+ // Find which items were just checked (were unchecked before, checked now)
138
+ const newlyChecked = [];
139
+ for (let pi = 0; pi < newPhases.length; pi++) {
140
+ const newPhase = newPhases[pi];
141
+ const oldPhase = oldPhases[pi];
142
+ if (!oldPhase) continue;
143
+
144
+ for (let ii = 0; ii < newPhase.items.length; ii++) {
145
+ const newItem = newPhase.items[ii];
146
+ const oldItem = oldPhase.items[ii];
147
+ if (!oldItem) continue;
148
+
149
+ if (newItem.checked && !oldItem.checked) {
150
+ newlyChecked.push({
151
+ phase: newPhase.number,
152
+ phaseName: newPhase.name,
153
+ text: newItem.text,
154
+ gate: newItem.gate,
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ if (newlyChecked.length === 0) {
161
+ process.exit(0);
162
+ }
163
+
164
+ // For each newly checked item: if it's an implementation item,
165
+ // check that all PREVIOUS phases have complete gates
166
+ for (const item of newlyChecked) {
167
+ // Checking gate items is always allowed
168
+ if (item.gate !== "implementation") continue;
169
+
170
+ // Check all phases before this item's phase
171
+ for (const phase of oldPhases) {
172
+ if (phase.number >= item.phase) break;
173
+
174
+ const uncheckedGates = phase.items.filter(
175
+ (i) =>
176
+ !i.checked && (i.gate === "verification" || i.gate === "context" || i.gate === "document"),
177
+ );
178
+
179
+ if (uncheckedGates.length > 0) {
180
+ const missing = uncheckedGates.map((i) => ` [${i.gate}] ${i.text}`).join("\n");
181
+ process.stderr.write(
182
+ `Phase ${item.phase} blocked: complete Phase ${phase.number} gates first:\n${missing}\n`,
183
+ );
184
+ process.exit(2);
185
+ }
186
+ }
187
+ }
188
+
189
+ // All checks passed
190
+ process.exit(0);
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse hook: nudges the agent to call advance_plan when a phase is complete.
4
+ *
5
+ * This hook is advisory — it cannot block. It outputs a reminder message
6
+ * that appears in the conversation as additional context.
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+
11
+ // Read hook input from stdin
12
+ let input = "";
13
+ for await (const chunk of process.stdin) {
14
+ input += chunk;
15
+ }
16
+
17
+ const event = JSON.parse(input);
18
+ const toolInput = event.tool_input ?? {};
19
+ const filePath = toolInput.file_path ?? "";
20
+
21
+ // Fast path: not an impl.md file
22
+ if (!filePath.endsWith("/impl.md") && !filePath.endsWith("\\impl.md")) {
23
+ process.exit(0);
24
+ }
25
+
26
+ // Read the impl file (post-edit state)
27
+ let content;
28
+ try {
29
+ content = readFileSync(filePath, "utf-8");
30
+ } catch {
31
+ process.exit(0);
32
+ }
33
+
34
+ // Parse phases
35
+ function parsePhases(text) {
36
+ const fmMatch = text.match(/^---\n[\s\S]*?\n---\n/);
37
+ const body = fmMatch ? text.slice(fmMatch[0].length) : text;
38
+
39
+ const lines = body.split("\n");
40
+ const phases = [];
41
+ let currentPhase = null;
42
+ let currentGateType = "implementation";
43
+
44
+ for (const line of lines) {
45
+ const phaseMatch = line.match(/^###\s+Phase\s+(\d+)[:\s]+(.*)/);
46
+ if (phaseMatch) {
47
+ if (currentPhase) phases.push(currentPhase);
48
+ currentPhase = {
49
+ number: parseInt(phaseMatch[1], 10),
50
+ name: phaseMatch[2].trim(),
51
+ items: [],
52
+ };
53
+ currentGateType = "implementation";
54
+ continue;
55
+ }
56
+
57
+ const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|Context|Document)\b/);
58
+ if (gateMatch) {
59
+ currentGateType = gateMatch[1].toLowerCase();
60
+ continue;
61
+ }
62
+
63
+ if (line.match(/^####\s+Phase\s+\d+\s+Forward Intelligence\b/)) {
64
+ currentGateType = "_fi";
65
+ continue;
66
+ }
67
+
68
+ if (currentPhase && currentGateType !== "_fi") {
69
+ const itemMatch = line.match(/^-\s+\[([ x])\]\s+(.*)/);
70
+ if (itemMatch) {
71
+ currentPhase.items.push({
72
+ checked: itemMatch[1] === "x",
73
+ gate: currentGateType,
74
+ });
75
+ }
76
+ }
77
+ }
78
+ if (currentPhase) phases.push(currentPhase);
79
+ return phases;
80
+ }
81
+
82
+ const phases = parsePhases(content);
83
+
84
+ // Find the first phase that just became fully complete
85
+ // (all items checked, including gates)
86
+ for (const phase of phases) {
87
+ const allChecked = phase.items.every((i) => i.checked);
88
+ if (!allChecked) continue;
89
+
90
+ // Check if the next phase has any unchecked items (meaning work hasn't started there yet)
91
+ const nextPhase = phases.find((p) => p.number === phase.number + 1);
92
+ if (nextPhase) {
93
+ const nextHasUnchecked = nextPhase.items.some((i) => !i.checked);
94
+ if (nextHasUnchecked) {
95
+ // This phase is complete and next phase hasn't started
96
+ const result = {
97
+ hookSpecificOutput: {
98
+ hookEventName: "PostToolUse",
99
+ },
100
+ };
101
+ // Output reminder as JSON to stdout
102
+ console.log(JSON.stringify(result));
103
+ console.error(
104
+ `Phase ${phase.number} (${phase.name}) is fully complete. Call advance_plan to validate gates before starting Phase ${nextPhase.number}.`,
105
+ );
106
+ process.exit(0);
107
+ }
108
+ }
109
+ }
110
+
111
+ process.exit(0);
@@ -0,0 +1,9 @@
1
+ # Search for official packages before building custom
2
+
3
+ Before building any integration, SDK wrapper, or utility:
4
+
5
+ 1. Check if an official package exists (npm, PyPI, crates.io)
6
+ 2. Check if a well-maintained community package exists
7
+ 3. Only build custom if nothing exists AND the need is truly unique
8
+
9
+ Official implementations are battle-tested, maintained, and handle edge cases you haven't thought of. Custom implementations create tech debt and break in production.
@@ -0,0 +1,10 @@
1
+ # Let errors propagate visibly
2
+
3
+ Don't silently swallow errors with empty catch blocks or fallback returns. Every `catch {}` is a future mystery bug.
4
+
5
+ If you catch an error, either:
6
+ - Re-throw it with context: `throw new Error("Failed to parse config", { cause: err })`
7
+ - Log it meaningfully and return an explicit failure value
8
+ - Handle it completely (not just suppress it)
9
+
10
+ Silent failures compound. One swallowed error leads to wrong data, which leads to wrong behavior three layers up.
@@ -0,0 +1,5 @@
1
+ # Auto-index during setup, not as a separate step
2
+
3
+ When setting up infrastructure that requires indexing, scanning, or initial data loading — do it as part of the setup command, not as a "next step" the user has to remember.
4
+
5
+ Every manual step between "install" and "working" is a step where someone gets stuck. If the tool needs an index to function, build the index during init.
@@ -0,0 +1,7 @@
1
+ # Never use fallback values where a value is expected
2
+
3
+ When a value should exist, don't provide a default. Let it fail visibly.
4
+
5
+ `process.env.DATABASE_URL ?? "localhost"` hides the fact that the env var is missing. The app runs, connects to the wrong database, and you debug for an hour.
6
+
7
+ If a value is required, assert its existence. If it's truly optional, make that explicit in the type.
@@ -0,0 +1,12 @@
1
+ # Don't mock the database in integration tests
2
+
3
+ Mocked tests pass. Production breaks. The mock doesn't know about:
4
+ - Migration changes
5
+ - Constraint violations
6
+ - Query performance
7
+ - Transaction behavior
8
+ - Type coercion differences
9
+
10
+ Use a real database instance for integration tests. Docker makes this trivial. The extra seconds are worth the confidence.
11
+
12
+ Unit tests can mock external calls. Integration tests should hit real infrastructure.
@@ -0,0 +1,7 @@
1
+ # One concern per change
2
+
3
+ Each commit, PR, or change should address one thing. Not two. Not "while I'm in here."
4
+
5
+ Mixed changes make review harder, reverts riskier, and git blame useless. If you notice something unrelated while working, note it and address it separately.
6
+
7
+ The exception: if the unrelated fix is a one-line typo or import cleanup in a file you're already changing. Use judgment, but default to separate.
@@ -0,0 +1,9 @@
1
+ # Read the file before modifying it
2
+
3
+ Never edit a file you haven't read in this session. You might:
4
+ - Overwrite recent changes
5
+ - Duplicate existing code
6
+ - Contradict patterns already established in the file
7
+ - Miss context that changes your approach
8
+
9
+ Read first. Understand the current state. Then modify.
@@ -0,0 +1,7 @@
1
+ # Always run checks before committing
2
+
3
+ Run type checks, linting, and tests before every commit. Not after. Not "when you remember."
4
+
5
+ A broken commit means someone else pulls broken code, or you spend time reverting. The cost of running checks is seconds. The cost of a broken commit is minutes to hours.
6
+
7
+ If checks are too slow to run before every commit, fix the checks — don't skip them.
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "InDusk development system — skills, MCP tools, and CLI for structured AI-assisted development",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
8
8
  "skills",
9
9
  "templates",
10
+ "hooks",
11
+ "lessons",
10
12
  "!dist/**/*.test.*",
11
13
  "!dist/__tests__"
12
14
  ],