@neurcode-ai/cli 0.9.27 → 0.9.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +58 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +38 -0
- package/dist/api-client.js.map +1 -1
- package/dist/commands/allow.d.ts.map +1 -1
- package/dist/commands/allow.js +5 -19
- package/dist/commands/allow.js.map +1 -1
- package/dist/commands/apply.d.ts +1 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +105 -46
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +83 -24
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/plan.d.ts +4 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +518 -42
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +629 -0
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/prompt.d.ts +7 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +130 -26
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/ship.d.ts +32 -0
- package/dist/commands/ship.d.ts.map +1 -1
- package/dist/commands/ship.js +1404 -75
- package/dist/commands/ship.js.map +1 -1
- package/dist/commands/verify.d.ts +6 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +542 -115
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +89 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/custom-policy-rules.d.ts +21 -0
- package/dist/utils/custom-policy-rules.d.ts.map +1 -0
- package/dist/utils/custom-policy-rules.js +71 -0
- package/dist/utils/custom-policy-rules.js.map +1 -0
- package/dist/utils/plan-cache.d.ts.map +1 -1
- package/dist/utils/plan-cache.js +4 -0
- package/dist/utils/plan-cache.js.map +1 -1
- package/dist/utils/policy-audit.d.ts +29 -0
- package/dist/utils/policy-audit.d.ts.map +1 -0
- package/dist/utils/policy-audit.js +208 -0
- package/dist/utils/policy-audit.js.map +1 -0
- package/dist/utils/policy-exceptions.d.ts +96 -0
- package/dist/utils/policy-exceptions.d.ts.map +1 -0
- package/dist/utils/policy-exceptions.js +389 -0
- package/dist/utils/policy-exceptions.js.map +1 -0
- package/dist/utils/policy-governance.d.ts +24 -0
- package/dist/utils/policy-governance.d.ts.map +1 -0
- package/dist/utils/policy-governance.js +124 -0
- package/dist/utils/policy-governance.js.map +1 -0
- package/dist/utils/policy-packs.d.ts +72 -1
- package/dist/utils/policy-packs.d.ts.map +1 -1
- package/dist/utils/policy-packs.js +285 -0
- package/dist/utils/policy-packs.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/plan.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.detectIntentMode = detectIntentMode;
|
|
|
37
37
|
exports.planCommand = planCommand;
|
|
38
38
|
const fs_1 = require("fs");
|
|
39
39
|
const path_1 = require("path");
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
40
41
|
const config_1 = require("../config");
|
|
41
42
|
const api_client_1 = require("../api-client");
|
|
42
43
|
const project_detector_1 = require("../utils/project-detector");
|
|
@@ -267,6 +268,125 @@ function renderCacheMissReason(reason, bestIntentSimilarity) {
|
|
|
267
268
|
}
|
|
268
269
|
return reasonText[reason];
|
|
269
270
|
}
|
|
271
|
+
function hasPersistedPlanId(planId) {
|
|
272
|
+
return typeof planId === 'string' && planId.trim().length > 0 && planId !== 'unknown';
|
|
273
|
+
}
|
|
274
|
+
function toUnixPath(filePath) {
|
|
275
|
+
return filePath.replace(/\\/g, '/');
|
|
276
|
+
}
|
|
277
|
+
function parsePositiveInt(raw) {
|
|
278
|
+
if (!raw)
|
|
279
|
+
return null;
|
|
280
|
+
const parsed = Number(raw);
|
|
281
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
282
|
+
return null;
|
|
283
|
+
return Math.floor(parsed);
|
|
284
|
+
}
|
|
285
|
+
function parseSnapshotMode(raw) {
|
|
286
|
+
if (!raw)
|
|
287
|
+
return null;
|
|
288
|
+
const normalized = raw.trim().toLowerCase();
|
|
289
|
+
if (normalized === 'auto' || normalized === 'full' || normalized === 'off') {
|
|
290
|
+
return normalized;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function resolveSnapshotMode(optionMode) {
|
|
295
|
+
if (process.env.NEURCODE_PLAN_SKIP_SNAPSHOTS === '1') {
|
|
296
|
+
return 'off';
|
|
297
|
+
}
|
|
298
|
+
const optionParsed = parseSnapshotMode(optionMode);
|
|
299
|
+
const envMode = parseSnapshotMode(process.env.NEURCODE_PLAN_SNAPSHOT_MODE);
|
|
300
|
+
return optionParsed || envMode || 'auto';
|
|
301
|
+
}
|
|
302
|
+
function resolveSnapshotMaxFiles(mode, optionValue) {
|
|
303
|
+
const envOverride = parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_MAX_FILES);
|
|
304
|
+
const configured = optionValue && optionValue > 0 ? Math.floor(optionValue) : envOverride;
|
|
305
|
+
if (configured && configured > 0)
|
|
306
|
+
return configured;
|
|
307
|
+
return mode === 'full' ? 500 : 40;
|
|
308
|
+
}
|
|
309
|
+
function resolveSnapshotBudgetMs(mode, optionValue) {
|
|
310
|
+
const envOverride = parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_BUDGET_MS);
|
|
311
|
+
const configured = optionValue && optionValue > 0 ? Math.floor(optionValue) : envOverride;
|
|
312
|
+
if (typeof configured === 'number' && configured > 0)
|
|
313
|
+
return configured;
|
|
314
|
+
return mode === 'full' ? 0 : 60_000;
|
|
315
|
+
}
|
|
316
|
+
function resolveSnapshotMaxBytes(mode) {
|
|
317
|
+
const envOverride = parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_MAX_BYTES);
|
|
318
|
+
if (typeof envOverride === 'number' && envOverride > 0)
|
|
319
|
+
return envOverride;
|
|
320
|
+
return mode === 'full' ? 0 : 256 * 1024;
|
|
321
|
+
}
|
|
322
|
+
function resolveSnapshotBatchSize(mode) {
|
|
323
|
+
const envOverride = parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_BATCH_SIZE);
|
|
324
|
+
if (typeof envOverride === 'number' && envOverride > 0) {
|
|
325
|
+
return Math.min(100, Math.max(1, envOverride));
|
|
326
|
+
}
|
|
327
|
+
return mode === 'full' ? 30 : 20;
|
|
328
|
+
}
|
|
329
|
+
function resolveSnapshotBatchTimeoutMs(singleRequestTimeoutMs, mode) {
|
|
330
|
+
const envOverride = parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_BATCH_TIMEOUT_MS);
|
|
331
|
+
if (typeof envOverride === 'number' && envOverride > 0)
|
|
332
|
+
return envOverride;
|
|
333
|
+
const multiplier = mode === 'full' ? 5 : 4;
|
|
334
|
+
return Math.max(singleRequestTimeoutMs * multiplier, 12000);
|
|
335
|
+
}
|
|
336
|
+
function isBatchSnapshotEndpointUnsupported(error) {
|
|
337
|
+
if (!(error instanceof Error))
|
|
338
|
+
return false;
|
|
339
|
+
const message = error.message.toLowerCase();
|
|
340
|
+
return (message.includes('status 404') ||
|
|
341
|
+
message.includes('not found') ||
|
|
342
|
+
message.includes('not_found') ||
|
|
343
|
+
message.includes('method not allowed'));
|
|
344
|
+
}
|
|
345
|
+
function getSnapshotManifestPath(projectRoot) {
|
|
346
|
+
return (0, path_1.join)(projectRoot, '.neurcode', 'snapshot-manifest.json');
|
|
347
|
+
}
|
|
348
|
+
function loadSnapshotManifest(projectRoot) {
|
|
349
|
+
const manifestPath = getSnapshotManifestPath(projectRoot);
|
|
350
|
+
if (!(0, fs_1.existsSync)(manifestPath)) {
|
|
351
|
+
return {
|
|
352
|
+
version: 1,
|
|
353
|
+
updatedAt: new Date().toISOString(),
|
|
354
|
+
entries: {},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(manifestPath, 'utf-8'));
|
|
359
|
+
if (parsed && parsed.version === 1 && parsed.entries && typeof parsed.entries === 'object') {
|
|
360
|
+
return {
|
|
361
|
+
version: 1,
|
|
362
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
363
|
+
entries: parsed.entries,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Ignore manifest parse errors and start fresh.
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
version: 1,
|
|
372
|
+
updatedAt: new Date().toISOString(),
|
|
373
|
+
entries: {},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function saveSnapshotManifest(projectRoot, manifest) {
|
|
377
|
+
const manifestPath = getSnapshotManifestPath(projectRoot);
|
|
378
|
+
const manifestDir = (0, path_1.join)(projectRoot, '.neurcode');
|
|
379
|
+
if (!(0, fs_1.existsSync)(manifestDir)) {
|
|
380
|
+
(0, fs_1.mkdirSync)(manifestDir, { recursive: true });
|
|
381
|
+
}
|
|
382
|
+
(0, fs_1.writeFileSync)(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
383
|
+
}
|
|
384
|
+
function computeSnapshotHash(content) {
|
|
385
|
+
return (0, crypto_1.createHash)('sha256').update(content, 'utf-8').digest('hex');
|
|
386
|
+
}
|
|
387
|
+
function emitPlanJson(payload) {
|
|
388
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
389
|
+
}
|
|
270
390
|
function emitCachedPlanHit(input) {
|
|
271
391
|
if (input.touchKey) {
|
|
272
392
|
try {
|
|
@@ -283,14 +403,33 @@ function emitCachedPlanHit(input) {
|
|
|
283
403
|
else {
|
|
284
404
|
console.log(chalk.dim(`⚡ Using cached plan (created: ${createdAtLabel})\n`));
|
|
285
405
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
406
|
+
const persistedPlan = hasPersistedPlanId(input.response.planId);
|
|
407
|
+
if (input.jsonMode) {
|
|
408
|
+
emitPlanJson({
|
|
409
|
+
success: persistedPlan,
|
|
410
|
+
cached: true,
|
|
411
|
+
mode: input.intentMode,
|
|
412
|
+
planId: persistedPlan ? input.response.planId : null,
|
|
413
|
+
sessionId: input.response.sessionId || null,
|
|
414
|
+
projectId: input.projectId || null,
|
|
415
|
+
timestamp: input.response.timestamp,
|
|
416
|
+
telemetry: input.response.telemetry,
|
|
417
|
+
message: persistedPlan
|
|
418
|
+
? `Using ${input.mode === 'near' ? 'near-' : ''}cached plan`
|
|
419
|
+
: 'Plan generated but could not be persisted (missing planId)',
|
|
420
|
+
plan: input.response.plan,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
displayPlan(input.response.plan);
|
|
425
|
+
console.log(chalk.dim(`\nGenerated at: ${new Date(input.response.timestamp).toLocaleString()} (cached)`));
|
|
426
|
+
if (persistedPlan) {
|
|
427
|
+
console.log(chalk.bold.cyan(`\n📌 Plan ID: ${input.response.planId} (Cached)`));
|
|
428
|
+
console.log(chalk.dim(' Run \'neurcode prompt\' to generate a Cursor/AI prompt.'));
|
|
429
|
+
}
|
|
291
430
|
}
|
|
292
431
|
try {
|
|
293
|
-
if (
|
|
432
|
+
if (persistedPlan) {
|
|
294
433
|
(0, state_1.setActivePlanId)(input.response.planId);
|
|
295
434
|
(0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
|
|
296
435
|
}
|
|
@@ -311,15 +450,47 @@ function emitCachedPlanHit(input) {
|
|
|
311
450
|
catch {
|
|
312
451
|
// ignore state write errors
|
|
313
452
|
}
|
|
453
|
+
return persistedPlan;
|
|
314
454
|
}
|
|
315
455
|
async function planCommand(intent, options) {
|
|
316
456
|
try {
|
|
317
457
|
if (!intent || !intent.trim()) {
|
|
458
|
+
if (options.json) {
|
|
459
|
+
emitPlanJson({
|
|
460
|
+
success: false,
|
|
461
|
+
cached: false,
|
|
462
|
+
mode: 'implementation',
|
|
463
|
+
planId: null,
|
|
464
|
+
sessionId: null,
|
|
465
|
+
projectId: options.projectId || null,
|
|
466
|
+
timestamp: new Date().toISOString(),
|
|
467
|
+
message: 'Intent cannot be empty',
|
|
468
|
+
});
|
|
469
|
+
}
|
|
318
470
|
console.error(chalk.red('❌ Error: Intent cannot be empty. What are you building?'));
|
|
319
471
|
console.log(chalk.dim('Usage: neurcode plan "<your intent description>"'));
|
|
320
472
|
console.log(chalk.dim('Example: neurcode plan "Add user authentication to login page"'));
|
|
321
473
|
process.exit(1);
|
|
322
474
|
}
|
|
475
|
+
if (options.snapshotMode) {
|
|
476
|
+
const parsedSnapshotMode = parseSnapshotMode(String(options.snapshotMode));
|
|
477
|
+
if (!parsedSnapshotMode) {
|
|
478
|
+
if (options.json) {
|
|
479
|
+
emitPlanJson({
|
|
480
|
+
success: false,
|
|
481
|
+
cached: false,
|
|
482
|
+
mode: 'implementation',
|
|
483
|
+
planId: null,
|
|
484
|
+
sessionId: null,
|
|
485
|
+
projectId: options.projectId || null,
|
|
486
|
+
timestamp: new Date().toISOString(),
|
|
487
|
+
message: `Invalid --snapshot-mode "${options.snapshotMode}". Expected: auto | full | off.`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
console.error(chalk.red(`❌ Invalid --snapshot-mode "${options.snapshotMode}". Expected: auto | full | off.`));
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
323
494
|
// Load configuration first (needed for TicketService)
|
|
324
495
|
const config = (0, config_1.loadConfig)();
|
|
325
496
|
// API URL is automatically set to production - no need to check
|
|
@@ -376,14 +547,18 @@ async function planCommand(intent, options) {
|
|
|
376
547
|
});
|
|
377
548
|
const cached = (0, plan_cache_1.readCachedPlan)(cwd, key);
|
|
378
549
|
if (cached) {
|
|
379
|
-
emitCachedPlanHit({
|
|
550
|
+
const persisted = emitCachedPlanHit({
|
|
380
551
|
cwd,
|
|
381
552
|
response: cached.response,
|
|
382
553
|
createdAt: cached.createdAt,
|
|
383
554
|
mode: 'exact',
|
|
384
555
|
orgId,
|
|
385
556
|
projectId: finalProjectIdEarly,
|
|
557
|
+
jsonMode: options.json === true,
|
|
558
|
+
intentMode,
|
|
386
559
|
});
|
|
560
|
+
if (!persisted)
|
|
561
|
+
process.exit(2);
|
|
387
562
|
return;
|
|
388
563
|
}
|
|
389
564
|
const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
|
|
@@ -398,7 +573,7 @@ async function planCommand(intent, options) {
|
|
|
398
573
|
minIntentSimilarity: nearIntentSimilarityFloor,
|
|
399
574
|
});
|
|
400
575
|
if (near) {
|
|
401
|
-
emitCachedPlanHit({
|
|
576
|
+
const persisted = emitCachedPlanHit({
|
|
402
577
|
cwd,
|
|
403
578
|
response: near.entry.response,
|
|
404
579
|
createdAt: near.entry.createdAt,
|
|
@@ -407,7 +582,11 @@ async function planCommand(intent, options) {
|
|
|
407
582
|
orgId,
|
|
408
583
|
projectId: finalProjectIdEarly,
|
|
409
584
|
touchKey: near.entry.key,
|
|
585
|
+
jsonMode: options.json === true,
|
|
586
|
+
intentMode,
|
|
410
587
|
});
|
|
588
|
+
if (!persisted)
|
|
589
|
+
process.exit(2);
|
|
411
590
|
return;
|
|
412
591
|
}
|
|
413
592
|
const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
|
|
@@ -586,14 +765,18 @@ async function planCommand(intent, options) {
|
|
|
586
765
|
});
|
|
587
766
|
const cached = (0, plan_cache_1.readCachedPlan)(cwd, key);
|
|
588
767
|
if (cached) {
|
|
589
|
-
emitCachedPlanHit({
|
|
768
|
+
const persisted = emitCachedPlanHit({
|
|
590
769
|
cwd,
|
|
591
770
|
response: cached.response,
|
|
592
771
|
createdAt: cached.createdAt,
|
|
593
772
|
mode: 'exact',
|
|
594
773
|
orgId,
|
|
595
774
|
projectId: finalProjectIdForGuard,
|
|
775
|
+
jsonMode: options.json === true,
|
|
776
|
+
intentMode,
|
|
596
777
|
});
|
|
778
|
+
if (!persisted)
|
|
779
|
+
process.exit(2);
|
|
597
780
|
return;
|
|
598
781
|
}
|
|
599
782
|
const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
|
|
@@ -608,7 +791,7 @@ async function planCommand(intent, options) {
|
|
|
608
791
|
minIntentSimilarity: nearIntentSimilarityFloor,
|
|
609
792
|
});
|
|
610
793
|
if (near) {
|
|
611
|
-
emitCachedPlanHit({
|
|
794
|
+
const persisted = emitCachedPlanHit({
|
|
612
795
|
cwd,
|
|
613
796
|
response: near.entry.response,
|
|
614
797
|
createdAt: near.entry.createdAt,
|
|
@@ -617,7 +800,11 @@ async function planCommand(intent, options) {
|
|
|
617
800
|
orgId,
|
|
618
801
|
projectId: finalProjectIdForGuard,
|
|
619
802
|
touchKey: near.entry.key,
|
|
803
|
+
jsonMode: options.json === true,
|
|
804
|
+
intentMode,
|
|
620
805
|
});
|
|
806
|
+
if (!persisted)
|
|
807
|
+
process.exit(2);
|
|
621
808
|
return;
|
|
622
809
|
}
|
|
623
810
|
const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
|
|
@@ -876,8 +1063,9 @@ async function planCommand(intent, options) {
|
|
|
876
1063
|
// Brain progression tracking should never block plan generation.
|
|
877
1064
|
}
|
|
878
1065
|
}
|
|
1066
|
+
const persistedPlanId = hasPersistedPlanId(response.planId);
|
|
879
1067
|
// Persist in local cache for instant repeat plans.
|
|
880
|
-
if (shouldUseCache && orgId && finalProjectIdForGuard) {
|
|
1068
|
+
if (persistedPlanId && shouldUseCache && orgId && finalProjectIdForGuard) {
|
|
881
1069
|
// Recompute repo fingerprint right before writing to cache so Neurcode-managed
|
|
882
1070
|
// housekeeping (e.g. `.neurcode/config.json`, `.gitignore` updates) doesn't
|
|
883
1071
|
// cause immediate cache misses on the next identical run.
|
|
@@ -918,60 +1106,301 @@ async function planCommand(intent, options) {
|
|
|
918
1106
|
if (orgId && finalProjectIdForGuard) {
|
|
919
1107
|
(0, neurcode_context_1.appendPlanToOrgProjectMemory)(cwd, orgId, finalProjectIdForGuard, intent, response);
|
|
920
1108
|
}
|
|
921
|
-
// Pre-
|
|
922
|
-
|
|
923
|
-
const
|
|
924
|
-
const
|
|
1109
|
+
// Pre-flight snapshots: capture current state for MODIFY targets.
|
|
1110
|
+
const modifyFiles = response.plan.files.filter((f) => f.action === 'MODIFY');
|
|
1111
|
+
const snapshotMode = resolveSnapshotMode(options.snapshotMode);
|
|
1112
|
+
const snapshotMaxFiles = resolveSnapshotMaxFiles(snapshotMode, options.snapshotMaxFiles);
|
|
1113
|
+
const snapshotBudgetMs = resolveSnapshotBudgetMs(snapshotMode, options.snapshotBudgetMs);
|
|
1114
|
+
const snapshotMaxBytes = resolveSnapshotMaxBytes(snapshotMode);
|
|
1115
|
+
let snapshotSummary;
|
|
925
1116
|
if (isReadOnlyAnalysis) {
|
|
926
1117
|
console.log(chalk.dim('\n🔎 Analysis mode: skipping pre-flight file snapshots'));
|
|
1118
|
+
snapshotSummary = {
|
|
1119
|
+
mode: snapshotMode,
|
|
1120
|
+
attempted: 0,
|
|
1121
|
+
processed: 0,
|
|
1122
|
+
saved: 0,
|
|
1123
|
+
failed: 0,
|
|
1124
|
+
skippedUnchanged: 0,
|
|
1125
|
+
skippedMissing: 0,
|
|
1126
|
+
skippedLarge: 0,
|
|
1127
|
+
skippedBudget: 0,
|
|
1128
|
+
capped: 0,
|
|
1129
|
+
usedBatchApi: false,
|
|
1130
|
+
batchFallbackToSingle: false,
|
|
1131
|
+
durationMs: 0,
|
|
1132
|
+
};
|
|
927
1133
|
}
|
|
928
|
-
else if (
|
|
929
|
-
console.log(chalk.dim('\n⚡ Snapshot capture
|
|
1134
|
+
else if (snapshotMode === 'off') {
|
|
1135
|
+
console.log(chalk.dim('\n⚡ Snapshot capture disabled (snapshot mode: off)'));
|
|
1136
|
+
snapshotSummary = {
|
|
1137
|
+
mode: snapshotMode,
|
|
1138
|
+
attempted: 0,
|
|
1139
|
+
processed: 0,
|
|
1140
|
+
saved: 0,
|
|
1141
|
+
failed: 0,
|
|
1142
|
+
skippedUnchanged: 0,
|
|
1143
|
+
skippedMissing: 0,
|
|
1144
|
+
skippedLarge: 0,
|
|
1145
|
+
skippedBudget: 0,
|
|
1146
|
+
capped: 0,
|
|
1147
|
+
usedBatchApi: false,
|
|
1148
|
+
batchFallbackToSingle: false,
|
|
1149
|
+
durationMs: 0,
|
|
1150
|
+
};
|
|
930
1151
|
}
|
|
931
1152
|
else if (modifyFiles.length > 0) {
|
|
932
|
-
|
|
1153
|
+
const snapshotStartedAt = Date.now();
|
|
1154
|
+
const snapshotCandidates = modifyFiles.slice(0, snapshotMaxFiles);
|
|
1155
|
+
const cappedCount = Math.max(0, modifyFiles.length - snapshotCandidates.length);
|
|
1156
|
+
const snapshotTimeoutMs = Math.max(1000, parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_TIMEOUT_MS) ?? (snapshotMode === 'full' ? 15000 : 8000));
|
|
1157
|
+
const snapshotConcurrency = Math.min(16, Math.max(1, parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_CONCURRENCY) ?? (snapshotMode === 'full' ? 8 : 6)));
|
|
1158
|
+
const snapshotBatchSize = resolveSnapshotBatchSize(snapshotMode);
|
|
1159
|
+
const snapshotBatchTimeoutMs = resolveSnapshotBatchTimeoutMs(snapshotTimeoutMs, snapshotMode);
|
|
1160
|
+
const deadline = snapshotBudgetMs > 0 ? Date.now() + snapshotBudgetMs : 0;
|
|
1161
|
+
const forceResnapshot = process.env.NEURCODE_PLAN_SNAPSHOT_FORCE === '1';
|
|
1162
|
+
console.log(chalk.dim(`\n📸 Capturing pre-flight snapshots for ${snapshotCandidates.length}/${modifyFiles.length} file(s)` +
|
|
1163
|
+
` [mode=${snapshotMode}, batch=${snapshotBatchSize}, fallback-concurrency=${snapshotConcurrency}` +
|
|
1164
|
+
`${snapshotBudgetMs > 0 ? `, budget=${snapshotBudgetMs}ms` : ', budget=unbounded'}]...`));
|
|
1165
|
+
const withTimeout = async (promise, timeoutMs) => {
|
|
1166
|
+
return await Promise.race([
|
|
1167
|
+
promise,
|
|
1168
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`snapshot timeout after ${timeoutMs}ms`)), timeoutMs)),
|
|
1169
|
+
]);
|
|
1170
|
+
};
|
|
1171
|
+
const manifest = loadSnapshotManifest(cwd);
|
|
1172
|
+
let manifestDirty = false;
|
|
1173
|
+
const preparedSnapshots = [];
|
|
1174
|
+
let snapshotsProcessed = 0;
|
|
933
1175
|
let snapshotsSaved = 0;
|
|
934
1176
|
let snapshotsFailed = 0;
|
|
935
|
-
|
|
1177
|
+
let snapshotsSkippedMissing = 0;
|
|
1178
|
+
let snapshotsSkippedUnchanged = 0;
|
|
1179
|
+
let snapshotsSkippedLarge = 0;
|
|
1180
|
+
let snapshotsSkippedBudget = 0;
|
|
1181
|
+
let usedBatchApi = false;
|
|
1182
|
+
let batchFallbackToSingle = false;
|
|
1183
|
+
for (const current of snapshotCandidates) {
|
|
1184
|
+
if (deadline > 0 && Date.now() >= deadline) {
|
|
1185
|
+
snapshotsSkippedBudget = snapshotCandidates.length - snapshotsProcessed;
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
snapshotsProcessed++;
|
|
936
1189
|
try {
|
|
937
|
-
|
|
938
|
-
const filePath = (0, path_1.resolve)(cwd,
|
|
939
|
-
// Check if file exists locally
|
|
1190
|
+
const normalizedPath = toUnixPath(current.path);
|
|
1191
|
+
const filePath = (0, path_1.resolve)(cwd, current.path);
|
|
940
1192
|
if (!(0, fs_1.existsSync)(filePath)) {
|
|
941
|
-
|
|
1193
|
+
snapshotsSkippedMissing++;
|
|
1194
|
+
if (process.env.DEBUG) {
|
|
1195
|
+
console.log(chalk.yellow(` ⚠️ Skipping ${current.path} (file not found locally)`));
|
|
1196
|
+
}
|
|
942
1197
|
continue;
|
|
943
1198
|
}
|
|
944
|
-
// Read current file content
|
|
945
1199
|
const fileContent = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1200
|
+
const fileSize = Buffer.byteLength(fileContent, 'utf-8');
|
|
1201
|
+
if (snapshotMaxBytes > 0 && fileSize > snapshotMaxBytes) {
|
|
1202
|
+
snapshotsSkippedLarge++;
|
|
1203
|
+
if (process.env.DEBUG) {
|
|
1204
|
+
console.log(chalk.yellow(` ⚠️ Skipping ${current.path} (size ${fileSize}B > ${snapshotMaxBytes}B)`));
|
|
1205
|
+
}
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const fileHash = computeSnapshotHash(fileContent);
|
|
1209
|
+
const previous = manifest.entries[normalizedPath];
|
|
1210
|
+
if (snapshotMode === 'auto' &&
|
|
1211
|
+
!forceResnapshot &&
|
|
1212
|
+
previous &&
|
|
1213
|
+
previous.sha256 === fileHash &&
|
|
1214
|
+
previous.size === fileSize) {
|
|
1215
|
+
snapshotsSkippedUnchanged++;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
preparedSnapshots.push({
|
|
1219
|
+
path: current.path,
|
|
1220
|
+
normalizedPath,
|
|
1221
|
+
content: fileContent,
|
|
1222
|
+
size: fileSize,
|
|
1223
|
+
sha256: fileHash,
|
|
1224
|
+
});
|
|
954
1225
|
}
|
|
955
1226
|
catch (error) {
|
|
956
1227
|
snapshotsFailed++;
|
|
957
|
-
|
|
958
|
-
|
|
1228
|
+
if (process.env.DEBUG) {
|
|
1229
|
+
console.warn(chalk.yellow(` ⚠️ Failed to prepare snapshot for ${current.path}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const reason = `Pre-Plan Snapshot for "${intent.trim()}"`;
|
|
1234
|
+
const markSnapshotSaved = (snapshot, snapshotAt) => {
|
|
1235
|
+
snapshotsSaved++;
|
|
1236
|
+
manifest.entries[snapshot.normalizedPath] = {
|
|
1237
|
+
sha256: snapshot.sha256,
|
|
1238
|
+
size: snapshot.size,
|
|
1239
|
+
snapshotAt: snapshotAt || new Date().toISOString(),
|
|
1240
|
+
};
|
|
1241
|
+
manifestDirty = true;
|
|
1242
|
+
};
|
|
1243
|
+
let pendingFallbackSnapshots = [];
|
|
1244
|
+
if (preparedSnapshots.length > 0) {
|
|
1245
|
+
try {
|
|
1246
|
+
usedBatchApi = true;
|
|
1247
|
+
for (let idx = 0; idx < preparedSnapshots.length; idx += snapshotBatchSize) {
|
|
1248
|
+
if (deadline > 0 && Date.now() >= deadline) {
|
|
1249
|
+
const remaining = preparedSnapshots.length - idx;
|
|
1250
|
+
snapshotsSkippedBudget += Math.max(0, remaining);
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
const chunk = preparedSnapshots.slice(idx, idx + snapshotBatchSize);
|
|
1254
|
+
const result = await withTimeout(client.saveFileVersionsBatch(chunk.map((snapshot) => ({
|
|
1255
|
+
filePath: snapshot.path,
|
|
1256
|
+
fileContent: snapshot.content,
|
|
1257
|
+
changeType: 'modify',
|
|
1258
|
+
linesAdded: 0,
|
|
1259
|
+
linesRemoved: 0,
|
|
1260
|
+
})), finalProjectId, reason), snapshotBatchTimeoutMs);
|
|
1261
|
+
const chunkByPath = new Map();
|
|
1262
|
+
for (const snapshot of chunk) {
|
|
1263
|
+
chunkByPath.set(snapshot.path, snapshot);
|
|
1264
|
+
}
|
|
1265
|
+
for (const saved of result.saved || []) {
|
|
1266
|
+
const prepared = chunkByPath.get(saved.filePath);
|
|
1267
|
+
if (!prepared)
|
|
1268
|
+
continue;
|
|
1269
|
+
markSnapshotSaved(prepared, saved.version?.createdAt);
|
|
1270
|
+
chunkByPath.delete(saved.filePath);
|
|
1271
|
+
}
|
|
1272
|
+
for (const failed of result.failed || []) {
|
|
1273
|
+
if (chunkByPath.has(failed.filePath)) {
|
|
1274
|
+
chunkByPath.delete(failed.filePath);
|
|
1275
|
+
}
|
|
1276
|
+
snapshotsFailed++;
|
|
1277
|
+
if (process.env.DEBUG) {
|
|
1278
|
+
console.warn(chalk.yellow(` ⚠️ Failed to save snapshot for ${failed.filePath}: ${failed.error}`));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
// Any residual paths were not explicitly reported by API; preserve via fallback.
|
|
1282
|
+
if (chunkByPath.size > 0) {
|
|
1283
|
+
pendingFallbackSnapshots.push(...chunkByPath.values());
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
// Backward compatibility: old API versions won't have save-batch endpoint.
|
|
1289
|
+
if (isBatchSnapshotEndpointUnsupported(error)) {
|
|
1290
|
+
batchFallbackToSingle = true;
|
|
1291
|
+
pendingFallbackSnapshots = [...preparedSnapshots];
|
|
1292
|
+
if (process.env.DEBUG || !options.json) {
|
|
1293
|
+
console.log(chalk.dim(' Batch snapshot endpoint unavailable; falling back to single-file snapshot uploads.'));
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
snapshotsFailed += preparedSnapshots.length;
|
|
1298
|
+
if (process.env.DEBUG) {
|
|
1299
|
+
console.warn(chalk.yellow(` ⚠️ Batch snapshot upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (pendingFallbackSnapshots.length > 0) {
|
|
1305
|
+
let fallbackCursor = 0;
|
|
1306
|
+
const fallbackWorkers = Array.from({ length: Math.min(snapshotConcurrency, pendingFallbackSnapshots.length) }, async () => {
|
|
1307
|
+
while (true) {
|
|
1308
|
+
const index = fallbackCursor++;
|
|
1309
|
+
if (index >= pendingFallbackSnapshots.length) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const snapshot = pendingFallbackSnapshots[index];
|
|
1313
|
+
if (deadline > 0 && Date.now() >= deadline) {
|
|
1314
|
+
snapshotsSkippedBudget++;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
try {
|
|
1318
|
+
await withTimeout(client.saveFileVersion(snapshot.path, snapshot.content, finalProjectId, reason, 'modify', 0, 0), snapshotTimeoutMs);
|
|
1319
|
+
markSnapshotSaved(snapshot);
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
snapshotsFailed++;
|
|
1323
|
+
if (process.env.DEBUG) {
|
|
1324
|
+
console.warn(chalk.yellow(` ⚠️ Failed to save snapshot for ${snapshot.path}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
await Promise.all(fallbackWorkers);
|
|
1330
|
+
}
|
|
1331
|
+
if (manifestDirty) {
|
|
1332
|
+
try {
|
|
1333
|
+
manifest.updatedAt = new Date().toISOString();
|
|
1334
|
+
saveSnapshotManifest(cwd, manifest);
|
|
1335
|
+
}
|
|
1336
|
+
catch (manifestError) {
|
|
1337
|
+
if (process.env.DEBUG) {
|
|
1338
|
+
console.warn(chalk.yellow(` ⚠️ Could not persist snapshot manifest: ${manifestError instanceof Error ? manifestError.message : 'Unknown error'}`));
|
|
1339
|
+
}
|
|
959
1340
|
}
|
|
960
1341
|
}
|
|
961
1342
|
if (snapshotsSaved > 0) {
|
|
962
1343
|
console.log(chalk.green(`\n✅ ${snapshotsSaved} pre-flight snapshot(s) saved successfully`));
|
|
963
|
-
|
|
964
|
-
console.log(chalk.yellow(` ${snapshotsFailed} snapshot(s) failed (plan will continue)`));
|
|
965
|
-
}
|
|
966
|
-
console.log(chalk.dim(' You can revert these files using: neurcode revert <filePath> --to-version <version>\n'));
|
|
1344
|
+
console.log(chalk.dim(' You can revert these files using: neurcode revert <filePath> --to-version <version>'));
|
|
967
1345
|
}
|
|
968
1346
|
else if (snapshotsFailed > 0) {
|
|
969
1347
|
console.log(chalk.yellow(`\n⚠️ No snapshots were saved (${snapshotsFailed} failed)`));
|
|
970
|
-
console.log(chalk.dim(' Plan will continue, but revert functionality may be limited
|
|
1348
|
+
console.log(chalk.dim(' Plan will continue, but revert functionality may be limited'));
|
|
971
1349
|
}
|
|
972
1350
|
else {
|
|
973
|
-
console.log(chalk.dim('\n No
|
|
1351
|
+
console.log(chalk.dim('\n No new snapshots were required'));
|
|
1352
|
+
}
|
|
1353
|
+
if (snapshotsSkippedUnchanged > 0) {
|
|
1354
|
+
console.log(chalk.dim(` ${snapshotsSkippedUnchanged} file(s) skipped (unchanged since last snapshot)`));
|
|
1355
|
+
}
|
|
1356
|
+
if (snapshotsSkippedMissing > 0) {
|
|
1357
|
+
console.log(chalk.dim(` ${snapshotsSkippedMissing} file(s) skipped (not found locally)`));
|
|
1358
|
+
}
|
|
1359
|
+
if (snapshotsSkippedLarge > 0) {
|
|
1360
|
+
console.log(chalk.dim(` ${snapshotsSkippedLarge} file(s) skipped (size limit)`));
|
|
1361
|
+
}
|
|
1362
|
+
if (cappedCount > 0) {
|
|
1363
|
+
console.log(chalk.dim(` ${cappedCount} file(s) deferred by snapshot max-file cap (${snapshotMaxFiles})`));
|
|
974
1364
|
}
|
|
1365
|
+
if (snapshotsSkippedBudget > 0) {
|
|
1366
|
+
console.log(chalk.dim(` ${snapshotsSkippedBudget} file(s) deferred by snapshot budget`));
|
|
1367
|
+
}
|
|
1368
|
+
if (usedBatchApi) {
|
|
1369
|
+
console.log(chalk.dim(` Snapshot transport: batch${batchFallbackToSingle ? ' (with single-file fallback)' : ''}`));
|
|
1370
|
+
}
|
|
1371
|
+
console.log('');
|
|
1372
|
+
snapshotSummary = {
|
|
1373
|
+
mode: snapshotMode,
|
|
1374
|
+
attempted: snapshotCandidates.length,
|
|
1375
|
+
processed: snapshotsProcessed,
|
|
1376
|
+
saved: snapshotsSaved,
|
|
1377
|
+
failed: snapshotsFailed,
|
|
1378
|
+
skippedUnchanged: snapshotsSkippedUnchanged,
|
|
1379
|
+
skippedMissing: snapshotsSkippedMissing,
|
|
1380
|
+
skippedLarge: snapshotsSkippedLarge,
|
|
1381
|
+
skippedBudget: snapshotsSkippedBudget,
|
|
1382
|
+
capped: cappedCount,
|
|
1383
|
+
usedBatchApi,
|
|
1384
|
+
batchFallbackToSingle,
|
|
1385
|
+
durationMs: Date.now() - snapshotStartedAt,
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
snapshotSummary = {
|
|
1390
|
+
mode: snapshotMode,
|
|
1391
|
+
attempted: 0,
|
|
1392
|
+
processed: 0,
|
|
1393
|
+
saved: 0,
|
|
1394
|
+
failed: 0,
|
|
1395
|
+
skippedUnchanged: 0,
|
|
1396
|
+
skippedMissing: 0,
|
|
1397
|
+
skippedLarge: 0,
|
|
1398
|
+
skippedBudget: 0,
|
|
1399
|
+
capped: 0,
|
|
1400
|
+
usedBatchApi: false,
|
|
1401
|
+
batchFallbackToSingle: false,
|
|
1402
|
+
durationMs: 0,
|
|
1403
|
+
};
|
|
975
1404
|
}
|
|
976
1405
|
// Step 3: Post-Generation Hallucination Check (DEEP SCAN)
|
|
977
1406
|
// Scan ALL plan content for phantom packages - not just summaries, but full proposed code
|
|
@@ -1105,6 +1534,26 @@ async function planCommand(intent, options) {
|
|
|
1105
1534
|
console.log(chalk.green('✅ No hallucinations detected'));
|
|
1106
1535
|
}
|
|
1107
1536
|
}
|
|
1537
|
+
if (!persistedPlanId) {
|
|
1538
|
+
const missingPlanMessage = 'Plan generated but failed to persist (planId missing). Cannot continue with prompt/apply/ship.';
|
|
1539
|
+
if (options.json) {
|
|
1540
|
+
emitPlanJson({
|
|
1541
|
+
success: false,
|
|
1542
|
+
cached: false,
|
|
1543
|
+
mode: intentMode,
|
|
1544
|
+
planId: null,
|
|
1545
|
+
sessionId: response.sessionId || null,
|
|
1546
|
+
projectId: finalProjectId || null,
|
|
1547
|
+
timestamp: response.timestamp,
|
|
1548
|
+
telemetry: response.telemetry,
|
|
1549
|
+
snapshot: snapshotSummary,
|
|
1550
|
+
message: missingPlanMessage,
|
|
1551
|
+
plan: response.plan,
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
console.error(chalk.red(`\n❌ ${missingPlanMessage}`));
|
|
1555
|
+
process.exit(2);
|
|
1556
|
+
}
|
|
1108
1557
|
// Display the plan (AFTER hallucination warnings)
|
|
1109
1558
|
if (isReadOnlyAnalysis) {
|
|
1110
1559
|
const writableTargets = response.plan.files.filter((file) => file.action !== 'BLOCK').length;
|
|
@@ -1113,13 +1562,13 @@ async function planCommand(intent, options) {
|
|
|
1113
1562
|
displayPlan(response.plan);
|
|
1114
1563
|
console.log(chalk.dim(`\nGenerated at: ${new Date(response.timestamp).toLocaleString()}`));
|
|
1115
1564
|
// Display plan ID if available
|
|
1116
|
-
if (
|
|
1565
|
+
if (persistedPlanId) {
|
|
1117
1566
|
console.log(chalk.bold.cyan(`\n📌 Plan ID: ${response.planId} (Saved)`));
|
|
1118
1567
|
console.log(chalk.dim(' Run \'neurcode prompt \' to generate a Cursor/AI prompt. (Ready now)'));
|
|
1119
1568
|
}
|
|
1120
1569
|
// Save sessionId and planId to state file (.neurcode/config.json)
|
|
1121
1570
|
try {
|
|
1122
|
-
if (
|
|
1571
|
+
if (persistedPlanId) {
|
|
1123
1572
|
// Save active plan ID (primary) and lastPlanId (backward compatibility)
|
|
1124
1573
|
(0, state_1.setActivePlanId)(response.planId);
|
|
1125
1574
|
(0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
|
|
@@ -1135,8 +1584,35 @@ async function planCommand(intent, options) {
|
|
|
1135
1584
|
console.warn(chalk.yellow(`⚠️ Could not save sessionId/planId to state: ${stateError instanceof Error ? stateError.message : 'Unknown error'}`));
|
|
1136
1585
|
}
|
|
1137
1586
|
}
|
|
1587
|
+
if (options.json) {
|
|
1588
|
+
emitPlanJson({
|
|
1589
|
+
success: true,
|
|
1590
|
+
cached: false,
|
|
1591
|
+
mode: intentMode,
|
|
1592
|
+
planId: response.planId,
|
|
1593
|
+
sessionId: response.sessionId || null,
|
|
1594
|
+
projectId: finalProjectId || null,
|
|
1595
|
+
timestamp: response.timestamp,
|
|
1596
|
+
telemetry: response.telemetry,
|
|
1597
|
+
snapshot: snapshotSummary,
|
|
1598
|
+
message: 'Plan generated and persisted',
|
|
1599
|
+
plan: response.plan,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1138
1602
|
}
|
|
1139
1603
|
catch (error) {
|
|
1604
|
+
if (options.json) {
|
|
1605
|
+
emitPlanJson({
|
|
1606
|
+
success: false,
|
|
1607
|
+
cached: false,
|
|
1608
|
+
mode: 'implementation',
|
|
1609
|
+
planId: null,
|
|
1610
|
+
sessionId: null,
|
|
1611
|
+
projectId: options.projectId || null,
|
|
1612
|
+
timestamp: new Date().toISOString(),
|
|
1613
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1140
1616
|
console.error(chalk.red('\n❌ Error generating plan:'));
|
|
1141
1617
|
if (error instanceof Error) {
|
|
1142
1618
|
console.error(chalk.red(error.message));
|