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,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-powered CLAUDE.md rewriter — context-injected approach.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 (filter): Quick check which sections need project-specific enhancement.
|
|
5
|
+
* Phase 2 (rewrite): All context embedded inline — zero tool calls needed.
|
|
6
|
+
* Phase 3 (verify): Validate that references in generated content match reality.
|
|
7
|
+
*
|
|
8
|
+
* Falls back to legacy tool-based approach if analysis cache is unavailable.
|
|
9
|
+
*/
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
12
|
+
import { isClaudeAvailable, runClaude } from './run-claude.js';
|
|
13
|
+
import { extractJsonObject } from './json-extract.js';
|
|
14
|
+
import * as logger from './logger.js';
|
|
15
|
+
|
|
16
|
+
/** Override rule — appended after every rewrite to ensure it survives. */
|
|
17
|
+
const OVERRIDE_RULE = '> **Override rule:** In every mode (plan, edit, default), check `.claude/` definitions FIRST. Only use Claude\'s built-in agents, skills, or commands if no project-specific match exists. See `.claude/rules/capability-map.md` for detailed routing.';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Append the override rule to rewritten content if not already present.
|
|
21
|
+
*/
|
|
22
|
+
function _ensureOverrideRule(content) {
|
|
23
|
+
if (!content) return content;
|
|
24
|
+
if (content.includes('**Override rule:**')) return content;
|
|
25
|
+
return content.trimEnd() + '\n\n' + OVERRIDE_RULE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Use Claude to rewrite CLAUDE.md based on installed configuration.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} targetDir - Project root
|
|
32
|
+
* @param {object|null} cache - From readAnalysisCache(); null triggers legacy
|
|
33
|
+
* @returns {Promise<boolean>} true on success
|
|
34
|
+
*/
|
|
35
|
+
export async function rewriteClaudeMd(targetDir, cache = null) {
|
|
36
|
+
if (!isClaudeAvailable()) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If no cache or no manifest, fall back to legacy tool-based approach
|
|
41
|
+
if (!cache || !cache.projectContext || !cache.manifest) {
|
|
42
|
+
return _legacyRewrite(targetDir);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Read current template CLAUDE.md (written by step 5)
|
|
47
|
+
const claudeMdPath = join(targetDir, 'CLAUDE.md');
|
|
48
|
+
if (!existsSync(claudeMdPath)) {
|
|
49
|
+
logger.debug('No CLAUDE.md found to rewrite.');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const templateContent = readFileSync(claudeMdPath, 'utf8');
|
|
53
|
+
|
|
54
|
+
// Read settings.json for MCP server info
|
|
55
|
+
const settingsJson = _readSettingsJson(targetDir);
|
|
56
|
+
|
|
57
|
+
// ── Phase 1: Filter — determine which sections need enhancement ──
|
|
58
|
+
const sectionsResult = await _filterSections(cache, templateContent, settingsJson, targetDir);
|
|
59
|
+
|
|
60
|
+
if (sectionsResult?.skipRewrite) {
|
|
61
|
+
logger.debug('Filter phase determined CLAUDE.md template is sufficient — skipping rewrite.');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If filter failed or returned sections, proceed with rewrite
|
|
66
|
+
const sections = sectionsResult?.sectionsToEnhance || null;
|
|
67
|
+
|
|
68
|
+
// ── Phase 2: Rewrite — context-injected, no tool calls ──────────
|
|
69
|
+
const rewritten = await _contextRewrite(cache, templateContent, settingsJson, sections, targetDir);
|
|
70
|
+
|
|
71
|
+
if (!rewritten) {
|
|
72
|
+
// Context-injected approach failed — fall back to legacy tool-based rewrite
|
|
73
|
+
logger.debug('Context-injected rewrite failed — falling back to legacy tool-based approach.');
|
|
74
|
+
return _legacyRewrite(targetDir);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Write the new CLAUDE.md (ensure override rule survives rewrite)
|
|
78
|
+
writeFileSync(claudeMdPath, _ensureOverrideRule(rewritten) + '\n', 'utf8');
|
|
79
|
+
|
|
80
|
+
// ── Phase 3: Verify — check references ──────────────────────────
|
|
81
|
+
const verified = _verifyClaudeMd(rewritten, targetDir);
|
|
82
|
+
if (verified.warnings.length > 0) {
|
|
83
|
+
for (const w of verified.warnings) {
|
|
84
|
+
logger.debug(`CLAUDE.md verify: ${w}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// If the two-phase approach fails entirely, try legacy as last resort
|
|
91
|
+
logger.debug(`Two-phase rewrite failed (${err.killed ? 'timeout' : err.message}) — trying legacy.`);
|
|
92
|
+
try {
|
|
93
|
+
return await _legacyRewrite(targetDir);
|
|
94
|
+
} catch {
|
|
95
|
+
logger.debug('CLAUDE.md rewrite failed — keeping original.');
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Phase 1: Filter ──────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Quick Claude call to check which CLAUDE.md sections need enhancement.
|
|
105
|
+
* Returns { sectionsToEnhance, skipRewrite } or null on failure.
|
|
106
|
+
*/
|
|
107
|
+
async function _filterSections(cache, templateContent, settingsJson, targetDir) {
|
|
108
|
+
const counts = cache.manifest.counts || {};
|
|
109
|
+
|
|
110
|
+
const filterPrompt = `Review this template CLAUDE.md against the actual project.
|
|
111
|
+
Determine which sections need project-specific enhancement.
|
|
112
|
+
|
|
113
|
+
## Project Context
|
|
114
|
+
${cache.projectContext}
|
|
115
|
+
|
|
116
|
+
## Installed Counts
|
|
117
|
+
- Agents: ${counts.agents || 0}, Skills: ${counts.skills || 0}, Rules: ${counts.rules || 0}, Workflows: ${counts.workflows || 0}
|
|
118
|
+
|
|
119
|
+
## Current CLAUDE.md (template version)
|
|
120
|
+
${templateContent}
|
|
121
|
+
|
|
122
|
+
## Task
|
|
123
|
+
Which sections need project-specific enhancement? A section needs enhancement if:
|
|
124
|
+
1. It uses generic/placeholder text that should reference actual project details
|
|
125
|
+
2. Build commands or project name/description are placeholders
|
|
126
|
+
|
|
127
|
+
Sections that are already accurate should be SKIPPED.
|
|
128
|
+
|
|
129
|
+
## Response Format
|
|
130
|
+
Return a JSON object (and NOTHING else):
|
|
131
|
+
{
|
|
132
|
+
"sectionsToEnhance": ["Project Overview", "Build & Run Commands"],
|
|
133
|
+
"skipRewrite": false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
If the template is already accurate for this project, return: { "sectionsToEnhance": [], "skipRewrite": true }`;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const output = await runClaude([
|
|
140
|
+
'-p',
|
|
141
|
+
'--max-turns', '2',
|
|
142
|
+
'--allowedTools', '',
|
|
143
|
+
], { cwd: targetDir, stdinInput: filterPrompt, timeout: 60_000 });
|
|
144
|
+
|
|
145
|
+
return _parseJsonResponse(output, 'sectionsToEnhance');
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err.killed) {
|
|
148
|
+
logger.debug('CLAUDE.md filter phase timed out — proceeding with full rewrite.');
|
|
149
|
+
} else {
|
|
150
|
+
logger.debug(`CLAUDE.md filter phase failed: ${err.message} — proceeding with full rewrite.`);
|
|
151
|
+
}
|
|
152
|
+
return null; // null = proceed with full rewrite as fallback
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Phase 2: Context-Injected Rewrite ────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Rewrite CLAUDE.md with all context embedded inline.
|
|
160
|
+
* No --allowedTools needed — zero tool calls.
|
|
161
|
+
* Returns markdown string or null on failure.
|
|
162
|
+
*/
|
|
163
|
+
async function _contextRewrite(cache, templateContent, settingsJson, sections, targetDir) {
|
|
164
|
+
const projectName = cache.projectInfo?.name || 'this project';
|
|
165
|
+
const installedSummary = _buildInstalledSummary(cache.manifest, settingsJson);
|
|
166
|
+
|
|
167
|
+
const sectionsNote = sections && sections.length > 0
|
|
168
|
+
? `\nFocus your enhancements on these sections: ${sections.join(', ')}\nKeep other sections close to the template version.`
|
|
169
|
+
: '';
|
|
170
|
+
|
|
171
|
+
const rewritePrompt = `You are rewriting the CLAUDE.md file for "${projectName}".
|
|
172
|
+
All the information you need is provided below. Do NOT request any tools or file access.
|
|
173
|
+
|
|
174
|
+
## Project Context
|
|
175
|
+
${cache.projectContext}
|
|
176
|
+
|
|
177
|
+
## Installed Configuration
|
|
178
|
+
${installedSummary}
|
|
179
|
+
${sectionsNote}
|
|
180
|
+
|
|
181
|
+
## Current Template CLAUDE.md
|
|
182
|
+
${templateContent}
|
|
183
|
+
|
|
184
|
+
## CRITICAL: Do NOT list auto-discovered components
|
|
185
|
+
|
|
186
|
+
Claude Code auto-discovers agents, skills, rules, MCP servers, and hooks from \`.claude/\`.
|
|
187
|
+
**USER_GUIDE.md** has the full reference for all installed components.
|
|
188
|
+
CLAUDE.md should contain ONLY what Claude cannot infer from code or auto-loaded files.
|
|
189
|
+
|
|
190
|
+
## CLAUDE.md structure (MUST be under 80 lines)
|
|
191
|
+
|
|
192
|
+
Write a CLAUDE.md with these exact sections:
|
|
193
|
+
|
|
194
|
+
### 1. Project Overview (3-8 lines)
|
|
195
|
+
- Project name, what it does, tech stack
|
|
196
|
+
- Architecture pattern if detected
|
|
197
|
+
- Use the project description from the context above
|
|
198
|
+
|
|
199
|
+
### 2. Build & Run Commands (only if known)
|
|
200
|
+
A markdown table of: install, dev, build, test, lint commands.
|
|
201
|
+
Only include commands that are known from the project context above.
|
|
202
|
+
|
|
203
|
+
### 3. Tech Stack (only if stack-specific conventions exist)
|
|
204
|
+
- Stack-specific conventions (purpose, not a file tree)
|
|
205
|
+
- Key directory conventions — max 3 lines, describe purpose not inventory
|
|
206
|
+
- One line pointing to the relevant rules file: "See \`.claude/rules/<stack>.md\`"
|
|
207
|
+
- OMIT this section entirely if no stack rules exist
|
|
208
|
+
- Do NOT include a directory tree or file-by-file listing
|
|
209
|
+
|
|
210
|
+
### 4. Usage Reporting (always include, do not modify)
|
|
211
|
+
- Always report the total API request usage (requests made, tokens consumed) when a task is finished.
|
|
212
|
+
- Include this section exactly as-is — do not rephrase or omit.
|
|
213
|
+
|
|
214
|
+
## Rules for writing
|
|
215
|
+
|
|
216
|
+
- MUST be under 80 lines total
|
|
217
|
+
- Be concise — one line per item, no lengthy explanations
|
|
218
|
+
- Do NOT include generic advice, filler, or self-evident statements
|
|
219
|
+
- Do NOT include content that Claude can infer from code or auto-loaded files
|
|
220
|
+
- Do NOT list agents, skills, rules, MCP servers, or security patterns — they are all auto-discovered
|
|
221
|
+
- Do NOT include an "Installed Configuration" section — auto-loaded components are documented elsewhere
|
|
222
|
+
- Do NOT wrap the output in \`\`\`markdown fences
|
|
223
|
+
- Do NOT add any explanation or commentary before or after the markdown
|
|
224
|
+
- Start your response directly with the # header
|
|
225
|
+
- End your response immediately after the last section — NOTHING after it
|
|
226
|
+
- ABSOLUTELY NO trailing text, changelog, or summary-of-differences section`;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const output = await runClaude([
|
|
230
|
+
'-p',
|
|
231
|
+
'--max-turns', '3',
|
|
232
|
+
'--allowedTools', '',
|
|
233
|
+
], { cwd: targetDir, stdinInput: rewritePrompt, timeout: 120_000 });
|
|
234
|
+
|
|
235
|
+
// Extract markdown from response
|
|
236
|
+
const responseText = extractMarkdown(output);
|
|
237
|
+
|
|
238
|
+
// Validate
|
|
239
|
+
if (!responseText || !responseText.includes('#')) {
|
|
240
|
+
logger.debug(`Context rewrite produced invalid markdown (first 300 chars): ${(output || '').slice(0, 300)}`);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const lineCount = responseText.split('\n').length;
|
|
245
|
+
if (lineCount > 80) {
|
|
246
|
+
logger.debug(`Context rewrite produced ${lineCount} lines — too long.`);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return responseText;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (err.killed) {
|
|
253
|
+
logger.debug('Context rewrite phase timed out.');
|
|
254
|
+
} else {
|
|
255
|
+
logger.debug(`Context rewrite phase failed: ${err.message}`);
|
|
256
|
+
}
|
|
257
|
+
return null; // Return null so caller can fall back to legacy
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Phase 3: Verify ──────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Validate references in generated CLAUDE.md against actual installed files.
|
|
265
|
+
*/
|
|
266
|
+
function _verifyClaudeMd(content, targetDir) {
|
|
267
|
+
const warnings = [];
|
|
268
|
+
|
|
269
|
+
// Check 1: Referenced file paths exist
|
|
270
|
+
const fileRefs = content.match(/`\.claude\/[^`]+`/g) || [];
|
|
271
|
+
for (const ref of fileRefs) {
|
|
272
|
+
const refPath = ref.replace(/`/g, '').trim();
|
|
273
|
+
if (refPath.endsWith('/') || refPath.endsWith('\\')) continue; // directory ref
|
|
274
|
+
if (!existsSync(join(targetDir, refPath))) {
|
|
275
|
+
warnings.push(`References ${refPath} which does not exist`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check 2: Line count is under limit
|
|
280
|
+
const lineCount = content.split('\n').length;
|
|
281
|
+
if (lineCount > 80) {
|
|
282
|
+
warnings.push(`CLAUDE.md has ${lineCount} lines (target: under 80)`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { warnings };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build the installed configuration summary block from cached manifest.
|
|
292
|
+
*/
|
|
293
|
+
function _buildInstalledSummary(manifest, settingsJson) {
|
|
294
|
+
const counts = manifest?.counts || {};
|
|
295
|
+
const mcpCount = Object.keys(settingsJson?.mcpServers || {}).length;
|
|
296
|
+
|
|
297
|
+
return [
|
|
298
|
+
`- Agents: ${counts.agents || 0} (auto-discovered from .claude/agents/)`,
|
|
299
|
+
`- Skills: ${counts.skills || 0} (auto-discovered from .claude/skills/)`,
|
|
300
|
+
`- Rules: ${counts.rules || 0} files (auto-loaded from .claude/rules/)`,
|
|
301
|
+
`- MCP Servers: ${mcpCount} (auto-loaded from settings.json)`,
|
|
302
|
+
`- Workflows: ${counts.workflows || 0} (in .claude/workflows/)`,
|
|
303
|
+
].join('\n');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Read .claude/settings.json safely.
|
|
308
|
+
*/
|
|
309
|
+
function _readSettingsJson(targetDir) {
|
|
310
|
+
const settingsPath = join(targetDir, '.claude', 'settings.json');
|
|
311
|
+
try {
|
|
312
|
+
if (existsSync(settingsPath)) {
|
|
313
|
+
return JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
// Ignore parse errors
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse a JSON response from Claude using multiple strategies.
|
|
323
|
+
*/
|
|
324
|
+
function _parseJsonResponse(text, key) {
|
|
325
|
+
if (!text) return null;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const obj = JSON.parse(text.trim());
|
|
329
|
+
if (obj && key in obj) return obj;
|
|
330
|
+
} catch {
|
|
331
|
+
// Not direct JSON
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
|
|
335
|
+
if (fenceMatch) {
|
|
336
|
+
try {
|
|
337
|
+
const obj = JSON.parse(fenceMatch[1].trim());
|
|
338
|
+
if (obj && key in obj) return obj;
|
|
339
|
+
} catch {
|
|
340
|
+
// Invalid JSON in fence
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const obj = extractJsonObject(text, key);
|
|
345
|
+
if (obj && key in obj) return obj;
|
|
346
|
+
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Markdown extraction (preserved from original) ────────────────────
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Extract clean markdown from Claude's response.
|
|
354
|
+
*/
|
|
355
|
+
function extractMarkdown(text) {
|
|
356
|
+
if (!text) return '';
|
|
357
|
+
|
|
358
|
+
let md;
|
|
359
|
+
|
|
360
|
+
// Strategy 1: ```markdown fence with GREEDY match to last ```
|
|
361
|
+
const fenceMatch = text.match(/```(?:markdown|md)\s*\n([\s\S]*)```\s*$/i);
|
|
362
|
+
if (fenceMatch) {
|
|
363
|
+
md = fenceMatch[1].trim();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Strategy 2: Preamble then first # header
|
|
367
|
+
if (!md) {
|
|
368
|
+
const headerIdx = text.indexOf('\n# ');
|
|
369
|
+
if (headerIdx !== -1 && headerIdx < 500) {
|
|
370
|
+
md = text.slice(headerIdx + 1);
|
|
371
|
+
md = md.replace(/\n```\s*$/, '');
|
|
372
|
+
md = md.trim();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Strategy 3: Starts with # directly
|
|
377
|
+
if (!md && text.trimStart().startsWith('#')) {
|
|
378
|
+
md = text.replace(/\n```\s*$/, '').trim();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Strategy 4: Any ``` fence containing # headers
|
|
382
|
+
if (!md) {
|
|
383
|
+
const anyFence = text.match(/```\w*\s*\n([\s\S]*)```\s*$/);
|
|
384
|
+
if (anyFence && anyFence[1].includes('#')) {
|
|
385
|
+
md = anyFence[1].trim();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Fallback
|
|
390
|
+
if (!md) {
|
|
391
|
+
md = text.trim();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
md = stripTrailingProse(md);
|
|
395
|
+
md = stripChangelogSuffix(md);
|
|
396
|
+
|
|
397
|
+
return md;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Strip trailing conversational prose.
|
|
402
|
+
*/
|
|
403
|
+
function stripTrailingProse(text) {
|
|
404
|
+
if (!text) return '';
|
|
405
|
+
|
|
406
|
+
const lines = text.split('\n');
|
|
407
|
+
let end = lines.length - 1;
|
|
408
|
+
while (end >= 0 && lines[end].trim() === '') {
|
|
409
|
+
end--;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
while (end >= 0) {
|
|
413
|
+
const line = lines[end].trim();
|
|
414
|
+
if (/^(#{1,6}\s|[-*+]\s|>\s|\||\d+\.\s|```|<)/.test(line)) break;
|
|
415
|
+
if (line === '') {
|
|
416
|
+
end--;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let probeIdx = end - 1;
|
|
421
|
+
while (probeIdx >= 0 && lines[probeIdx].trim() !== '') {
|
|
422
|
+
if (/^(#{1,6}\s|[-*+]\s|>\s|\||\d+\.\s|```|<)/.test(lines[probeIdx].trim())) {
|
|
423
|
+
return lines.slice(0, end + 1).join('\n').trimEnd();
|
|
424
|
+
}
|
|
425
|
+
probeIdx--;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (probeIdx >= 0 && lines[probeIdx].trim() === '') {
|
|
429
|
+
end = probeIdx;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return lines.slice(0, end + 1).join('\n').trimEnd();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Strip changelog / summary blocks.
|
|
441
|
+
*/
|
|
442
|
+
function stripChangelogSuffix(text) {
|
|
443
|
+
if (!text) return '';
|
|
444
|
+
|
|
445
|
+
const triggerRe = /^(\*{2}|#{1,6}\s*)(key changes|changes from|what changed|summary of changes|notes?:|differences?:|changelog)\b/i;
|
|
446
|
+
|
|
447
|
+
const lines = text.split('\n');
|
|
448
|
+
for (let i = 0; i < lines.length; i++) {
|
|
449
|
+
if (triggerRe.test(lines[i].trim())) {
|
|
450
|
+
let cutAt = i;
|
|
451
|
+
while (cutAt > 0 && lines[cutAt - 1].trim() === '') cutAt--;
|
|
452
|
+
return lines.slice(0, cutAt).join('\n').trimEnd();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return text;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Legacy fallback ──────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Original tool-based CLAUDE.md rewrite (preserved for backward compat).
|
|
463
|
+
*/
|
|
464
|
+
const LEGACY_REWRITE_PROMPT = `You are rewriting the CLAUDE.md file for this project.
|
|
465
|
+
|
|
466
|
+
IMPORTANT: Ignore any existing CLAUDE.md content. Write a completely new one based ONLY
|
|
467
|
+
on the actual project files and the installed .claude/ configuration.
|
|
468
|
+
|
|
469
|
+
## What to read
|
|
470
|
+
|
|
471
|
+
1. Read .claude/.claude-craft.json — the single source of truth: project analysis (name, description, tech stack, architecture, build commands), installation metadata, and resolved tech stacks with impact scores
|
|
472
|
+
2. List .claude/rules/ — check which rule files exist (for "See .claude/rules/<name>.md" pointers)
|
|
473
|
+
|
|
474
|
+
Do NOT read or enumerate agents, skills, rules, MCP servers, or settings.json — they are auto-loaded by Claude Code.
|
|
475
|
+
|
|
476
|
+
## CRITICAL: Do NOT list auto-discovered components
|
|
477
|
+
|
|
478
|
+
Claude Code auto-discovers agents, skills, rules, MCP servers, and hooks from \`.claude/\`.
|
|
479
|
+
**USER_GUIDE.md** has the full reference for all installed components.
|
|
480
|
+
CLAUDE.md should contain ONLY what Claude cannot infer from code or auto-loaded files.
|
|
481
|
+
|
|
482
|
+
## CLAUDE.md structure (MUST be under 80 lines)
|
|
483
|
+
|
|
484
|
+
Write a CLAUDE.md with these exact sections:
|
|
485
|
+
|
|
486
|
+
### 1. Project Overview (3-8 lines)
|
|
487
|
+
- Project name, what it does, tech stack
|
|
488
|
+
- Architecture pattern if detected
|
|
489
|
+
- Use the description from .claude-craft.json
|
|
490
|
+
|
|
491
|
+
### 2. Build & Run Commands (only if known)
|
|
492
|
+
A markdown table of: install, dev, build, test, lint commands.
|
|
493
|
+
Only include commands that exist in .claude-craft.json buildCommands.
|
|
494
|
+
|
|
495
|
+
### 3. Tech Stack (only if stack-specific conventions exist)
|
|
496
|
+
- Stack-specific conventions (purpose, not a file tree)
|
|
497
|
+
- Key directory conventions — max 3 lines, describe purpose not inventory
|
|
498
|
+
- One line pointing to the relevant rules file
|
|
499
|
+
- OMIT this section entirely if no stack rules exist in .claude/rules/
|
|
500
|
+
- Do NOT include a directory tree or file-by-file listing
|
|
501
|
+
|
|
502
|
+
### 4. Usage Reporting (always include, do not modify)
|
|
503
|
+
- Always report the total API request usage (requests made, tokens consumed) when a task is finished.
|
|
504
|
+
- Include this section exactly as-is — do not rephrase or omit.
|
|
505
|
+
|
|
506
|
+
## Rules for writing
|
|
507
|
+
|
|
508
|
+
- MUST be under 80 lines total
|
|
509
|
+
- Be concise — one line per item, no lengthy explanations
|
|
510
|
+
- Do NOT include generic advice, filler, or self-evident statements
|
|
511
|
+
- Do NOT include content that Claude can infer from code or auto-loaded files
|
|
512
|
+
- Do NOT list agents, skills, rules, MCP servers, or security patterns — they are all auto-discovered
|
|
513
|
+
- Do NOT include an "Installed Configuration" section — auto-loaded components are documented elsewhere
|
|
514
|
+
- Do NOT try to write/edit files — just output the markdown content
|
|
515
|
+
- Do NOT wrap the output in \`\`\`markdown fences
|
|
516
|
+
- Do NOT add any explanation or commentary before or after the markdown
|
|
517
|
+
- Start your response directly with the # header
|
|
518
|
+
- End your response immediately after the last section — NOTHING after it
|
|
519
|
+
- ABSOLUTELY NO trailing text, changelog, or summary-of-differences section`;
|
|
520
|
+
|
|
521
|
+
async function _legacyRewrite(targetDir) {
|
|
522
|
+
try {
|
|
523
|
+
const output = await runClaude([
|
|
524
|
+
'-p',
|
|
525
|
+
'--max-turns', '5',
|
|
526
|
+
'--allowedTools', 'Read,Glob,Bash(ls:*)',
|
|
527
|
+
], { cwd: targetDir, stdinInput: LEGACY_REWRITE_PROMPT, timeout: 180_000 });
|
|
528
|
+
|
|
529
|
+
const responseText = extractMarkdown(output);
|
|
530
|
+
|
|
531
|
+
if (!responseText || !responseText.includes('#')) {
|
|
532
|
+
logger.debug(`Legacy rewrite raw output (first 500 chars): ${(output || '').slice(0, 500)}`);
|
|
533
|
+
logger.debug('Claude did not produce valid markdown for CLAUDE.md — keeping original.');
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const lineCount = responseText.split('\n').length;
|
|
538
|
+
if (lineCount > 80) {
|
|
539
|
+
logger.debug(`Claude produced ${lineCount} lines for CLAUDE.md (limit 80) — keeping original.`);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
writeFileSync(join(targetDir, 'CLAUDE.md'), _ensureOverrideRule(responseText) + '\n', 'utf8');
|
|
544
|
+
return true;
|
|
545
|
+
} catch (err) {
|
|
546
|
+
if (err.killed) {
|
|
547
|
+
logger.debug('CLAUDE.md rewrite timed out — keeping original.');
|
|
548
|
+
} else {
|
|
549
|
+
logger.debug('CLAUDE.md rewrite failed — keeping original.');
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-powered scoring for optional candidates.
|
|
3
|
+
*
|
|
4
|
+
* Uses the local Claude CLI to evaluate which optional candidates
|
|
5
|
+
* should be included in the configuration.
|
|
6
|
+
*/
|
|
7
|
+
import { isClaudeAvailable, runClaude } from './run-claude.js';
|
|
8
|
+
import { extractJsonObject } from './json-extract.js';
|
|
9
|
+
import * as logger from './logger.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Score optional candidates using Claude.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} scoringPrompt - Prompt generated by the server
|
|
15
|
+
* @param {string} targetDir - Project root (cwd for Claude)
|
|
16
|
+
* @returns {Promise<{ selected: string[], reasoning: string }>}
|
|
17
|
+
* Falls back to selecting ALL candidates if Claude is unavailable.
|
|
18
|
+
*/
|
|
19
|
+
export async function scoreWithClaude(scoringPrompt, targetDir) {
|
|
20
|
+
if (!isClaudeAvailable()) {
|
|
21
|
+
logger.warn('Claude CLI not available — including all optional candidates.');
|
|
22
|
+
return { selected: null, reasoning: 'Claude unavailable — inclusive fallback' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const output = await runClaude([
|
|
27
|
+
'-p',
|
|
28
|
+
'--max-turns', '3',
|
|
29
|
+
], { cwd: targetDir, stdinInput: scoringPrompt });
|
|
30
|
+
|
|
31
|
+
// Parse the scoring result directly (no JSON wrapper to extract)
|
|
32
|
+
const result = parseScoreResult(output);
|
|
33
|
+
if (result) {
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.warn('Claude did not return valid scoring JSON — including all candidates.');
|
|
38
|
+
logger.debug(`Raw output (first 500 chars): ${(output || '').slice(0, 500)}`);
|
|
39
|
+
return { selected: null, reasoning: 'Parse failure — inclusive fallback' };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err.killed) {
|
|
42
|
+
logger.warn('Claude scoring timed out — including all candidates.');
|
|
43
|
+
} else {
|
|
44
|
+
logger.warn('Claude scoring failed — including all candidates.');
|
|
45
|
+
}
|
|
46
|
+
return { selected: null, reasoning: 'Error — inclusive fallback' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse Claude's scoring response.
|
|
52
|
+
* Handles both raw JSON and JSON embedded in markdown fences.
|
|
53
|
+
*/
|
|
54
|
+
function parseScoreResult(text) {
|
|
55
|
+
if (!text) return null;
|
|
56
|
+
|
|
57
|
+
const tryParse = (obj) => {
|
|
58
|
+
// V3 format: { selections: [{ id, category, include, reason }] }
|
|
59
|
+
if (Array.isArray(obj.selections)) {
|
|
60
|
+
const selected = obj.selections.filter((s) => s.include).map((s) => s.id);
|
|
61
|
+
const reasoning = obj.selections
|
|
62
|
+
.filter((s) => s.reason)
|
|
63
|
+
.map((s) => `${s.id}: ${s.include ? 'included' : 'excluded'} — ${s.reason}`)
|
|
64
|
+
.join('; ');
|
|
65
|
+
return { selected, reasoning };
|
|
66
|
+
}
|
|
67
|
+
// Fallback: { selected: ["id1", "id2"], reasoning: "..." }
|
|
68
|
+
if (Array.isArray(obj.selected)) {
|
|
69
|
+
return { selected: obj.selected, reasoning: obj.reasoning || '' };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Try direct JSON parse
|
|
75
|
+
try {
|
|
76
|
+
const result = tryParse(JSON.parse(text.trim()));
|
|
77
|
+
if (result) return result;
|
|
78
|
+
} catch {
|
|
79
|
+
// Not direct JSON
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Try extracting JSON from markdown code fence
|
|
83
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
|
|
84
|
+
if (fenceMatch) {
|
|
85
|
+
try {
|
|
86
|
+
const result = tryParse(JSON.parse(fenceMatch[1].trim()));
|
|
87
|
+
if (result) return result;
|
|
88
|
+
} catch {
|
|
89
|
+
// Invalid JSON in fence
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Use brace-balanced extraction
|
|
94
|
+
const obj = extractJsonObject(text, 'selections') || extractJsonObject(text, 'selected');
|
|
95
|
+
if (obj) {
|
|
96
|
+
const result = tryParse(obj);
|
|
97
|
+
if (result) return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|