@neurcode-ai/cli 0.9.28 → 0.9.30

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 (37) hide show
  1. package/LICENSE +201 -0
  2. package/dist/api-client.d.ts +78 -0
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +49 -0
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/commands/apply.d.ts.map +1 -1
  7. package/dist/commands/apply.js +33 -0
  8. package/dist/commands/apply.js.map +1 -1
  9. package/dist/commands/approve.d.ts +11 -0
  10. package/dist/commands/approve.d.ts.map +1 -0
  11. package/dist/commands/approve.js +116 -0
  12. package/dist/commands/approve.js.map +1 -0
  13. package/dist/commands/plan.d.ts.map +1 -1
  14. package/dist/commands/plan.js +244 -64
  15. package/dist/commands/plan.js.map +1 -1
  16. package/dist/commands/prompt.d.ts.map +1 -1
  17. package/dist/commands/prompt.js +71 -4
  18. package/dist/commands/prompt.js.map +1 -1
  19. package/dist/commands/ship.d.ts +2 -0
  20. package/dist/commands/ship.d.ts.map +1 -1
  21. package/dist/commands/ship.js +228 -1
  22. package/dist/commands/ship.js.map +1 -1
  23. package/dist/commands/verify.d.ts +2 -0
  24. package/dist/commands/verify.d.ts.map +1 -1
  25. package/dist/commands/verify.js +253 -18
  26. package/dist/commands/verify.js.map +1 -1
  27. package/dist/index.js +26 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/utils/governance.d.ts +33 -0
  30. package/dist/utils/governance.d.ts.map +1 -0
  31. package/dist/utils/governance.js +68 -0
  32. package/dist/utils/governance.js.map +1 -0
  33. package/dist/utils/manual-approvals.d.ts +20 -0
  34. package/dist/utils/manual-approvals.d.ts.map +1 -0
  35. package/dist/utils/manual-approvals.js +104 -0
  36. package/dist/utils/manual-approvals.js.map +1 -0
  37. package/package.json +12 -9
@@ -319,6 +319,29 @@ function resolveSnapshotMaxBytes(mode) {
319
319
  return envOverride;
320
320
  return mode === 'full' ? 0 : 256 * 1024;
321
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
+ }
322
345
  function getSnapshotManifestPath(projectRoot) {
323
346
  return (0, path_1.join)(projectRoot, '.neurcode', 'snapshot-manifest.json');
324
347
  }
@@ -390,6 +413,7 @@ function emitCachedPlanHit(input) {
390
413
  sessionId: input.response.sessionId || null,
391
414
  projectId: input.projectId || null,
392
415
  timestamp: input.response.timestamp,
416
+ telemetry: input.response.telemetry,
393
417
  message: persistedPlan
394
418
  ? `Using ${input.mode === 'near' ? 'near-' : ''}cached plan`
395
419
  : 'Plan generated but could not be persisted (missing planId)',
@@ -1088,29 +1112,56 @@ async function planCommand(intent, options) {
1088
1112
  const snapshotMaxFiles = resolveSnapshotMaxFiles(snapshotMode, options.snapshotMaxFiles);
1089
1113
  const snapshotBudgetMs = resolveSnapshotBudgetMs(snapshotMode, options.snapshotBudgetMs);
1090
1114
  const snapshotMaxBytes = resolveSnapshotMaxBytes(snapshotMode);
1115
+ let snapshotSummary;
1091
1116
  if (isReadOnlyAnalysis) {
1092
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
+ };
1093
1133
  }
1094
1134
  else if (snapshotMode === 'off') {
1095
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
+ };
1096
1151
  }
1097
1152
  else if (modifyFiles.length > 0) {
1153
+ const snapshotStartedAt = Date.now();
1098
1154
  const snapshotCandidates = modifyFiles.slice(0, snapshotMaxFiles);
1099
1155
  const cappedCount = Math.max(0, modifyFiles.length - snapshotCandidates.length);
1100
- const snapshotConcurrency = Math.min(16, Math.max(1, parsePositiveInt(process.env.NEURCODE_PLAN_SNAPSHOT_CONCURRENCY) ?? (snapshotMode === 'full' ? 8 : 6)));
1101
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);
1102
1160
  const deadline = snapshotBudgetMs > 0 ? Date.now() + snapshotBudgetMs : 0;
1103
1161
  const forceResnapshot = process.env.NEURCODE_PLAN_SNAPSHOT_FORCE === '1';
1104
1162
  console.log(chalk.dim(`\n📸 Capturing pre-flight snapshots for ${snapshotCandidates.length}/${modifyFiles.length} file(s)` +
1105
- ` [mode=${snapshotMode}, concurrency=${snapshotConcurrency}` +
1163
+ ` [mode=${snapshotMode}, batch=${snapshotBatchSize}, fallback-concurrency=${snapshotConcurrency}` +
1106
1164
  `${snapshotBudgetMs > 0 ? `, budget=${snapshotBudgetMs}ms` : ', budget=unbounded'}]...`));
1107
- let snapshotsSaved = 0;
1108
- let snapshotsFailed = 0;
1109
- let snapshotsSkippedMissing = 0;
1110
- let snapshotsSkippedUnchanged = 0;
1111
- let snapshotsSkippedLarge = 0;
1112
- let snapshotsProcessed = 0;
1113
- let budgetExhausted = false;
1114
1165
  const withTimeout = async (promise, timeoutMs) => {
1115
1166
  return await Promise.race([
1116
1167
  promise,
@@ -1119,70 +1170,164 @@ async function planCommand(intent, options) {
1119
1170
  };
1120
1171
  const manifest = loadSnapshotManifest(cwd);
1121
1172
  let manifestDirty = false;
1122
- let cursor = 0;
1123
- const workers = Array.from({ length: Math.min(snapshotConcurrency, snapshotCandidates.length) }, async () => {
1124
- while (cursor < snapshotCandidates.length) {
1125
- if (budgetExhausted)
1126
- break;
1127
- if (deadline > 0 && Date.now() >= deadline) {
1128
- budgetExhausted = true;
1129
- break;
1173
+ const preparedSnapshots = [];
1174
+ let snapshotsProcessed = 0;
1175
+ let snapshotsSaved = 0;
1176
+ let snapshotsFailed = 0;
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++;
1189
+ try {
1190
+ const normalizedPath = toUnixPath(current.path);
1191
+ const filePath = (0, path_1.resolve)(cwd, current.path);
1192
+ if (!(0, fs_1.existsSync)(filePath)) {
1193
+ snapshotsSkippedMissing++;
1194
+ if (process.env.DEBUG) {
1195
+ console.log(chalk.yellow(` ⚠️ Skipping ${current.path} (file not found locally)`));
1196
+ }
1197
+ continue;
1130
1198
  }
1131
- const current = snapshotCandidates[cursor++];
1132
- if (!current)
1133
- break;
1134
- snapshotsProcessed++;
1135
- try {
1136
- const normalizedPath = toUnixPath(current.path);
1137
- const filePath = (0, path_1.resolve)(cwd, current.path);
1138
- if (!(0, fs_1.existsSync)(filePath)) {
1139
- snapshotsSkippedMissing++;
1140
- if (process.env.DEBUG) {
1141
- console.log(chalk.yellow(` ⚠️ Skipping ${current.path} (file not found locally)`));
1142
- }
1143
- continue;
1199
+ const fileContent = (0, fs_1.readFileSync)(filePath, 'utf-8');
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)`));
1144
1205
  }
1145
- const fileContent = (0, fs_1.readFileSync)(filePath, 'utf-8');
1146
- const fileSize = Buffer.byteLength(fileContent, 'utf-8');
1147
- if (snapshotMaxBytes > 0 && fileSize > snapshotMaxBytes) {
1148
- snapshotsSkippedLarge++;
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
+ });
1225
+ }
1226
+ catch (error) {
1227
+ snapshotsFailed++;
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++;
1149
1277
  if (process.env.DEBUG) {
1150
- console.log(chalk.yellow(` ⚠️ Skipping ${current.path} (size ${fileSize}B > ${snapshotMaxBytes}B)`));
1278
+ console.warn(chalk.yellow(` ⚠️ Failed to save snapshot for ${failed.filePath}: ${failed.error}`));
1151
1279
  }
1152
- continue;
1153
1280
  }
1154
- const fileHash = computeSnapshotHash(fileContent);
1155
- const previous = manifest.entries[normalizedPath];
1156
- if (snapshotMode === 'auto' &&
1157
- !forceResnapshot &&
1158
- previous &&
1159
- previous.sha256 === fileHash &&
1160
- previous.size === fileSize) {
1161
- snapshotsSkippedUnchanged++;
1162
- continue;
1281
+ // Any residual paths were not explicitly reported by API; preserve via fallback.
1282
+ if (chunkByPath.size > 0) {
1283
+ pendingFallbackSnapshots.push(...chunkByPath.values());
1163
1284
  }
1164
- const reason = `Pre-Plan Snapshot for "${intent.trim()}"`;
1165
- await withTimeout(client.saveFileVersion(current.path, fileContent, finalProjectId, reason, 'modify', 0, 0), snapshotTimeoutMs);
1166
- snapshotsSaved++;
1167
- manifest.entries[normalizedPath] = {
1168
- sha256: fileHash,
1169
- size: fileSize,
1170
- snapshotAt: new Date().toISOString(),
1171
- };
1172
- manifestDirty = true;
1173
- if (process.env.DEBUG) {
1174
- console.log(chalk.green(` ✓ Snapshot saved: ${current.path}`));
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.'));
1175
1294
  }
1176
1295
  }
1177
- catch (error) {
1178
- snapshotsFailed++;
1296
+ else {
1297
+ snapshotsFailed += preparedSnapshots.length;
1179
1298
  if (process.env.DEBUG) {
1180
- console.warn(chalk.yellow(` ⚠️ Failed to save snapshot for ${current.path}: ${error instanceof Error ? error.message : 'Unknown error'}`));
1299
+ console.warn(chalk.yellow(` ⚠️ Batch snapshot upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
1181
1300
  }
1182
1301
  }
1183
1302
  }
1184
- });
1185
- await Promise.all(workers);
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
+ }
1186
1331
  if (manifestDirty) {
1187
1332
  try {
1188
1333
  manifest.updatedAt = new Date().toISOString();
@@ -1194,7 +1339,6 @@ async function planCommand(intent, options) {
1194
1339
  }
1195
1340
  }
1196
1341
  }
1197
- const snapshotsSkippedBudget = Math.max(0, snapshotCandidates.length - snapshotsProcessed);
1198
1342
  if (snapshotsSaved > 0) {
1199
1343
  console.log(chalk.green(`\n✅ ${snapshotsSaved} pre-flight snapshot(s) saved successfully`));
1200
1344
  console.log(chalk.dim(' You can revert these files using: neurcode revert <filePath> --to-version <version>'));
@@ -1221,10 +1365,42 @@ async function planCommand(intent, options) {
1221
1365
  if (snapshotsSkippedBudget > 0) {
1222
1366
  console.log(chalk.dim(` ${snapshotsSkippedBudget} file(s) deferred by snapshot budget`));
1223
1367
  }
1224
- if (budgetExhausted && process.env.DEBUG) {
1225
- console.log(chalk.dim(' Snapshot budget exhausted before processing all candidates.'));
1368
+ if (usedBatchApi) {
1369
+ console.log(chalk.dim(` Snapshot transport: batch${batchFallbackToSingle ? ' (with single-file fallback)' : ''}`));
1226
1370
  }
1227
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
+ };
1228
1404
  }
1229
1405
  // Step 3: Post-Generation Hallucination Check (DEEP SCAN)
1230
1406
  // Scan ALL plan content for phantom packages - not just summaries, but full proposed code
@@ -1369,6 +1545,8 @@ async function planCommand(intent, options) {
1369
1545
  sessionId: response.sessionId || null,
1370
1546
  projectId: finalProjectId || null,
1371
1547
  timestamp: response.timestamp,
1548
+ telemetry: response.telemetry,
1549
+ snapshot: snapshotSummary,
1372
1550
  message: missingPlanMessage,
1373
1551
  plan: response.plan,
1374
1552
  });
@@ -1415,6 +1593,8 @@ async function planCommand(intent, options) {
1415
1593
  sessionId: response.sessionId || null,
1416
1594
  projectId: finalProjectId || null,
1417
1595
  timestamp: response.timestamp,
1596
+ telemetry: response.telemetry,
1597
+ snapshot: snapshotSummary,
1418
1598
  message: 'Plan generated and persisted',
1419
1599
  plan: response.plan,
1420
1600
  });