ccraft 1.0.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/bin/claude-craft.js +85 -0
- package/package.json +39 -0
- package/src/commands/auth.js +43 -0
- package/src/commands/create.js +543 -0
- package/src/commands/install.js +480 -0
- package/src/commands/logout.js +24 -0
- package/src/commands/update.js +339 -0
- package/src/constants.js +299 -0
- package/src/generators/directories.js +30 -0
- package/src/generators/metadata.js +57 -0
- package/src/generators/security.js +39 -0
- package/src/prompts/gather.js +308 -0
- package/src/ui/brand.js +62 -0
- package/src/ui/cards.js +179 -0
- package/src/ui/format.js +55 -0
- package/src/ui/phase-header.js +20 -0
- package/src/ui/prompts.js +56 -0
- package/src/ui/tables.js +89 -0
- package/src/ui/tasks.js +258 -0
- package/src/ui/theme.js +83 -0
- package/src/utils/analysis-cache.js +519 -0
- package/src/utils/api-client.js +253 -0
- package/src/utils/api-file-writer.js +197 -0
- package/src/utils/bootstrap-runner.js +148 -0
- package/src/utils/claude-analyzer.js +255 -0
- package/src/utils/claude-optimizer.js +341 -0
- package/src/utils/claude-rewriter.js +553 -0
- package/src/utils/claude-scorer.js +101 -0
- package/src/utils/description-analyzer.js +116 -0
- package/src/utils/detect-project.js +1276 -0
- package/src/utils/existing-setup.js +341 -0
- package/src/utils/file-writer.js +64 -0
- package/src/utils/json-extract.js +56 -0
- package/src/utils/logger.js +27 -0
- package/src/utils/mcp-setup.js +461 -0
- package/src/utils/preflight.js +112 -0
- package/src/utils/prompt-api-key.js +59 -0
- package/src/utils/run-claude.js +152 -0
- package/src/utils/security.js +82 -0
- package/src/utils/toolkit-rule-generator.js +364 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { execFile, execFileSync, spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// On Windows, .cmd/.bat files require cmd.exe to execute.
|
|
4
|
+
// We invoke cmd.exe explicitly (instead of shell: true with args) to avoid
|
|
5
|
+
// the DEP0190 deprecation warning about unescaped arguments.
|
|
6
|
+
const isWindows = process.platform === 'win32';
|
|
7
|
+
const CMD_EXE = process.env.ComSpec || 'cmd.exe';
|
|
8
|
+
|
|
9
|
+
/** Prepend cmd.exe /c on Windows so .cmd files execute without shell: true */
|
|
10
|
+
export function platformCmd(cmd, args) {
|
|
11
|
+
return isWindows
|
|
12
|
+
? { file: CMD_EXE, args: ['/c', cmd, ...args] }
|
|
13
|
+
: { file: cmd, args };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if the `claude` CLI is on PATH.
|
|
18
|
+
* This one stays sync — it's fast (<100ms) and runs before any spinner.
|
|
19
|
+
*/
|
|
20
|
+
export function isClaudeAvailable() {
|
|
21
|
+
try {
|
|
22
|
+
const { file, args } = platformCmd('claude', ['--version']);
|
|
23
|
+
execFileSync(file, args, {
|
|
24
|
+
stdio: 'pipe',
|
|
25
|
+
timeout: 5000,
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
});
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check Claude Code installation and authorization status.
|
|
36
|
+
* Uses `claude auth status --json` (local-only, no API call).
|
|
37
|
+
*
|
|
38
|
+
* @returns {{ installed: boolean, authorized: boolean, detail: object|null }}
|
|
39
|
+
*/
|
|
40
|
+
export function getClaudeAuthStatus() {
|
|
41
|
+
// Step 1: Check if claude is installed
|
|
42
|
+
try {
|
|
43
|
+
const { file, args } = platformCmd('claude', ['--version']);
|
|
44
|
+
execFileSync(file, args, { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
45
|
+
} catch {
|
|
46
|
+
return { installed: false, authorized: false, detail: null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If ANTHROPIC_API_KEY env var is set, Claude can authenticate via that
|
|
50
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
51
|
+
return { installed: true, authorized: true, detail: { authMethod: 'env-var' } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 2: Check auth status
|
|
55
|
+
try {
|
|
56
|
+
const { file, args } = platformCmd('claude', ['auth', 'status', '--json']);
|
|
57
|
+
const output = execFileSync(file, args, {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
stdio: 'pipe',
|
|
60
|
+
timeout: 5000,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
});
|
|
63
|
+
const status = JSON.parse(output.trim());
|
|
64
|
+
return {
|
|
65
|
+
installed: true,
|
|
66
|
+
authorized: status.loggedIn === true,
|
|
67
|
+
detail: status,
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
// If auth status command is unavailable (older CLI), assume authorized —
|
|
71
|
+
// actual auth failures will surface when runClaude() is called later.
|
|
72
|
+
return { installed: true, authorized: true, detail: null };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run `claude -p` asynchronously so the event loop stays free
|
|
78
|
+
* and ora spinners keep animating.
|
|
79
|
+
*
|
|
80
|
+
* @param {string[]} args – CLI arguments (after `claude`)
|
|
81
|
+
* @param {object} opts – spawn options (cwd, timeout, stdinInput, etc.)
|
|
82
|
+
* @param {string} [opts.stdinInput] – When provided, pipe this text to stdin
|
|
83
|
+
* instead of passing it as a CLI argument. Avoids Windows cmd.exe argument
|
|
84
|
+
* length limits and special character issues.
|
|
85
|
+
* @param {number} [opts.timeout] – Override default timeout (ms). Default: 180000.
|
|
86
|
+
* @returns {Promise<string>} stdout
|
|
87
|
+
*/
|
|
88
|
+
export function runClaude(args, opts = {}) {
|
|
89
|
+
const { stdinInput, timeout: userTimeout, ...restOpts } = opts;
|
|
90
|
+
const timeout = userTimeout ?? 180_000;
|
|
91
|
+
const { file, args: execArgs } = platformCmd('claude', args);
|
|
92
|
+
|
|
93
|
+
if (stdinInput != null) {
|
|
94
|
+
// Use spawn + stdin piping for long prompts / Windows safety
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const child = spawn(file, execArgs, {
|
|
97
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
...restOpts,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let stdout = '';
|
|
103
|
+
let stderr = '';
|
|
104
|
+
let killed = false;
|
|
105
|
+
|
|
106
|
+
const timer = setTimeout(() => {
|
|
107
|
+
killed = true;
|
|
108
|
+
child.kill();
|
|
109
|
+
}, timeout);
|
|
110
|
+
|
|
111
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
112
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
113
|
+
|
|
114
|
+
child.on('error', (err) => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
reject(err);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
child.on('close', (code) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
if (killed) {
|
|
122
|
+
const err = new Error('Process timed out');
|
|
123
|
+
err.killed = true;
|
|
124
|
+
return reject(err);
|
|
125
|
+
}
|
|
126
|
+
if (code !== 0) {
|
|
127
|
+
const err = new Error(`claude exited with code ${code}: ${stderr}`);
|
|
128
|
+
err.code = code;
|
|
129
|
+
return reject(err);
|
|
130
|
+
}
|
|
131
|
+
resolve(stdout);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
child.stdin.write(stdinInput);
|
|
135
|
+
child.stdin.end();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Existing execFile path for simple commands (no stdin needed)
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
execFile(file, execArgs, {
|
|
142
|
+
timeout,
|
|
143
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
...restOpts,
|
|
146
|
+
stdio: undefined, // execFile uses pipes by default
|
|
147
|
+
}, (err, stdout, stderr) => {
|
|
148
|
+
if (err) return reject(err);
|
|
149
|
+
resolve(stdout);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { safeWriteFile } from './file-writer.js';
|
|
4
|
+
import { SENSITIVE_PATTERNS } from '../constants.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate .gitignore additions to protect sensitive files.
|
|
8
|
+
*/
|
|
9
|
+
export function generateSecurityGitignore(detected) {
|
|
10
|
+
const lines = [
|
|
11
|
+
'# ── Security: sensitive files (added by claude-craft) ──',
|
|
12
|
+
'.env',
|
|
13
|
+
'.env.*',
|
|
14
|
+
'!.env.example',
|
|
15
|
+
'*.pem',
|
|
16
|
+
'*.key',
|
|
17
|
+
'*.cert',
|
|
18
|
+
'*.p12',
|
|
19
|
+
'credentials.json',
|
|
20
|
+
'secrets.yaml',
|
|
21
|
+
'secrets.yml',
|
|
22
|
+
'serviceAccountKey*.json',
|
|
23
|
+
'firebase-adminsdk*.json',
|
|
24
|
+
'.aws/',
|
|
25
|
+
'.gcp/',
|
|
26
|
+
'id_rsa',
|
|
27
|
+
'id_ed25519',
|
|
28
|
+
'',
|
|
29
|
+
'# ── Claude settings (added by claude-craft) ──',
|
|
30
|
+
'.claude/',
|
|
31
|
+
'CLAUDE.md',
|
|
32
|
+
'USER_GUIDE.md',
|
|
33
|
+
'.plans/',
|
|
34
|
+
'',
|
|
35
|
+
];
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if .gitignore already covers sensitive patterns.
|
|
41
|
+
*/
|
|
42
|
+
export function checkGitignoreSecurity(targetDir) {
|
|
43
|
+
const gitignorePath = join(targetDir, '.gitignore');
|
|
44
|
+
if (!existsSync(gitignorePath)) return { exists: false, coversSensitive: false, missing: SENSITIVE_PATTERNS };
|
|
45
|
+
|
|
46
|
+
const content = readFileSync(gitignorePath, 'utf8');
|
|
47
|
+
const missing = [];
|
|
48
|
+
|
|
49
|
+
// Check core patterns
|
|
50
|
+
if (!content.includes('.env')) missing.push('.env');
|
|
51
|
+
if (!content.includes('*.pem') && !content.includes('.pem')) missing.push('*.pem');
|
|
52
|
+
if (!content.includes('*.key') && !content.includes('.key')) missing.push('*.key');
|
|
53
|
+
if (!content.includes('credentials')) missing.push('credentials.json');
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
exists: true,
|
|
57
|
+
coversSensitive: missing.length === 0,
|
|
58
|
+
missing,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate security-related permission prompts for settings.json.
|
|
64
|
+
* These define what Claude is allowed to do without asking.
|
|
65
|
+
*/
|
|
66
|
+
export function generatePermissionConfig(config) {
|
|
67
|
+
const permissions = {
|
|
68
|
+
// Default deny-list for sensitive operations
|
|
69
|
+
deny: [
|
|
70
|
+
'Bash(rm -rf *)',
|
|
71
|
+
'Bash(rm -rf /)',
|
|
72
|
+
'Bash(git push --force)',
|
|
73
|
+
'Bash(git reset --hard)',
|
|
74
|
+
'Bash(DROP TABLE)',
|
|
75
|
+
'Bash(DELETE FROM)',
|
|
76
|
+
'Bash(curl * | bash)',
|
|
77
|
+
'Bash(wget * | bash)',
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return permissions;
|
|
82
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toolkit rule generator — produces .claude/rules/toolkit-usage.md
|
|
3
|
+
* based on actually installed agents, skills, and commands.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code auto-discovers these files but doesn't know when to prefer them
|
|
6
|
+
* over generic approaches. This rule file makes the mapping explicit.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs';
|
|
10
|
+
import * as logger from './logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MCP capability descriptions for the routing section.
|
|
14
|
+
* Maps MCP id → { capability, userIntents[] }
|
|
15
|
+
*/
|
|
16
|
+
const MCP_CAPABILITIES = {
|
|
17
|
+
gemini: {
|
|
18
|
+
capability: 'URLs, web pages, YouTube, multimedia, web search, deep research',
|
|
19
|
+
intents: ['URLs, web pages, YouTube', 'Web search, deep research'],
|
|
20
|
+
agent: 'researcher',
|
|
21
|
+
},
|
|
22
|
+
context7: {
|
|
23
|
+
capability: 'Live documentation lookup for libraries and frameworks',
|
|
24
|
+
intents: ['Live documentation lookup'],
|
|
25
|
+
},
|
|
26
|
+
github: {
|
|
27
|
+
capability: 'GitHub PRs, issues, code search, Dependabot alerts',
|
|
28
|
+
intents: ['GitHub PRs, issues, code search'],
|
|
29
|
+
},
|
|
30
|
+
gitlab: {
|
|
31
|
+
capability: 'GitLab MRs, issues, code search, dependency scanning',
|
|
32
|
+
intents: ['GitLab MRs, issues, code search'],
|
|
33
|
+
},
|
|
34
|
+
postgres: { capability: 'PostgreSQL queries and schema inspection', intents: ['Database queries'] },
|
|
35
|
+
mongodb: { capability: 'MongoDB queries and collection inspection', intents: ['Database queries'] },
|
|
36
|
+
mssql: { capability: 'SQL Server queries and schema inspection', intents: ['Database queries'] },
|
|
37
|
+
mysql: { capability: 'MySQL queries and schema inspection', intents: ['Database queries'] },
|
|
38
|
+
sqlite: { capability: 'SQLite queries and schema inspection', intents: ['Database queries'] },
|
|
39
|
+
redis: { capability: 'Redis key inspection and cache analysis', intents: ['Cache operations'] },
|
|
40
|
+
playwright: { capability: 'Browser automation, screenshots, testing', intents: ['Browser testing'] },
|
|
41
|
+
'sequential-thinking': { capability: 'Complex multi-step reasoning', intents: ['Complex reasoning'] },
|
|
42
|
+
figma: { capability: 'Design specs extraction from Figma files', intents: ['Design specs'] },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse YAML frontmatter from a markdown file.
|
|
47
|
+
* Extracts name and description fields only.
|
|
48
|
+
*/
|
|
49
|
+
function parseFrontmatter(content) {
|
|
50
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
51
|
+
if (!match) return {};
|
|
52
|
+
|
|
53
|
+
const fields = {};
|
|
54
|
+
const lines = match[1].split('\n');
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const line = lines[i];
|
|
58
|
+
const colonIdx = line.indexOf(':');
|
|
59
|
+
if (colonIdx === -1) continue;
|
|
60
|
+
|
|
61
|
+
const key = line.slice(0, colonIdx).trim();
|
|
62
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
63
|
+
|
|
64
|
+
// Handle YAML block scalar (> or >-)
|
|
65
|
+
if (value === '>' || value === '>-') {
|
|
66
|
+
const continuation = [];
|
|
67
|
+
while (i + 1 < lines.length && lines[i + 1].startsWith(' ')) {
|
|
68
|
+
i++;
|
|
69
|
+
continuation.push(lines[i].trim());
|
|
70
|
+
}
|
|
71
|
+
value = continuation.join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Strip surrounding quotes
|
|
75
|
+
value = value.replace(/^['"]|['"]$/g, '');
|
|
76
|
+
|
|
77
|
+
if (key === 'name' || key === 'description') {
|
|
78
|
+
fields[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return fields;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Collect installed components by reading actual files from disk.
|
|
87
|
+
*/
|
|
88
|
+
function collectComponents(targetDir) {
|
|
89
|
+
const agents = collectFromDir(join(targetDir, '.claude', 'agents'), '*.md');
|
|
90
|
+
const skills = collectSkills(join(targetDir, '.claude', 'skills'));
|
|
91
|
+
const commands = collectCommands(join(targetDir, '.claude', 'commands'));
|
|
92
|
+
return { agents, skills, commands };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read all .md files from a flat directory and extract frontmatter.
|
|
97
|
+
*/
|
|
98
|
+
function collectFromDir(dir) {
|
|
99
|
+
if (!existsSync(dir)) return [];
|
|
100
|
+
|
|
101
|
+
const items = [];
|
|
102
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
104
|
+
const content = readFileSync(join(dir, entry.name), 'utf8');
|
|
105
|
+
const fm = parseFrontmatter(content);
|
|
106
|
+
if (fm.name) {
|
|
107
|
+
items.push({ name: fm.name, description: fm.description || '' });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return items;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Collect skills from .claude/skills/[name]/SKILL.md structure.
|
|
115
|
+
*/
|
|
116
|
+
function collectSkills(dir) {
|
|
117
|
+
if (!existsSync(dir)) return [];
|
|
118
|
+
|
|
119
|
+
const items = [];
|
|
120
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
121
|
+
if (!entry.isDirectory()) continue;
|
|
122
|
+
const skillFile = join(dir, entry.name, 'SKILL.md');
|
|
123
|
+
if (!existsSync(skillFile)) continue;
|
|
124
|
+
const content = readFileSync(skillFile, 'utf8');
|
|
125
|
+
const fm = parseFrontmatter(content);
|
|
126
|
+
if (fm.name) {
|
|
127
|
+
items.push({ name: fm.name, description: fm.description || '' });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return items;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Collect commands from .claude/commands/ including variants (subdirectories).
|
|
135
|
+
* Handles two cases:
|
|
136
|
+
* - commands/plan.md + commands/plan/fast.md → /plan with variants /plan:fast
|
|
137
|
+
* - commands/fix/ (no fix.md) → variant-only commands /fix:fast, /fix:hard
|
|
138
|
+
*/
|
|
139
|
+
function collectCommands(dir) {
|
|
140
|
+
if (!existsSync(dir)) return [];
|
|
141
|
+
|
|
142
|
+
const items = [];
|
|
143
|
+
const seenDirs = new Set();
|
|
144
|
+
|
|
145
|
+
// Pass 1: commands with a parent .md file
|
|
146
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
147
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
148
|
+
const content = readFileSync(join(dir, entry.name), 'utf8');
|
|
149
|
+
const fm = parseFrontmatter(content);
|
|
150
|
+
const cmdName = entry.name.replace('.md', '');
|
|
151
|
+
const variants = collectCommandVariants(join(dir, cmdName));
|
|
152
|
+
seenDirs.add(cmdName);
|
|
153
|
+
items.push({
|
|
154
|
+
name: fm.name || cmdName,
|
|
155
|
+
description: fm.description || '',
|
|
156
|
+
variants,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Pass 2: variant-only directories (no parent .md file)
|
|
162
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
163
|
+
if (!entry.isDirectory() || seenDirs.has(entry.name)) continue;
|
|
164
|
+
const variants = collectCommandVariants(join(dir, entry.name));
|
|
165
|
+
if (variants.length === 0) continue;
|
|
166
|
+
// Use first variant's description as the command group description
|
|
167
|
+
const groupDesc = variants.length === 1
|
|
168
|
+
? variants[0].description
|
|
169
|
+
: `${variants.length} variants for ${entry.name} operations`;
|
|
170
|
+
items.push({
|
|
171
|
+
name: entry.name,
|
|
172
|
+
description: groupDesc,
|
|
173
|
+
variants,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return items;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Collect command variants from a subdirectory (e.g., commands/plan/fast.md).
|
|
182
|
+
*/
|
|
183
|
+
function collectCommandVariants(dir) {
|
|
184
|
+
if (!existsSync(dir)) return [];
|
|
185
|
+
|
|
186
|
+
const variants = [];
|
|
187
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
188
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
189
|
+
const content = readFileSync(join(dir, entry.name), 'utf8');
|
|
190
|
+
const fm = parseFrontmatter(content);
|
|
191
|
+
const variantName = entry.name.replace('.md', '');
|
|
192
|
+
variants.push({ name: fm.name || variantName, description: fm.description || '' });
|
|
193
|
+
}
|
|
194
|
+
return variants;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Detect installed MCPs from settings.json.
|
|
199
|
+
*/
|
|
200
|
+
function collectMcps(targetDir) {
|
|
201
|
+
const settingsPath = join(targetDir, '.claude', 'settings.json');
|
|
202
|
+
if (!existsSync(settingsPath)) return [];
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
206
|
+
return Object.keys(settings.mcpServers || {});
|
|
207
|
+
} catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Build MCP-Enhanced Capabilities section based on installed MCPs.
|
|
214
|
+
*/
|
|
215
|
+
function buildMcpSection(mcpIds) {
|
|
216
|
+
if (mcpIds.length === 0) return [];
|
|
217
|
+
|
|
218
|
+
const lines = [
|
|
219
|
+
'## MCP-Enhanced Capabilities',
|
|
220
|
+
'',
|
|
221
|
+
'NEVER say "I can\'t" when an MCP can handle it. See `rules/mcp-routing.md` for the full routing map.',
|
|
222
|
+
'',
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
for (const id of mcpIds) {
|
|
226
|
+
const cap = MCP_CAPABILITIES[id];
|
|
227
|
+
if (!cap) continue;
|
|
228
|
+
const agentNote = cap.agent ? ` or **${cap.agent}** agent` : '';
|
|
229
|
+
lines.push(`- **${cap.intents[0]}** → use **${id.charAt(0).toUpperCase() + id.slice(1)} MCP**${agentNote}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push('');
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Build the mandatory preferences section based on installed agents.
|
|
238
|
+
*/
|
|
239
|
+
function buildPreferences(agents) {
|
|
240
|
+
const prefs = [];
|
|
241
|
+
const agentNames = new Set(agents.map((a) => a.name));
|
|
242
|
+
|
|
243
|
+
if (agentNames.has('scout')) {
|
|
244
|
+
prefs.push('- Use **scout** agent for file discovery and codebase exploration — NOT generic Explore');
|
|
245
|
+
}
|
|
246
|
+
if (agentNames.has('architect')) {
|
|
247
|
+
prefs.push('- Use **architect** agent for system design and trade-off analysis — NOT generic Plan');
|
|
248
|
+
}
|
|
249
|
+
if (agentNames.has('planner')) {
|
|
250
|
+
prefs.push('- Use **planner** agent for implementation planning — NOT generic Plan');
|
|
251
|
+
}
|
|
252
|
+
if (agentNames.has('code-reviewer')) {
|
|
253
|
+
prefs.push('- Use **code-reviewer** agent for post-implementation quality review');
|
|
254
|
+
}
|
|
255
|
+
if (agentNames.has('debugger')) {
|
|
256
|
+
prefs.push('- Use **debugger** agent for bug investigation — NOT manual debugging');
|
|
257
|
+
}
|
|
258
|
+
if (agentNames.has('researcher')) {
|
|
259
|
+
prefs.push('- Use **researcher** agent for multi-source technical research — has Gemini + context7 for web/docs');
|
|
260
|
+
}
|
|
261
|
+
if (agentNames.has('interviewer')) {
|
|
262
|
+
prefs.push('- **ALWAYS** run the interviewer trigger checklist before implementation — invoke **interviewer** agent proactively, not reactively. See capability-map.md Auto-Invoke section for the full checklist');
|
|
263
|
+
}
|
|
264
|
+
if (agentNames.has('tdd-guide')) {
|
|
265
|
+
prefs.push('- Use **tdd-guide** agent for test-driven development workflows');
|
|
266
|
+
}
|
|
267
|
+
if (agentNames.has('build-resolver')) {
|
|
268
|
+
prefs.push('- Use **build-resolver** agent for build failures and dependency issues');
|
|
269
|
+
}
|
|
270
|
+
if (agentNames.has('refactor-cleaner')) {
|
|
271
|
+
prefs.push('- Use **refactor-cleaner** agent for safe refactoring operations');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return prefs;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Generate .claude/rules/toolkit-usage.md from installed components.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} targetDir - Project root
|
|
281
|
+
* @returns {{ agentCount: number, skillCount: number, commandCount: number }}
|
|
282
|
+
*/
|
|
283
|
+
export function generateToolkitRule(targetDir) {
|
|
284
|
+
const { agents, skills, commands } = collectComponents(targetDir);
|
|
285
|
+
|
|
286
|
+
if (agents.length === 0 && skills.length === 0 && commands.length === 0) {
|
|
287
|
+
logger.debug('No agents, skills, or commands found — skipping toolkit rule.');
|
|
288
|
+
return { agentCount: 0, skillCount: 0, commandCount: 0 };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const sections = ['# Installed Toolkit', ''];
|
|
292
|
+
sections.push('Use the project\'s specialized agents, skills, and commands instead of generic approaches.');
|
|
293
|
+
sections.push('');
|
|
294
|
+
|
|
295
|
+
// Agents table
|
|
296
|
+
if (agents.length > 0) {
|
|
297
|
+
sections.push('## Agents');
|
|
298
|
+
sections.push('');
|
|
299
|
+
sections.push('| Agent | Use When |');
|
|
300
|
+
sections.push('|-------|----------|');
|
|
301
|
+
for (const agent of agents) {
|
|
302
|
+
sections.push(`| **${agent.name}** | ${agent.description} |`);
|
|
303
|
+
}
|
|
304
|
+
sections.push('');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Skills table
|
|
308
|
+
if (skills.length > 0) {
|
|
309
|
+
sections.push('## Skills (invoke with /skill-name)');
|
|
310
|
+
sections.push('');
|
|
311
|
+
sections.push('| Skill | Use When |');
|
|
312
|
+
sections.push('|-------|----------|');
|
|
313
|
+
for (const skill of skills) {
|
|
314
|
+
sections.push(`| **/${skill.name}** | ${skill.description} |`);
|
|
315
|
+
}
|
|
316
|
+
sections.push('');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Commands table
|
|
320
|
+
if (commands.length > 0) {
|
|
321
|
+
sections.push('## Commands (invoke with /command-name)');
|
|
322
|
+
sections.push('');
|
|
323
|
+
sections.push('| Command | Use When |');
|
|
324
|
+
sections.push('|---------|----------|');
|
|
325
|
+
for (const cmd of commands) {
|
|
326
|
+
// Filter variants that duplicate the parent name
|
|
327
|
+
const uniqueVariants = cmd.variants.filter((v) => v.name !== cmd.name);
|
|
328
|
+
const variantNote = uniqueVariants.length > 0
|
|
329
|
+
? ` Variants: ${uniqueVariants.map((v) => `/${v.name}`).join(', ')}`
|
|
330
|
+
: '';
|
|
331
|
+
sections.push(`| **/${cmd.name}** | ${cmd.description}${variantNote} |`);
|
|
332
|
+
}
|
|
333
|
+
sections.push('');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// MCP-Enhanced Capabilities
|
|
337
|
+
const mcpIds = collectMcps(targetDir);
|
|
338
|
+
const mcpSection = buildMcpSection(mcpIds);
|
|
339
|
+
if (mcpSection.length > 0) {
|
|
340
|
+
sections.push(...mcpSection);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Mandatory preferences
|
|
344
|
+
const prefs = buildPreferences(agents);
|
|
345
|
+
if (prefs.length > 0) {
|
|
346
|
+
sections.push('## Mandatory Preferences');
|
|
347
|
+
sections.push('');
|
|
348
|
+
sections.push(...prefs);
|
|
349
|
+
sections.push('');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const content = sections.join('\n').trimEnd() + '\n';
|
|
353
|
+
|
|
354
|
+
// Write the rule file
|
|
355
|
+
const ruleDir = join(targetDir, '.claude', 'rules');
|
|
356
|
+
mkdirSync(ruleDir, { recursive: true });
|
|
357
|
+
writeFileSync(join(ruleDir, 'toolkit-usage.md'), content, 'utf8');
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
agentCount: agents.length,
|
|
361
|
+
skillCount: skills.length,
|
|
362
|
+
commandCount: commands.length,
|
|
363
|
+
};
|
|
364
|
+
}
|