ccraft 1.0.10 → 1.0.12
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 +2 -0
- package/package.json +1 -1
- package/src/commands/create.js +9 -49
- package/src/commands/install.js +18 -91
- package/src/commands/update.js +1 -12
- package/src/constants.js +23 -0
- package/src/prompts/gather.js +17 -0
- package/src/ui/cards.js +41 -19
- package/src/ui/tasks.js +12 -32
- package/src/utils/analysis-cache.js +2 -32
- package/src/utils/api-client.js +6 -8
- package/src/utils/api-file-writer.js +2 -6
- package/src/utils/claude-scorer.js +0 -101
package/bin/claude-craft.js
CHANGED
|
@@ -64,6 +64,7 @@ program
|
|
|
64
64
|
.option('-y, --yes', 'Accept all defaults (non-interactive)')
|
|
65
65
|
.option('-n, --name <name>', 'Project name (non-interactive mode)')
|
|
66
66
|
.option('--description <text>', 'Project description (non-interactive mode)')
|
|
67
|
+
.option('--pro', 'Developer mode — skip persona selection, show all options')
|
|
67
68
|
.option('-d, --dir <path>', 'Parent directory to create the project in (default: cwd)')
|
|
68
69
|
.action(runCreate);
|
|
69
70
|
|
|
@@ -72,6 +73,7 @@ program
|
|
|
72
73
|
.description('Generate Claude Code configuration files in the current project')
|
|
73
74
|
.option('-y, --yes', 'Accept all defaults (non-interactive)')
|
|
74
75
|
.option(`-p, --preset <preset>`, `Apply a framework preset (${Object.keys(PRESET_ALIASES).join(', ')})`)
|
|
76
|
+
.option('--pro', 'Developer mode — skip persona selection, show all options')
|
|
75
77
|
.option('-d, --dir <path>', 'Target directory (default: cwd)')
|
|
76
78
|
.action(runInstall);
|
|
77
79
|
|
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -3,13 +3,12 @@ import { mkdirSync, existsSync, writeFileSync, readdirSync } from 'fs';
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
-
import { gatherCreateProfile
|
|
6
|
+
import { gatherCreateProfile } from '../prompts/gather.js';
|
|
7
7
|
import { themedInput } from '../ui/prompts.js';
|
|
8
8
|
import { callGenerate, ApiError } from '../utils/api-client.js';
|
|
9
9
|
import { runPreflight } from '../utils/preflight.js';
|
|
10
10
|
import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
|
|
11
11
|
import { setupMcps } from '../utils/mcp-setup.js';
|
|
12
|
-
import { scoreWithClaude } from '../utils/claude-scorer.js';
|
|
13
12
|
import { optimizeSettings } from '../utils/claude-optimizer.js';
|
|
14
13
|
import { rewriteClaudeMd } from '../utils/claude-rewriter.js';
|
|
15
14
|
import { detectProject } from '../utils/detect-project.js';
|
|
@@ -19,7 +18,6 @@ import { platformCmd } from '../utils/run-claude.js';
|
|
|
19
18
|
import { runBootstrap } from '../utils/bootstrap-runner.js';
|
|
20
19
|
import {
|
|
21
20
|
writeAnalysisCache,
|
|
22
|
-
writeUserProfile,
|
|
23
21
|
updateManifest,
|
|
24
22
|
readAnalysisCache,
|
|
25
23
|
promoteCache,
|
|
@@ -46,7 +44,7 @@ function renderCreatePhase(number, name, totalPhases) {
|
|
|
46
44
|
console.log();
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
function countTotalItems(summary
|
|
47
|
+
function countTotalItems(summary) {
|
|
50
48
|
const countBucket = (bucket) => {
|
|
51
49
|
if (!bucket) return 0;
|
|
52
50
|
return Object.values(bucket).reduce(
|
|
@@ -54,13 +52,7 @@ function countTotalItems(summary, selectedCandidateIds) {
|
|
|
54
52
|
0,
|
|
55
53
|
);
|
|
56
54
|
};
|
|
57
|
-
|
|
58
|
-
if (selectedCandidateIds === null) {
|
|
59
|
-
total += countBucket(summary.candidates);
|
|
60
|
-
} else {
|
|
61
|
-
total += selectedCandidateIds.length;
|
|
62
|
-
}
|
|
63
|
-
return total;
|
|
55
|
+
return countBucket(summary.guaranteed) + countBucket(summary.candidates);
|
|
64
56
|
}
|
|
65
57
|
|
|
66
58
|
// ── Main command ─────────────────────────────────────────────────────────────
|
|
@@ -170,13 +162,6 @@ export async function runCreate(options = {}) {
|
|
|
170
162
|
: `Created ${chalk.bold(name)}/ (git init skipped — git not available).`);
|
|
171
163
|
}
|
|
172
164
|
|
|
173
|
-
// Build user profile
|
|
174
|
-
const userProfile = {
|
|
175
|
-
intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'],
|
|
176
|
-
sourceControl: 'github',
|
|
177
|
-
documentTools: [],
|
|
178
|
-
};
|
|
179
|
-
|
|
180
165
|
// ================================================================
|
|
181
166
|
// STEP 3: Configuration & Install
|
|
182
167
|
// ================================================================
|
|
@@ -237,7 +222,6 @@ export async function runCreate(options = {}) {
|
|
|
237
222
|
let apiResponse;
|
|
238
223
|
try {
|
|
239
224
|
apiResponse = await callGenerate(
|
|
240
|
-
userProfile,
|
|
241
225
|
{
|
|
242
226
|
...syntheticProjectInfo,
|
|
243
227
|
detectedFiles: [],
|
|
@@ -270,33 +254,14 @@ export async function runCreate(options = {}) {
|
|
|
270
254
|
console.log();
|
|
271
255
|
renderComponentBreakdown(summary);
|
|
272
256
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
257
|
+
// Auto-install all MCPs returned by the server
|
|
258
|
+
const selectedMcps = mcpConfigs || [];
|
|
259
|
+
const mcpKeys = {};
|
|
276
260
|
const securityConfig = { addSecurityGitignore: true };
|
|
277
261
|
|
|
278
|
-
|
|
279
|
-
selectedMcps = mcpConfigs.filter(
|
|
280
|
-
(m) => m.tier === 'core' || m.tier === 'role' || m.tier === 'stack' || m.tier === 'auto' || m.recommended,
|
|
281
|
-
);
|
|
282
|
-
} else {
|
|
283
|
-
const mcpResult = await gatherMcpConfig(mcpConfigs);
|
|
284
|
-
selectedMcps = mcpResult.selectedMcps;
|
|
285
|
-
mcpKeys = mcpResult.mcpKeys;
|
|
286
|
-
|
|
287
|
-
const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
|
|
288
|
-
const proceed = await confirmInstallation(finalSummary);
|
|
289
|
-
if (!proceed) {
|
|
290
|
-
console.log();
|
|
291
|
-
logger.info('Cancelled.');
|
|
292
|
-
process.exit(0);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Cache analysis and profile
|
|
262
|
+
// Cache analysis
|
|
297
263
|
try {
|
|
298
264
|
writeAnalysisCache(targetDir, syntheticProjectInfo, syntheticDetected, null);
|
|
299
|
-
writeUserProfile(targetDir, userProfile);
|
|
300
265
|
} catch (cacheErr) {
|
|
301
266
|
logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
|
|
302
267
|
}
|
|
@@ -304,12 +269,10 @@ export async function runCreate(options = {}) {
|
|
|
304
269
|
// Write files
|
|
305
270
|
let results;
|
|
306
271
|
let filesToWrite;
|
|
307
|
-
let selectedCandidateIds = null;
|
|
308
272
|
|
|
309
273
|
try {
|
|
310
274
|
const installCtx = await runInstallTasks({
|
|
311
275
|
apiResponse,
|
|
312
|
-
selectedCandidateIds: null,
|
|
313
276
|
targetDir,
|
|
314
277
|
selectedMcps,
|
|
315
278
|
mcpKeys,
|
|
@@ -317,16 +280,14 @@ export async function runCreate(options = {}) {
|
|
|
317
280
|
detected: syntheticDetected,
|
|
318
281
|
buildFileList,
|
|
319
282
|
writeApiFiles,
|
|
320
|
-
scoreWithClaude,
|
|
321
283
|
});
|
|
322
284
|
|
|
323
285
|
results = installCtx.results;
|
|
324
286
|
filesToWrite = installCtx.filesToWrite;
|
|
325
|
-
selectedCandidateIds = installCtx.selectedCandidateIds ?? null;
|
|
326
287
|
} catch {
|
|
327
288
|
// Fallback to direct write
|
|
328
289
|
const spinnerWrite = ora('Writing configuration files...').start();
|
|
329
|
-
filesToWrite = buildFileList(apiResponse
|
|
290
|
+
filesToWrite = buildFileList(apiResponse);
|
|
330
291
|
results = await writeApiFiles(filesToWrite, targetDir, {
|
|
331
292
|
force: true,
|
|
332
293
|
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
@@ -436,7 +397,6 @@ export async function runCreate(options = {}) {
|
|
|
436
397
|
// Overwrite cache with real data
|
|
437
398
|
const detected = { ...fsDetected, ...projectInfo };
|
|
438
399
|
writeAnalysisCache(targetDir, projectInfo, detected, null);
|
|
439
|
-
writeUserProfile(targetDir, userProfile);
|
|
440
400
|
} catch (err) {
|
|
441
401
|
logger.debug(`Post-bootstrap analysis failed: ${err.message}`);
|
|
442
402
|
}
|
|
@@ -471,7 +431,7 @@ export async function runCreate(options = {}) {
|
|
|
471
431
|
}
|
|
472
432
|
|
|
473
433
|
// ── Success ──────────────────────────────────────────────────────
|
|
474
|
-
const totalItems = countTotalItems(summary
|
|
434
|
+
const totalItems = countTotalItems(summary);
|
|
475
435
|
const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
|
|
476
436
|
|
|
477
437
|
renderSuccessCard({
|
package/src/commands/install.js
CHANGED
|
@@ -11,26 +11,21 @@ import {
|
|
|
11
11
|
} from '../utils/existing-setup.js';
|
|
12
12
|
import {
|
|
13
13
|
gatherProjectPath,
|
|
14
|
-
gatherUserProfile,
|
|
15
|
-
gatherMcpConfig,
|
|
16
|
-
gatherSecurityConfig,
|
|
17
14
|
confirmInstallation,
|
|
18
15
|
} from '../prompts/gather.js';
|
|
19
16
|
import { callGenerate, ApiError } from '../utils/api-client.js';
|
|
20
17
|
import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
|
|
21
18
|
import { setupMcps } from '../utils/mcp-setup.js';
|
|
22
|
-
import { scoreWithClaude } from '../utils/claude-scorer.js';
|
|
23
19
|
import { optimizeSettings } from '../utils/claude-optimizer.js';
|
|
24
20
|
import { runPreflight } from '../utils/preflight.js';
|
|
25
21
|
import {
|
|
26
22
|
writeAnalysisCache,
|
|
27
|
-
writeUserProfile,
|
|
28
23
|
updateManifest,
|
|
29
24
|
readAnalysisCache,
|
|
30
25
|
promoteCache,
|
|
31
26
|
cleanupAnalysisCache,
|
|
32
27
|
} from '../utils/analysis-cache.js';
|
|
33
|
-
import {
|
|
28
|
+
import { VERSION } from '../constants.js';
|
|
34
29
|
import * as logger from '../utils/logger.js';
|
|
35
30
|
|
|
36
31
|
// UI modules
|
|
@@ -39,16 +34,14 @@ import { renderPhaseHeader } from '../ui/phase-header.js';
|
|
|
39
34
|
import { renderProjectCard, renderSuccessCard } from '../ui/cards.js';
|
|
40
35
|
import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
|
|
41
36
|
import { runExistingSetupTasks, runAnalysisTasks, runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
|
|
42
|
-
import { colors } from '../ui/theme.js';
|
|
43
|
-
import { dotPad } from '../ui/format.js';
|
|
44
37
|
|
|
45
38
|
/**
|
|
46
39
|
* Main install command — 5-phase orchestrator.
|
|
47
40
|
*
|
|
48
|
-
* Phase 1:
|
|
41
|
+
* Phase 1: Preflight (env check + project path)
|
|
49
42
|
* Phase 2: Project Discovery (Claude analysis + filesystem scan)
|
|
50
|
-
* Phase 3:
|
|
51
|
-
* Phase 4:
|
|
43
|
+
* Phase 3: Generate & Install (server API + confirmation + file writing)
|
|
44
|
+
* Phase 4: MCP Verification
|
|
52
45
|
* Phase 5: Finalization (settings optimization + CLAUDE.md rewrite + success)
|
|
53
46
|
*/
|
|
54
47
|
export async function runInstall(options = {}) {
|
|
@@ -72,17 +65,6 @@ export async function runInstall(options = {}) {
|
|
|
72
65
|
targetDir = resolve(options.dir || process.cwd());
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
// ── User profile ───────────────────────────────────────────────
|
|
76
|
-
renderPhaseHeader(1);
|
|
77
|
-
|
|
78
|
-
let userProfile;
|
|
79
|
-
if (options.yes) {
|
|
80
|
-
userProfile = { intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'], sourceControl: 'github', documentTools: [] };
|
|
81
|
-
logger.info('Non-interactive mode — using default profile (web, all intents, github).');
|
|
82
|
-
} else {
|
|
83
|
-
userProfile = await gatherUserProfile();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
68
|
// ================================================================
|
|
87
69
|
// PHASE 2: Project Discovery
|
|
88
70
|
// ================================================================
|
|
@@ -180,10 +162,9 @@ export async function runInstall(options = {}) {
|
|
|
180
162
|
detected = { ...fsDetected, ...projectInfo };
|
|
181
163
|
}
|
|
182
164
|
|
|
183
|
-
// Cache analysis
|
|
165
|
+
// Cache analysis for later phases and future update runs
|
|
184
166
|
try {
|
|
185
167
|
writeAnalysisCache(targetDir, projectInfo, detected, existingContext);
|
|
186
|
-
writeUserProfile(targetDir, userProfile);
|
|
187
168
|
} catch (cacheErr) {
|
|
188
169
|
logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
|
|
189
170
|
}
|
|
@@ -192,24 +173,6 @@ export async function runInstall(options = {}) {
|
|
|
192
173
|
console.log();
|
|
193
174
|
renderProjectCard(projectInfo);
|
|
194
175
|
|
|
195
|
-
// Preset alias injection
|
|
196
|
-
if (options.preset) {
|
|
197
|
-
const alias = PRESET_ALIASES[options.preset];
|
|
198
|
-
if (!alias) {
|
|
199
|
-
logger.error(
|
|
200
|
-
`Unknown preset ${chalk.bold(options.preset)}. ` +
|
|
201
|
-
`Available presets: ${Object.keys(PRESET_ALIASES).map((p) => colors.success(p)).join(', ')}`
|
|
202
|
-
);
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
for (const fw of alias.frameworks) {
|
|
206
|
-
if (!projectInfo.frameworks.includes(fw)) projectInfo.frameworks.push(fw);
|
|
207
|
-
}
|
|
208
|
-
for (const lang of alias.languages) {
|
|
209
|
-
if (!projectInfo.languages.includes(lang)) projectInfo.languages.push(lang);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
176
|
// ================================================================
|
|
214
177
|
// PHASE 3: Configuration
|
|
215
178
|
// ================================================================
|
|
@@ -220,7 +183,6 @@ export async function runInstall(options = {}) {
|
|
|
220
183
|
let apiResponse;
|
|
221
184
|
try {
|
|
222
185
|
apiResponse = await callGenerate(
|
|
223
|
-
userProfile,
|
|
224
186
|
{
|
|
225
187
|
name: projectInfo.name,
|
|
226
188
|
projectType: projectInfo.projectType,
|
|
@@ -241,7 +203,7 @@ export async function runInstall(options = {}) {
|
|
|
241
203
|
detectedFiles: detected._rootFiles || [],
|
|
242
204
|
databases: projectInfo.databases || detected.databases || [],
|
|
243
205
|
},
|
|
244
|
-
{
|
|
206
|
+
{ projectPath: targetDir },
|
|
245
207
|
);
|
|
246
208
|
spinner3.succeed('Server returned configuration.');
|
|
247
209
|
} catch (err) {
|
|
@@ -255,27 +217,17 @@ export async function runInstall(options = {}) {
|
|
|
255
217
|
|
|
256
218
|
const { summary, mcpConfigs } = apiResponse;
|
|
257
219
|
|
|
258
|
-
// Display
|
|
220
|
+
// Display component summary
|
|
259
221
|
console.log();
|
|
260
222
|
renderComponentBreakdown(summary);
|
|
261
223
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (options.yes) {
|
|
268
|
-
selectedMcps = mcpConfigs.filter(
|
|
269
|
-
(m) => m.tier === 'core' || m.tier === 'role' || m.tier === 'stack' || m.tier === 'auto' || m.recommended
|
|
270
|
-
);
|
|
271
|
-
securityConfig = { addSecurityGitignore: true };
|
|
272
|
-
} else {
|
|
273
|
-
const mcpResult = await gatherMcpConfig(mcpConfigs);
|
|
274
|
-
selectedMcps = mcpResult.selectedMcps;
|
|
275
|
-
mcpKeys = mcpResult.mcpKeys;
|
|
276
|
-
|
|
277
|
-
securityConfig = gatherSecurityConfig(detected);
|
|
224
|
+
// All MCPs are auto-installed; no interactive selection
|
|
225
|
+
const selectedMcps = mcpConfigs || [];
|
|
226
|
+
const mcpKeys = {};
|
|
227
|
+
const securityConfig = { addSecurityGitignore: true };
|
|
278
228
|
|
|
229
|
+
// Confirmation gate (skipped in non-interactive mode)
|
|
230
|
+
if (!options.yes) {
|
|
279
231
|
const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
|
|
280
232
|
const proceed = await confirmInstallation(finalSummary);
|
|
281
233
|
if (!proceed) {
|
|
@@ -292,12 +244,10 @@ export async function runInstall(options = {}) {
|
|
|
292
244
|
|
|
293
245
|
let results;
|
|
294
246
|
let filesToWrite;
|
|
295
|
-
let selectedCandidateIds = null;
|
|
296
247
|
|
|
297
248
|
try {
|
|
298
249
|
const installCtx = await runInstallTasks({
|
|
299
250
|
apiResponse,
|
|
300
|
-
selectedCandidateIds: null,
|
|
301
251
|
targetDir,
|
|
302
252
|
selectedMcps,
|
|
303
253
|
mcpKeys,
|
|
@@ -305,29 +255,14 @@ export async function runInstall(options = {}) {
|
|
|
305
255
|
detected,
|
|
306
256
|
buildFileList,
|
|
307
257
|
writeApiFiles,
|
|
308
|
-
scoreWithClaude,
|
|
309
258
|
});
|
|
310
259
|
|
|
311
260
|
results = installCtx.results;
|
|
312
261
|
filesToWrite = installCtx.filesToWrite;
|
|
313
|
-
selectedCandidateIds = installCtx.selectedCandidateIds ?? null;
|
|
314
262
|
} catch (installErr) {
|
|
315
263
|
// Fallback to sequential if task runner fails
|
|
316
|
-
const candidateCount = apiResponse.candidates?.items?.length || 0;
|
|
317
|
-
|
|
318
|
-
if (apiResponse.prompts?.scoring && candidateCount > 0) {
|
|
319
|
-
const spinner4 = ora(`Evaluating ${candidateCount} optional candidates...`).start();
|
|
320
|
-
const scoreResult = await scoreWithClaude(apiResponse.prompts.scoring, targetDir);
|
|
321
|
-
if (scoreResult.selected) {
|
|
322
|
-
selectedCandidateIds = scoreResult.selected;
|
|
323
|
-
spinner4.succeed(`Selected ${selectedCandidateIds.length}/${candidateCount} candidates.`);
|
|
324
|
-
} else {
|
|
325
|
-
spinner4.warn(`Scoring unavailable — including all ${candidateCount} candidates.`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
264
|
const spinnerWrite = ora('Writing configuration files...').start();
|
|
330
|
-
filesToWrite = buildFileList(apiResponse
|
|
265
|
+
filesToWrite = buildFileList(apiResponse);
|
|
331
266
|
results = await writeApiFiles(filesToWrite, targetDir, {
|
|
332
267
|
force: true,
|
|
333
268
|
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
@@ -417,7 +352,7 @@ export async function runInstall(options = {}) {
|
|
|
417
352
|
}
|
|
418
353
|
|
|
419
354
|
// ── Success ──────────────────────────────────────────────────────
|
|
420
|
-
const totalItems = countTotalItems(summary
|
|
355
|
+
const totalItems = countTotalItems(summary);
|
|
421
356
|
const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
|
|
422
357
|
|
|
423
358
|
renderSuccessCard({
|
|
@@ -457,9 +392,9 @@ export async function runInstall(options = {}) {
|
|
|
457
392
|
}
|
|
458
393
|
|
|
459
394
|
/**
|
|
460
|
-
* Count total installed items from
|
|
395
|
+
* Count total installed items from the summary.
|
|
461
396
|
*/
|
|
462
|
-
function countTotalItems(summary
|
|
397
|
+
function countTotalItems(summary) {
|
|
463
398
|
const countBucket = (bucket) => {
|
|
464
399
|
if (!bucket) return 0;
|
|
465
400
|
return Object.values(bucket).reduce(
|
|
@@ -468,13 +403,5 @@ function countTotalItems(summary, selectedCandidateIds) {
|
|
|
468
403
|
);
|
|
469
404
|
};
|
|
470
405
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (selectedCandidateIds === null) {
|
|
474
|
-
total += countBucket(summary.candidates);
|
|
475
|
-
} else {
|
|
476
|
-
total += selectedCandidateIds.length;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return total;
|
|
406
|
+
return countBucket(summary.guaranteed) + countBucket(summary.candidates);
|
|
480
407
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* calls /api/update to get delta components, and installs only what's new.
|
|
4
4
|
*
|
|
5
5
|
* Flow:
|
|
6
|
-
* 1. Load stored
|
|
6
|
+
* 1. Load stored analysis from .claude/.claude-craft/
|
|
7
7
|
* 2. Re-run project analysis (detectProject + analyzeWithClaude)
|
|
8
8
|
* 3. Extract installed relative paths from manifest
|
|
9
9
|
* 4. Call /api/update — server returns only new/unlocked components
|
|
@@ -16,7 +16,6 @@ import ora from 'ora';
|
|
|
16
16
|
import { detectProject } from '../utils/detect-project.js';
|
|
17
17
|
import { analyzeWithClaude } from '../utils/claude-analyzer.js';
|
|
18
18
|
import {
|
|
19
|
-
readUserProfile,
|
|
20
19
|
readPermanentAnalysis,
|
|
21
20
|
readInstalledManifest,
|
|
22
21
|
mergePermanentManifest,
|
|
@@ -135,18 +134,9 @@ export async function runUpdate(options = {}) {
|
|
|
135
134
|
});
|
|
136
135
|
|
|
137
136
|
// ── Guard: must have a previous install ───────────────────────────
|
|
138
|
-
const storedProfile = readUserProfile(targetDir);
|
|
139
137
|
const previousAnalysis = readPermanentAnalysis(targetDir);
|
|
140
138
|
const installedManifest = readInstalledManifest(targetDir);
|
|
141
139
|
|
|
142
|
-
if (!storedProfile) {
|
|
143
|
-
logger.error(
|
|
144
|
-
'No stored user profile found. Re-run: ' + chalk.bold('ccraft install') +
|
|
145
|
-
' to rebuild the profile cache.',
|
|
146
|
-
);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
140
|
if (!previousAnalysis) {
|
|
151
141
|
logger.error(
|
|
152
142
|
'No stored project analysis found. Re-run: ' + chalk.bold('ccraft install') +
|
|
@@ -231,7 +221,6 @@ export async function runUpdate(options = {}) {
|
|
|
231
221
|
let updateResponse;
|
|
232
222
|
try {
|
|
233
223
|
updateResponse = await callUpdate(
|
|
234
|
-
storedProfile,
|
|
235
224
|
currentProjectInfo,
|
|
236
225
|
previousAnalysis,
|
|
237
226
|
installedRelativePaths,
|
package/src/constants.js
CHANGED
|
@@ -20,6 +20,29 @@ export const INTENTS = [
|
|
|
20
20
|
{ name: 'Code review', value: 'reviewing', description: 'Review PRs, suggest improvements, catch issues' },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
+
// ── Personas ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export const PERSONAS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'I\'m building something new — describe what I want, Claude handles the rest',
|
|
28
|
+
value: 'vibe',
|
|
29
|
+
description: 'Streamlined setup with smart defaults',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'I\'m a developer — full control over agents, skills, MCPs, and workflows',
|
|
33
|
+
value: 'developer',
|
|
34
|
+
description: 'All configuration options available',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const VIBE_DEFAULTS = {
|
|
39
|
+
// Intentionally narrowed to core intents for non-technical users.
|
|
40
|
+
// Full intent list is available in the developer path via gatherUserProfile().
|
|
41
|
+
intents: ['implementing', 'debugging'],
|
|
42
|
+
sourceControl: 'github',
|
|
43
|
+
documentTools: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
23
46
|
// ── Source control platforms ─────────────────────────────────────────
|
|
24
47
|
|
|
25
48
|
export const SOURCE_CONTROLS = [
|
package/src/prompts/gather.js
CHANGED
|
@@ -7,12 +7,29 @@ import {
|
|
|
7
7
|
PROJECT_TYPES,
|
|
8
8
|
SOURCE_CONTROLS,
|
|
9
9
|
DOCUMENT_TOOLS,
|
|
10
|
+
PERSONAS,
|
|
10
11
|
} from '../constants.js';
|
|
11
12
|
import { validateApiKeyFormat } from '../utils/mcp-setup.js';
|
|
12
13
|
import { themedSelect, themedCheckbox, themedConfirm, themedPassword, themedInput } from '../ui/prompts.js';
|
|
13
14
|
import { renderSummaryCard, renderWarningCard } from '../ui/cards.js';
|
|
14
15
|
import { colors } from '../ui/theme.js';
|
|
15
16
|
|
|
17
|
+
// ── Persona selection ───────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export async function gatherPersona() {
|
|
20
|
+
console.log();
|
|
21
|
+
const persona = await themedSelect({
|
|
22
|
+
message: 'How do you want to use Claude Code?',
|
|
23
|
+
choices: PERSONAS.map((p) => ({
|
|
24
|
+
name: p.name,
|
|
25
|
+
value: p.value,
|
|
26
|
+
description: p.description,
|
|
27
|
+
})),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return persona;
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
// ── Project path ────────────────────────────────────────────────────────
|
|
17
34
|
|
|
18
35
|
export async function gatherProjectPath() {
|
package/src/ui/cards.js
CHANGED
|
@@ -120,33 +120,55 @@ export function renderSummaryCard(summary) {
|
|
|
120
120
|
/**
|
|
121
121
|
* Render the final success card.
|
|
122
122
|
*/
|
|
123
|
-
export function renderSuccessCard({ totalItems, mcpCount, mcpsNeedingKeys }) {
|
|
123
|
+
export function renderSuccessCard({ totalItems, mcpCount, mcpsNeedingKeys, persona }) {
|
|
124
|
+
const isVibe = persona === 'vibe';
|
|
124
125
|
const lines = [];
|
|
125
|
-
lines.push(chalk.bold.green(' Installation complete!'));
|
|
126
|
-
lines.push('');
|
|
127
|
-
lines.push(` ${chalk.dim('Settings installed'.padEnd(22))}${colors.success(String(totalItems))} ${chalk.dim('(guaranteed + selected)')}`);
|
|
128
|
-
lines.push(` ${chalk.dim('MCP servers'.padEnd(22))}${colors.success(String(mcpCount))}`);
|
|
129
126
|
|
|
130
|
-
if (
|
|
127
|
+
if (isVibe) {
|
|
128
|
+
lines.push(chalk.bold.green(' Claude is ready to build your app!'));
|
|
131
129
|
lines.push('');
|
|
132
|
-
lines.push(chalk.
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
lines.push(` ${chalk.dim('Components installed'.padEnd(22))}${colors.success(String(totalItems))}`);
|
|
131
|
+
lines.push(` ${chalk.dim('MCP servers'.padEnd(22))}${colors.success(String(mcpCount))}`);
|
|
132
|
+
|
|
133
|
+
if (mcpsNeedingKeys.length > 0) {
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push(chalk.yellow(' Some MCP servers need an API key:'));
|
|
136
|
+
for (const r of mcpsNeedingKeys) {
|
|
137
|
+
lines.push(chalk.yellow(` ${chalk.dim('•')} ${r.id}: set ${r.apiKey.keyName}`));
|
|
138
|
+
}
|
|
135
139
|
}
|
|
136
|
-
}
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push(chalk.dim(' Next steps:'));
|
|
143
|
+
lines.push(chalk.dim(` 1. Start Claude Code and describe what you want to build`));
|
|
144
|
+
lines.push(chalk.dim(` 2. Claude will handle the rest!`));
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(chalk.bold.green(' Installation complete!'));
|
|
147
|
+
lines.push('');
|
|
148
|
+
lines.push(` ${chalk.dim('Settings installed'.padEnd(22))}${colors.success(String(totalItems))} ${chalk.dim('(guaranteed + selected)')}`);
|
|
149
|
+
lines.push(` ${chalk.dim('MCP servers'.padEnd(22))}${colors.success(String(mcpCount))}`);
|
|
150
|
+
|
|
151
|
+
if (mcpsNeedingKeys.length > 0) {
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push(chalk.yellow(' MCP servers needing API keys:'));
|
|
154
|
+
for (const r of mcpsNeedingKeys) {
|
|
155
|
+
lines.push(chalk.yellow(` ${chalk.dim('•')} ${r.id}: set ${r.apiKey.keyName}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
lines.push('');
|
|
160
|
+
lines.push(chalk.dim(' Next steps:'));
|
|
161
|
+
lines.push(chalk.dim(` 1. Read ${chalk.underline('USER_GUIDE.md')} for a full feature overview`));
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
let step = 2;
|
|
164
|
+
if (mcpsNeedingKeys.length > 0) {
|
|
165
|
+
lines.push(chalk.dim(` ${step}. Set missing API keys for MCP servers (see above)`));
|
|
166
|
+
step++;
|
|
167
|
+
}
|
|
168
|
+
lines.push(chalk.dim(` ${step}. Start Claude Code and try out some commands!`));
|
|
145
169
|
step++;
|
|
170
|
+
lines.push(chalk.dim(` ${step}. Customize .claude/ to your needs`));
|
|
146
171
|
}
|
|
147
|
-
lines.push(chalk.dim(` ${step}. Start Claude Code and try out some commands!`));
|
|
148
|
-
step++;
|
|
149
|
-
lines.push(chalk.dim(` ${step}. Customize .claude/ to your needs`));
|
|
150
172
|
|
|
151
173
|
const content = lines.join('\n');
|
|
152
174
|
|
package/src/ui/tasks.js
CHANGED
|
@@ -104,7 +104,6 @@ export async function runAnalysisTasks(targetDir, { analyzeWithClaude, detectPro
|
|
|
104
104
|
*/
|
|
105
105
|
export async function runInstallTasks({
|
|
106
106
|
apiResponse,
|
|
107
|
-
selectedCandidateIds,
|
|
108
107
|
targetDir,
|
|
109
108
|
selectedMcps,
|
|
110
109
|
mcpKeys,
|
|
@@ -112,43 +111,24 @@ export async function runInstallTasks({
|
|
|
112
111
|
detected,
|
|
113
112
|
buildFileList,
|
|
114
113
|
writeApiFiles,
|
|
115
|
-
scoreWithClaude,
|
|
116
114
|
}) {
|
|
117
115
|
const ctx = {};
|
|
118
116
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const taskList = [];
|
|
123
|
-
|
|
124
|
-
if (hasScoring) {
|
|
125
|
-
taskList.push({
|
|
126
|
-
title: `Evaluating ${candidateCount} optional candidates with Claude`,
|
|
117
|
+
const taskList = [
|
|
118
|
+
{
|
|
119
|
+
title: 'Writing configuration files',
|
|
127
120
|
task: async (ctx) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
ctx.filesToWrite = buildFileList(apiResponse);
|
|
122
|
+
ctx.results = await writeApiFiles(ctx.filesToWrite, targetDir, {
|
|
123
|
+
force: true,
|
|
124
|
+
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
125
|
+
mcpKeys,
|
|
126
|
+
securityConfig,
|
|
127
|
+
detected,
|
|
128
|
+
});
|
|
134
129
|
},
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
taskList.push({
|
|
139
|
-
title: 'Writing configuration files',
|
|
140
|
-
task: async (ctx) => {
|
|
141
|
-
const ids = ctx.selectedCandidateIds ?? selectedCandidateIds;
|
|
142
|
-
ctx.filesToWrite = buildFileList(apiResponse, ids);
|
|
143
|
-
ctx.results = await writeApiFiles(ctx.filesToWrite, targetDir, {
|
|
144
|
-
force: true,
|
|
145
|
-
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
146
|
-
mcpKeys,
|
|
147
|
-
securityConfig,
|
|
148
|
-
detected,
|
|
149
|
-
});
|
|
150
130
|
},
|
|
151
|
-
|
|
131
|
+
];
|
|
152
132
|
|
|
153
133
|
const tasks = createTaskRunner(taskList);
|
|
154
134
|
await tasks.run(ctx);
|
|
@@ -16,7 +16,7 @@ const CACHE_DIR = '.claude/.claude-craft-temp';
|
|
|
16
16
|
const PERMANENT_DIR = '.claude/.claude-craft';
|
|
17
17
|
|
|
18
18
|
/** Files promoted from temp → permanent after install. */
|
|
19
|
-
const PERMANENT_FILES = ['project-analysis.json', 'project-context.md', 'installed-manifest.json'
|
|
19
|
+
const PERMANENT_FILES = ['project-analysis.json', 'project-context.md', 'installed-manifest.json'];
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Resolve the temp cache directory path for a given target directory.
|
|
@@ -315,37 +315,7 @@ export function formatRichProjectContext(projectInfo, detected) {
|
|
|
315
315
|
return lines.join('\n').trimEnd();
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
* Write user profile to the temp cache directory.
|
|
320
|
-
* Called alongside writeAnalysisCache during install.
|
|
321
|
-
*
|
|
322
|
-
* @param {string} targetDir - Project root
|
|
323
|
-
* @param {object} userProfile - { intents, sourceControl, documentTools }
|
|
324
|
-
*/
|
|
325
|
-
export function writeUserProfile(targetDir, userProfile) {
|
|
326
|
-
const dir = cachePath(targetDir);
|
|
327
|
-
mkdirSync(dir, { recursive: true });
|
|
328
|
-
writeFileSync(join(dir, 'user-profile.json'), JSON.stringify(userProfile, null, 2), 'utf8');
|
|
329
|
-
logger.debug('User profile written to analysis cache.');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Read the stored user profile from the permanent .claude/.claude-craft/ directory.
|
|
334
|
-
* Returns null if not found.
|
|
335
|
-
*
|
|
336
|
-
* @param {string} targetDir - Project root
|
|
337
|
-
* @returns {object|null}
|
|
338
|
-
*/
|
|
339
|
-
export function readUserProfile(targetDir) {
|
|
340
|
-
const filePath = join(permanentPath(targetDir), 'user-profile.json');
|
|
341
|
-
try {
|
|
342
|
-
if (!existsSync(filePath)) return null;
|
|
343
|
-
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
344
|
-
} catch (err) {
|
|
345
|
-
logger.debug(`Failed to read user profile: ${err.message}`);
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
318
|
+
// writeUserProfile and readUserProfile removed — user profile no longer used.
|
|
349
319
|
|
|
350
320
|
/**
|
|
351
321
|
* Read the permanent project analysis JSON.
|
package/src/utils/api-client.js
CHANGED
|
@@ -45,12 +45,11 @@ export function saveConfig(config) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Call POST /api/generate on the server.
|
|
47
47
|
*
|
|
48
|
-
* @param {object} profile - { intents, sourceControl, documentTools }
|
|
49
48
|
* @param {object} analysis - Project analysis data
|
|
50
|
-
* @param {object} [options] - {
|
|
49
|
+
* @param {object} [options] - { projectPath }
|
|
51
50
|
* @returns {Promise<{ files, summary, mcpConfigs, serverVersion }>}
|
|
52
51
|
*/
|
|
53
|
-
export async function callGenerate(
|
|
52
|
+
export async function callGenerate(analysis, options = {}) {
|
|
54
53
|
const config = loadConfig();
|
|
55
54
|
if (!config?.apiKey) {
|
|
56
55
|
throw new ApiError(
|
|
@@ -75,7 +74,7 @@ export async function callGenerate(profile, analysis, options = {}) {
|
|
|
75
74
|
'X-Claude-Craft-Version': VERSION,
|
|
76
75
|
'X-Claude-Craft-Api-Version': '1',
|
|
77
76
|
},
|
|
78
|
-
body: JSON.stringify({
|
|
77
|
+
body: JSON.stringify({ analysis, options }),
|
|
79
78
|
signal: controller.signal,
|
|
80
79
|
});
|
|
81
80
|
|
|
@@ -131,13 +130,12 @@ export async function callGenerate(profile, analysis, options = {}) {
|
|
|
131
130
|
* Call POST /api/update on the server.
|
|
132
131
|
* Returns delta components (new files not already installed) + change summary.
|
|
133
132
|
*
|
|
134
|
-
* @param {object} profile - { intents, sourceControl, documentTools }
|
|
135
133
|
* @param {object} currentAnalysis - Freshly computed project analysis
|
|
136
134
|
* @param {object} previousAnalysis - Previously stored project analysis
|
|
137
135
|
* @param {string[]} installedRelativePaths - Relative file paths already on disk
|
|
138
|
-
* @returns {Promise<{ changes, guaranteed,
|
|
136
|
+
* @returns {Promise<{ changes, guaranteed, mcpConfigs, summary }>}
|
|
139
137
|
*/
|
|
140
|
-
export async function callUpdate(
|
|
138
|
+
export async function callUpdate(currentAnalysis, previousAnalysis, installedRelativePaths) {
|
|
141
139
|
const config = loadConfig();
|
|
142
140
|
if (!config?.apiKey) {
|
|
143
141
|
throw new ApiError(
|
|
@@ -162,7 +160,7 @@ export async function callUpdate(profile, currentAnalysis, previousAnalysis, ins
|
|
|
162
160
|
'X-Claude-Craft-Version': VERSION,
|
|
163
161
|
'X-Claude-Craft-Api-Version': '1',
|
|
164
162
|
},
|
|
165
|
-
body: JSON.stringify({
|
|
163
|
+
body: JSON.stringify({ currentAnalysis, previousAnalysis, installedPaths: installedRelativePaths }),
|
|
166
164
|
signal: controller.signal,
|
|
167
165
|
});
|
|
168
166
|
|
|
@@ -132,19 +132,15 @@ export async function writeApiFiles(files, targetDir, opts = {}) {
|
|
|
132
132
|
/**
|
|
133
133
|
* Build a flat file list from a V3 API response.
|
|
134
134
|
*
|
|
135
|
-
* @param {object} apiResponse -
|
|
136
|
-
* @param {string[]|null} selectedCandidateIds - IDs selected by Claude, or null for all
|
|
135
|
+
* @param {object} apiResponse - Response with guaranteed.files and candidates.items
|
|
137
136
|
* @returns {Array<{relativePath: string, content: string, type: string}>}
|
|
138
137
|
*/
|
|
139
|
-
export function buildFileList(apiResponse
|
|
138
|
+
export function buildFileList(apiResponse) {
|
|
140
139
|
const files = [...(apiResponse.guaranteed?.files || [])];
|
|
141
140
|
|
|
142
141
|
const candidates = apiResponse.candidates?.items || [];
|
|
143
|
-
const selectedSet = selectedCandidateIds ? new Set(selectedCandidateIds) : null;
|
|
144
142
|
|
|
145
143
|
for (const candidate of candidates) {
|
|
146
|
-
// Skip unselected candidates (null = include all)
|
|
147
|
-
if (selectedSet && !selectedSet.has(candidate.id)) continue;
|
|
148
144
|
|
|
149
145
|
if (Array.isArray(candidate.files) && candidate.files.length > 0) {
|
|
150
146
|
// Multi-file candidates (skills with references/)
|
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
}
|