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.
Files changed (40) hide show
  1. package/bin/claude-craft.js +85 -0
  2. package/package.json +39 -0
  3. package/src/commands/auth.js +43 -0
  4. package/src/commands/create.js +543 -0
  5. package/src/commands/install.js +480 -0
  6. package/src/commands/logout.js +24 -0
  7. package/src/commands/update.js +339 -0
  8. package/src/constants.js +299 -0
  9. package/src/generators/directories.js +30 -0
  10. package/src/generators/metadata.js +57 -0
  11. package/src/generators/security.js +39 -0
  12. package/src/prompts/gather.js +308 -0
  13. package/src/ui/brand.js +62 -0
  14. package/src/ui/cards.js +179 -0
  15. package/src/ui/format.js +55 -0
  16. package/src/ui/phase-header.js +20 -0
  17. package/src/ui/prompts.js +56 -0
  18. package/src/ui/tables.js +89 -0
  19. package/src/ui/tasks.js +258 -0
  20. package/src/ui/theme.js +83 -0
  21. package/src/utils/analysis-cache.js +519 -0
  22. package/src/utils/api-client.js +253 -0
  23. package/src/utils/api-file-writer.js +197 -0
  24. package/src/utils/bootstrap-runner.js +148 -0
  25. package/src/utils/claude-analyzer.js +255 -0
  26. package/src/utils/claude-optimizer.js +341 -0
  27. package/src/utils/claude-rewriter.js +553 -0
  28. package/src/utils/claude-scorer.js +101 -0
  29. package/src/utils/description-analyzer.js +116 -0
  30. package/src/utils/detect-project.js +1276 -0
  31. package/src/utils/existing-setup.js +341 -0
  32. package/src/utils/file-writer.js +64 -0
  33. package/src/utils/json-extract.js +56 -0
  34. package/src/utils/logger.js +27 -0
  35. package/src/utils/mcp-setup.js +461 -0
  36. package/src/utils/preflight.js +112 -0
  37. package/src/utils/prompt-api-key.js +59 -0
  38. package/src/utils/run-claude.js +152 -0
  39. package/src/utils/security.js +82 -0
  40. 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
+ }