vibecodingmachine-cli 2025.12.1-534 → 2025.12.22-2230

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.
@@ -8,7 +8,8 @@ const readline = require('readline');
8
8
  const repo = require('../commands/repo');
9
9
  const auto = require('../commands/auto');
10
10
  const status = require('../commands/status');
11
- const { getRepoPath, readConfig, writeConfig } = require('./config');
11
+ const requirements = require('../commands/requirements');
12
+ const { getRepoPath, readConfig, writeConfig, getAutoConfig } = require('./config');
12
13
  const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
13
14
  const { checkAutoModeStatus } = require('./auto-mode');
14
15
  const {
@@ -18,6 +19,7 @@ const {
18
19
  requirementsExists,
19
20
  isComputerNameEnabled
20
21
  } = require('vibecodingmachine-core');
22
+ const pkg = require('../../package.json');
21
23
 
22
24
  /**
23
25
  * Format IDE name for display
@@ -32,7 +34,8 @@ function formatIDEName(ide) {
32
34
  'cline': 'Cline CLI',
33
35
  'cursor': 'Cursor',
34
36
  'vscode': 'VS Code',
35
- 'windsurf': 'Windsurf'
37
+ 'windsurf': 'Windsurf',
38
+ 'kiro': 'AWS Kiro'
36
39
  };
37
40
  return ideNames[ide] || ide;
38
41
  }
@@ -52,6 +55,7 @@ function getAgentDisplayName(agentType) {
52
55
  if (agentType === 'cursor') return 'Cursor IDE Agent';
53
56
  if (agentType === 'windsurf') return 'Windsurf IDE Agent';
54
57
  if (agentType === 'antigravity') return 'Google Antigravity IDE Agent';
58
+ if (agentType === 'kiro') return 'AWS Kiro AI IDE Agent';
55
59
  if (agentType === 'vscode') return 'VS Code IDE Agent';
56
60
 
57
61
  // Claude Code CLI
@@ -210,95 +214,9 @@ function formatPath(fullPath) {
210
214
 
211
215
  async function countRequirements() {
212
216
  try {
213
- const { getRequirementsPath, getVibeCodingMachineDir } = require('vibecodingmachine-core');
217
+ const { getProjectRequirementStats } = require('vibecodingmachine-core');
214
218
  const repoPath = await getRepoPath();
215
- const reqPath = await getRequirementsPath(repoPath);
216
-
217
- if (!reqPath || !await fs.pathExists(reqPath)) {
218
- return null;
219
- }
220
-
221
- const content = await fs.readFile(reqPath, 'utf8');
222
-
223
- // Count requirements in each section
224
- let todoCount = 0;
225
- let toVerifyCount = 0;
226
- let verifiedCount = 0;
227
-
228
- // Split by sections
229
- const lines = content.split('\n');
230
- let currentSection = '';
231
-
232
- for (const line of lines) {
233
- const trimmed = line.trim();
234
-
235
- // Check for requirement headers first (###), then section headers (##)
236
- // This prevents ### from being treated as section headers
237
- if (trimmed.startsWith('###')) {
238
- // Count requirements (### headers in new format)
239
- // IMPORTANT: Only count if we're in a recognized section
240
- if (currentSection) {
241
- const requirementText = trimmed.replace(/^###\s*/, '').trim();
242
- if (requirementText) { // Only count if requirement text is not empty
243
- if (currentSection === 'todo') {
244
- todoCount++;
245
- } else if (currentSection === 'toverify') {
246
- toVerifyCount++;
247
- }
248
- }
249
- }
250
- } else if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
251
- // Detect section headers (must start with ## but not ###)
252
- if (trimmed.includes('ā³ Requirements not yet completed') ||
253
- trimmed.includes('Requirements not yet completed')) {
254
- currentSection = 'todo';
255
- } else if (trimmed.includes('šŸ” TO VERIFY BY HUMAN') ||
256
- trimmed.includes('TO VERIFY BY HUMAN') ||
257
- trimmed.includes('šŸ” TO VERIFY') ||
258
- trimmed.includes('TO VERIFY') ||
259
- trimmed.includes('āœ… Verified by AI') ||
260
- trimmed.includes('Verified by AI')) {
261
- currentSection = 'toverify';
262
- } else {
263
- // Any other section header clears the current section
264
- currentSection = '';
265
- }
266
- }
267
- }
268
-
269
- // Count verified requirements from CHANGELOG.md (at repository root)
270
- const allnightDir = await getVibeCodingMachineDir();
271
- if (allnightDir) {
272
- // CHANGELOG.md is at the repository root
273
- // If .vibecodingmachine is inside repo: go up one level
274
- // If .vibecodingmachine is sibling (../.vibecodingmachine-reponame): go up one level then into repo
275
- let changelogPath;
276
- const allnightStatus = await require('vibecodingmachine-core').checkVibeCodingMachineExists();
277
-
278
- if (allnightStatus.insideExists) {
279
- // .vibecodingmachine is inside repo, so go up one level
280
- changelogPath = path.join(path.dirname(allnightDir), 'CHANGELOG.md');
281
- } else if (allnightStatus.siblingExists) {
282
- // .vibecodingmachine is sibling, use current working directory
283
- changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
284
- }
285
-
286
- if (changelogPath && await fs.pathExists(changelogPath)) {
287
- const changelogContent = await fs.readFile(changelogPath, 'utf8');
288
- // Count entries that look like completed requirements
289
- // Each entry typically starts with "- " followed by date/description
290
- const changelogLines = changelogContent.split('\n');
291
- for (const line of changelogLines) {
292
- const trimmed = line.trim();
293
- // Count lines that start with "- " and have substantial content (not just empty bullets)
294
- if (trimmed.startsWith('- ') && trimmed.length > 10) {
295
- verifiedCount++;
296
- }
297
- }
298
- }
299
- }
300
-
301
- return { todoCount, toVerifyCount, verifiedCount };
219
+ return await getProjectRequirementStats(repoPath);
302
220
  } catch (error) {
303
221
  return null;
304
222
  }
@@ -350,36 +268,27 @@ async function getCurrentProgress() {
350
268
  }
351
269
 
352
270
  async function showWelcomeScreen() {
271
+
272
+
353
273
  const repoPath = process.cwd(); // Always use current working directory
354
274
  const autoStatus = await checkAutoModeStatus();
355
- const allnightStatus = await checkVibeCodingMachineExists();
356
275
  const hostname = getHostname();
357
- const requirementsFilename = await getRequirementsFilename();
358
- const useHostname = await isComputerNameEnabled();
359
276
 
360
277
  // Get current IDE from config
361
278
  const { getAutoConfig } = require('./config');
362
279
  const autoConfig = await getAutoConfig();
363
- const currentIDE = autoConfig.ide || autoStatus.ide || 'claude-code';
364
280
 
365
281
  // Check for requirements file
366
282
  const hasRequirements = await requirementsExists();
367
- let requirementsLocation = '';
368
-
369
- if (allnightStatus.insideExists) {
370
- requirementsLocation = '.vibecodingmachine';
371
- } else if (allnightStatus.siblingExists) {
372
- requirementsLocation = path.basename(allnightStatus.siblingDir);
373
- }
374
283
 
375
284
  // Count requirements if file exists
376
285
  const counts = hasRequirements ? await countRequirements() : null;
377
286
 
378
- // Clear the screen
287
+ // Clear the screen using console.clear() for better cross-platform compatibility
288
+ // This ensures proper screen refresh and prevents text overlap
379
289
  console.clear();
380
290
 
381
291
  // Get version from package.json
382
- const pkg = require('../../package.json');
383
292
  const version = `v${pkg.version}`;
384
293
 
385
294
  // Display welcome banner with version
@@ -419,14 +328,18 @@ async function showWelcomeScreen() {
419
328
  const icon = stageIcons[progress.status] || 'ā³';
420
329
  const statusColor = progress.status === 'DONE' ? chalk.green : chalk.magenta;
421
330
 
331
+ // Truncate requirement text first, THEN apply color to fix box alignment
332
+ const requirementText = progress.requirement ? progress.requirement.substring(0, 60) + (progress.requirement.length > 60 ? '...' : '') : 'No requirement';
333
+
422
334
  console.log(boxen(
423
335
  statusColor.bold(`${icon} ${progress.status}`) + '\n' +
424
- chalk.gray(progress.requirement ? progress.requirement.substring(0, 60) + (progress.requirement.length > 60 ? '...' : '') : 'No requirement'),
336
+ chalk.gray(requirementText),
425
337
  {
426
338
  padding: { left: 1, right: 1, top: 0, bottom: 0 },
427
339
  margin: 0,
428
340
  borderStyle: 'round',
429
- borderColor: 'magenta'
341
+ borderColor: 'magenta',
342
+ width: 70
430
343
  }
431
344
  ));
432
345
  }
@@ -475,7 +388,7 @@ async function showRequirementsTree() {
475
388
  };
476
389
 
477
390
  // Build tree structure
478
- const buildTree = () => {
391
+ const buildTree = async () => {
479
392
  tree.items = [];
480
393
 
481
394
  // Root: Requirements
@@ -485,63 +398,72 @@ async function showRequirementsTree() {
485
398
  tree.items.push({ level: 1, type: 'add', label: 'āž• Add new requirement', key: 'add-one' });
486
399
  tree.items.push({ level: 1, type: 'add', label: 'āž• Add multiple requirements', key: 'add-many' });
487
400
 
488
- // Calculate counts and percentages
401
+ // Use pre-calculated stats and labels from shared logic
402
+ const stats = await countRequirements();
403
+ const { todoCount, toVerifyCount, verifiedCount, total, todoLabel, toVerifyLabel, verifiedLabel } = stats || {
404
+ todoCount: 0, toVerifyCount: 0, verifiedCount: 0, total: 0,
405
+ todoLabel: 'ā³ TODO (0 - 0%)', toVerifyLabel: 'āœ… TO VERIFY (0 - 0%)', verifiedLabel: 'šŸŽ‰ VERIFIED (0 - 0%)'
406
+ };
407
+
489
408
  const verifiedReqs = tree.verifiedReqs || [];
490
409
  const verifyReqs = tree.verifyReqs || [];
491
410
  const clarificationReqs = tree.clarificationReqs || [];
492
411
  const todoReqs = tree.todoReqs || [];
493
412
  const recycledReqs = tree.recycledReqs || [];
494
- const total = verifiedReqs.length + verifyReqs.length + clarificationReqs.length + todoReqs.length + recycledReqs.length;
495
-
496
- const verifiedPercent = total > 0 ? Math.round((verifiedReqs.length / total) * 100) : 0;
497
- const verifyPercent = total > 0 ? Math.round((verifyReqs.length / total) * 100) : 0;
498
- const clarificationPercent = total > 0 ? Math.round((clarificationReqs.length / total) * 100) : 0;
499
- const todoPercent = total > 0 ? Math.round((todoReqs.length / total) * 100) : 0;
500
- const recycledPercent = total > 0 ? Math.round((recycledReqs.length / total) * 100) : 0;
501
413
 
502
- // VERIFIED section (first)
503
- tree.items.push({ level: 1, type: 'section', label: `šŸŽ‰ VERIFIED (${verifiedReqs.length} - ${verifiedPercent}%)`, key: 'verified' });
414
+ // VERIFIED section (first) - only show if has requirements
415
+ if (verifiedReqs.length > 0 || verifiedCount > 0) {
416
+ tree.items.push({ level: 1, type: 'section', label: `šŸŽ‰ ${verifiedLabel}`, key: 'verified' });
504
417
 
505
- if (tree.expanded.verified) {
506
- verifiedReqs.forEach((req, idx) => {
507
- tree.items.push({ level: 2, type: 'verified', label: req, key: `verified-${idx}` });
508
- });
418
+ if (tree.expanded.verified) {
419
+ verifiedReqs.forEach((req, idx) => {
420
+ tree.items.push({ level: 2, type: 'verified', label: req, key: `verified-${idx}` });
421
+ });
422
+ }
509
423
  }
510
424
 
511
- // TO VERIFY section (second)
512
- tree.items.push({ level: 1, type: 'section', label: `āœ… TO VERIFY (${verifyReqs.length} - ${verifyPercent}%)`, key: 'verify', section: 'āœ… Verified by AI screenshot' });
425
+ // TO VERIFY section (second) - only show if has requirements
426
+ if (verifyReqs.length > 0 || toVerifyCount > 0) {
427
+ tree.items.push({ level: 1, type: 'section', label: `āœ… ${toVerifyLabel}`, key: 'verify', section: 'āœ… Verified by AI screenshot' });
513
428
 
514
- if (tree.expanded.verify) {
515
- verifyReqs.forEach((req, idx) => {
516
- tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `verify-${idx}`, req, sectionKey: 'verify' });
517
- });
429
+ if (tree.expanded.verify) {
430
+ verifyReqs.forEach((req, idx) => {
431
+ tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `verify-${idx}`, req, sectionKey: 'verify' });
432
+ });
433
+ }
518
434
  }
519
435
 
520
- // NEEDING CLARIFICATION section (third)
521
- tree.items.push({ level: 1, type: 'section', label: `ā“ NEEDING CLARIFICATION (${clarificationReqs.length} - ${clarificationPercent}%)`, key: 'clarification', section: 'ā“ Requirements needing manual feedback' });
436
+ // NEEDING CLARIFICATION section (third) - only show if has requirements
437
+ if (clarificationReqs.length > 0) {
438
+ tree.items.push({ level: 1, type: 'section', label: `ā“ NEEDING CLARIFICATION (${clarificationReqs.length} - ${clarificationPercent}%)`, key: 'clarification', section: 'ā“ Requirements needing manual feedback' });
522
439
 
523
- if (tree.expanded.clarification) {
524
- clarificationReqs.forEach((req, idx) => {
525
- tree.items.push({ level: 2, type: 'clarification', label: req.title, key: `clarification-${idx}`, req, sectionKey: 'clarification' });
526
- });
440
+ if (tree.expanded.clarification) {
441
+ clarificationReqs.forEach((req, idx) => {
442
+ tree.items.push({ level: 2, type: 'clarification', label: req.title, key: `clarification-${idx}`, req, sectionKey: 'clarification' });
443
+ });
444
+ }
527
445
  }
528
446
 
529
- // TODO section (fourth)
530
- tree.items.push({ level: 1, type: 'section', label: `ā³ TODO (${todoReqs.length} - ${todoPercent}%)`, key: 'todo', section: 'ā³ Requirements not yet completed' });
447
+ // TODO section (fourth) - only show if has requirements
448
+ if (todoReqs.length > 0 || todoCount > 0) {
449
+ tree.items.push({ level: 1, type: 'section', label: `ā³ ${todoLabel}`, key: 'todo', section: 'ā³ Requirements not yet completed' });
531
450
 
532
- if (tree.expanded.todo) {
533
- todoReqs.forEach((req, idx) => {
534
- tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `todo-${idx}`, req, sectionKey: 'todo' });
535
- });
451
+ if (tree.expanded.todo) {
452
+ todoReqs.forEach((req, idx) => {
453
+ tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `todo-${idx}`, req, sectionKey: 'todo' });
454
+ });
455
+ }
536
456
  }
537
457
 
538
- // RECYCLED section (last)
539
- tree.items.push({ level: 1, type: 'section', label: `ā™»ļø RECYCLED (${recycledReqs.length} - ${recycledPercent}%)`, key: 'recycled', section: 'ā™»ļø Recycled' });
458
+ // RECYCLED section (last) - only show if has requirements
459
+ if (recycledReqs.length > 0) {
460
+ tree.items.push({ level: 1, type: 'section', label: `ā™»ļø RECYCLED (${recycledReqs.length} - ${recycledPercent}%)`, key: 'recycled', section: 'ā™»ļø Recycled' });
540
461
 
541
- if (tree.expanded.recycled) {
542
- recycledReqs.forEach((req, idx) => {
543
- tree.items.push({ level: 2, type: 'recycled', label: req.title, key: `recycled-${idx}`, req, sectionKey: 'recycled' });
544
- });
462
+ if (tree.expanded.recycled) {
463
+ recycledReqs.forEach((req, idx) => {
464
+ tree.items.push({ level: 2, type: 'recycled', label: req.title, key: `recycled-${idx}`, req, sectionKey: 'recycled' });
465
+ });
466
+ }
545
467
  }
546
468
  }
547
469
  };
@@ -563,27 +485,96 @@ async function showRequirementsTree() {
563
485
 
564
486
  // For TO VERIFY section, check multiple possible section titles
565
487
  const sectionTitles = sectionKey === 'verify'
566
- ? ['šŸ” TO VERIFY BY HUMAN', 'TO VERIFY BY HUMAN', 'šŸ” TO VERIFY', 'TO VERIFY', 'āœ… Verified by AI screenshot', 'Verified by AI screenshot']
488
+ ? ['šŸ” TO VERIFY BY HUMAN', 'TO VERIFY BY HUMAN', 'šŸ” TO VERIFY', 'TO VERIFY', 'āœ… Verified by AI screenshot', 'Verified by AI screenshot', 'Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG']
567
489
  : [sectionTitle];
568
490
 
491
+ // For TO VERIFY, we need to find the exact section header
492
+ // The section header is: ## āœ… Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
493
+ const toVerifySectionHeader = '## āœ… Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
494
+
569
495
  for (let i = 0; i < lines.length; i++) {
570
496
  const line = lines[i];
497
+ const trimmed = line.trim();
571
498
 
572
499
  // Check if this line matches any of the section titles
573
- if (sectionTitles.some(title => line.includes(title))) {
574
- inSection = true;
575
- continue;
500
+ // IMPORTANT: Only check section headers (lines starting with ##), not requirement text
501
+ if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
502
+ // Reset inSection if we hit a section header that's not our target section
503
+ if (sectionKey === 'verify' && inSection) {
504
+ // Check if this is still a TO VERIFY section header
505
+ if (!isStillToVerify) {
506
+ // This will be handled by the "leaving section" check below, but ensure we don't process it as entering
507
+ }
508
+ }
509
+ if (sectionKey === 'verify') {
510
+ // For TO VERIFY, check for the specific section header with exact matching
511
+ // Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
512
+ const isToVerifyHeader = trimmed === '## šŸ” TO VERIFY BY HUMAN' ||
513
+ trimmed.startsWith('## šŸ” TO VERIFY BY HUMAN') ||
514
+ trimmed === '## šŸ” TO VERIFY' ||
515
+ trimmed.startsWith('## šŸ” TO VERIFY') ||
516
+ trimmed === '## TO VERIFY' ||
517
+ trimmed.startsWith('## TO VERIFY') ||
518
+ trimmed === '## āœ… TO VERIFY' ||
519
+ trimmed.startsWith('## āœ… TO VERIFY') ||
520
+ trimmed === toVerifySectionHeader ||
521
+ (trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
522
+
523
+ if (isToVerifyHeader) {
524
+ // Make sure it's not a VERIFIED section (without TO VERIFY)
525
+ if (!trimmed.includes('## šŸ“ VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('šŸ“ VERIFIED')) {
526
+ inSection = true;
527
+ continue;
528
+ }
529
+ } else {
530
+ // If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
531
+ // This prevents incorrectly reading from TODO or other sections
532
+ if (trimmed.includes('ā³ Requirements not yet completed') ||
533
+ trimmed.includes('Requirements not yet completed') ||
534
+ trimmed === '## šŸ“ VERIFIED' ||
535
+ trimmed.startsWith('## šŸ“ VERIFIED')) {
536
+ // We're in TODO or VERIFIED section, not TO VERIFY - reset
537
+ inSection = false;
538
+ }
539
+ }
540
+ } else if (sectionTitles.some(title => trimmed.includes(title))) {
541
+ inSection = true;
542
+ continue;
543
+ }
576
544
  }
577
545
 
578
- if (inSection && line.startsWith('## ') && !line.startsWith('###') && !sectionTitles.some(title => line.includes(title))) {
579
- break;
546
+ // Check if we're leaving the section (new section header that doesn't match)
547
+ if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
548
+ // If this is a new section header and it's not one of our section titles, we've left the section
549
+ if (sectionKey === 'verify') {
550
+ // For TO VERIFY, only break if this is clearly a different section
551
+ // Check for specific section headers that indicate we've left TO VERIFY
552
+
553
+ if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
554
+ break; // Different section, we've left TO VERIFY
555
+ }
556
+ // Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
557
+ } else {
558
+ // For other sections, break if it's a new section header that doesn't match
559
+ if (!sectionTitles.some(title => trimmed.includes(title))) {
560
+ break;
561
+ }
562
+ }
580
563
  }
581
564
 
582
565
  // Read requirements in new format (### header)
583
566
  if (inSection && line.trim().startsWith('###')) {
584
- const title = line.trim().replace(/^###\s*/, '');
567
+ const title = line.trim().replace(/^###\s*/, '').trim();
568
+
569
+ // Skip malformed requirements (title is just a package name, empty, or too short)
570
+ // Common package names that shouldn't be requirement titles
571
+ const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
572
+ if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
573
+ continue; // Skip this malformed requirement
574
+ }
575
+
585
576
  const details = [];
586
- let package = null;
577
+ let pkg = null;
587
578
 
588
579
  // Read package and description
589
580
  for (let j = i + 1; j < lines.length; j++) {
@@ -594,18 +585,29 @@ async function showRequirementsTree() {
594
585
  }
595
586
  // Check for PACKAGE line
596
587
  if (nextLine.startsWith('PACKAGE:')) {
597
- package = nextLine.replace(/^PACKAGE:\s*/, '').trim();
588
+ pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
598
589
  } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
599
590
  // Description line
600
591
  details.push(nextLine);
601
592
  }
602
593
  }
603
594
 
604
- requirements.push({ title, details, package, lineIndex: i });
595
+ requirements.push({ title, details, pkg, lineIndex: i });
605
596
  }
606
597
  }
607
598
 
608
- return requirements;
599
+ // Remove duplicates based on title (keep first occurrence)
600
+ const seenTitles = new Set();
601
+ const uniqueRequirements = [];
602
+ for (const req of requirements) {
603
+ const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
604
+ if (!seenTitles.has(normalizedTitle)) {
605
+ seenTitles.add(normalizedTitle);
606
+ uniqueRequirements.push(req);
607
+ }
608
+ }
609
+
610
+ return uniqueRequirements;
609
611
  };
610
612
 
611
613
  // Load VERIFIED requirements from CHANGELOG
@@ -721,7 +723,7 @@ async function showRequirementsTree() {
721
723
  tree.recycledReqs = await loadSection('recycled', 'ā™»ļø Recycled');
722
724
 
723
725
  let inTree = true;
724
- buildTree();
726
+ await buildTree();
725
727
 
726
728
  while (inTree) {
727
729
  console.clear();
@@ -828,7 +830,7 @@ async function showRequirementsTree() {
828
830
  if (tree.expanded[current.key]) {
829
831
  // Collapse expanded section
830
832
  tree.expanded[current.key] = false;
831
- buildTree();
833
+ await buildTree();
832
834
  } else if (current.level > 0) {
833
835
  // Go to parent
834
836
  for (let i = tree.selected - 1; i >= 0; i--) {
@@ -841,10 +843,12 @@ async function showRequirementsTree() {
841
843
  // At root level, go back to main menu
842
844
  inTree = false;
843
845
  }
844
- } else if (key.name === 'up') {
846
+ } else if (key.name === 'k' || key.name === 'up') {
845
847
  tree.selected = Math.max(0, tree.selected - 1);
846
- } else if (key.name === 'down') {
848
+ await buildTree();
849
+ } else if (key.name === 'j' || key.name === 'down') {
847
850
  tree.selected = Math.min(tree.items.length - 1, tree.selected + 1);
851
+ await buildTree();
848
852
  } else if (key.name === 'right' || key.name === 'return' || key.name === 'space') {
849
853
  const current = tree.items[tree.selected];
850
854
  if (!current) continue; // Safety check
@@ -861,19 +865,19 @@ async function showRequirementsTree() {
861
865
  } else if (current.key === 'recycled') {
862
866
  tree.recycledReqs = await loadSection(current.key, current.section);
863
867
  }
864
- buildTree();
868
+ await buildTree();
865
869
  } else {
866
870
  tree.expanded[current.key] = false;
867
- buildTree();
871
+ await buildTree();
868
872
  }
869
873
  } else if (current.type === 'requirement') {
870
874
  // Show requirement actions
871
875
  await showRequirementActions(current.req, current.sectionKey, tree);
872
- buildTree();
876
+ await buildTree();
873
877
  } else if (current.type === 'clarification') {
874
878
  // Show clarification requirement with questions
875
879
  await showClarificationActions(current.req, tree, loadClarification);
876
- buildTree();
880
+ await buildTree();
877
881
  } else if (current.type === 'verified') {
878
882
  // Show verified item details (read-only)
879
883
  console.clear();
@@ -900,7 +904,7 @@ async function showRequirementsTree() {
900
904
  await handleAddRequirement(current.key);
901
905
  // Reload TODO section
902
906
  tree.todoReqs = await loadSection('todo', 'ā³ Requirements not yet completed');
903
- buildTree();
907
+ await buildTree();
904
908
  }
905
909
  } else if (key.name === 'r') {
906
910
  const current = tree.items[tree.selected];
@@ -908,15 +912,21 @@ async function showRequirementsTree() {
908
912
 
909
913
  if (current.type === 'requirement') {
910
914
  await deleteRequirement(current.req, current.sectionKey, tree);
911
- buildTree();
915
+ // Reload the section that the requirement was deleted from
916
+ if (current.sectionKey === 'todo') {
917
+ tree.todoReqs = await loadSection('todo', 'ā³ Requirements not yet completed');
918
+ } else if (current.sectionKey === 'verify') {
919
+ tree.verifyReqs = await loadSection('verify', 'āœ… Verified by AI screenshot');
920
+ }
921
+ await buildTree();
912
922
  } else if (current.type === 'clarification') {
913
923
  await deleteClarification(current.req, tree);
914
924
  tree.clarificationReqs = await loadClarification();
915
- buildTree();
925
+ await buildTree();
916
926
  } else if (current.type === 'recycled') {
917
- await deleteRequirement(current.req, current.sectionKey, tree);
927
+ await permanentlyDeleteRequirement(current.req, current.sectionKey, tree);
918
928
  tree.recycledReqs = await loadSection('recycled', 'ā™»ļø Recycled');
919
- buildTree();
929
+ await buildTree();
920
930
  }
921
931
  } else if (key.name === 'j') {
922
932
  const current = tree.items[tree.selected];
@@ -924,7 +934,7 @@ async function showRequirementsTree() {
924
934
 
925
935
  if (current.type === 'requirement') {
926
936
  await moveRequirementDown(current.req, current.sectionKey, tree);
927
- buildTree();
937
+ await buildTree();
928
938
  // Move selection down to follow the item
929
939
  if (tree.selected < tree.items.length - 1) {
930
940
  tree.selected++;
@@ -936,7 +946,7 @@ async function showRequirementsTree() {
936
946
 
937
947
  if (current.type === 'requirement') {
938
948
  await moveRequirementUp(current.req, current.sectionKey, tree);
939
- buildTree();
949
+ await buildTree();
940
950
  // Move selection up to follow the item
941
951
  if (tree.selected > 0) {
942
952
  tree.selected--;
@@ -948,7 +958,7 @@ async function showRequirementsTree() {
948
958
 
949
959
  if (current.type === 'requirement') {
950
960
  await promoteRequirement(current.req, current.sectionKey, tree, loadSection, loadVerified);
951
- buildTree();
961
+ await buildTree();
952
962
  }
953
963
  } else if (key.name === 'd') {
954
964
  const current = tree.items[tree.selected];
@@ -959,12 +969,12 @@ async function showRequirementsTree() {
959
969
  await moveClarificationToTodo(current.req, tree);
960
970
  tree.clarificationReqs = await loadClarification();
961
971
  tree.todoReqs = await loadSection('todo', 'ā³ Requirements not yet completed');
962
- buildTree();
972
+ await buildTree();
963
973
  } else if (current.type === 'requirement' || current.type === 'verified') {
964
974
  const sectionKey = current.type === 'verified' ? 'verified' : current.sectionKey;
965
975
  const reqTitle = current.type === 'verified' ? current.label : current.req.title;
966
976
  await demoteRequirement(reqTitle, sectionKey, tree, loadSection, loadVerified);
967
- buildTree();
977
+ await buildTree();
968
978
  }
969
979
  }
970
980
  }
@@ -1404,8 +1414,9 @@ async function showClarificationActions(req, tree, loadClarification) {
1404
1414
  }
1405
1415
  async function showRequirementActions(req, sectionKey, tree) {
1406
1416
  const actions = [
1407
- { label: 'šŸ‘ Thumbs up (move to top)', value: 'thumbs-up' },
1408
- { label: 'šŸ‘Ž Thumbs down (move to bottom)', value: 'thumbs-down' },
1417
+ { label: 'āœļø Rename/Edit', value: 'rename' },
1418
+ { label: 'šŸ‘ Thumbs up (promote to Verified)', value: 'thumbs-up' },
1419
+ { label: 'šŸ‘Ž Thumbs down (demote to TODO)', value: 'thumbs-down' },
1409
1420
  { label: 'ā¬†ļø Move up', value: 'move-up' },
1410
1421
  { label: 'ā¬‡ļø Move down', value: 'move-down' },
1411
1422
  { label: 'šŸ—‘ļø Delete', value: 'delete' }
@@ -1477,7 +1488,9 @@ async function showRequirementActions(req, sectionKey, tree) {
1477
1488
 
1478
1489
  if (!key) continue;
1479
1490
 
1480
- if ((key.ctrl && key.name === 'c') || key.name === 'escape' || key.name === 'left') {
1491
+ if (key.ctrl && key.name === 'c') {
1492
+ process.exit(0);
1493
+ } else if (key.name === 'escape' || key.name === 'left') {
1481
1494
  return; // Go back
1482
1495
  } else if (key.name === 'up') {
1483
1496
  selected = Math.max(0, selected - 1);
@@ -1536,11 +1549,156 @@ async function performRequirementAction(action, req, sectionKey, tree) {
1536
1549
  console.log(chalk.green('\nāœ“ Deleted\n'));
1537
1550
  }
1538
1551
  break;
1552
+ case 'rename':
1553
+ await renameRequirement(req, sectionKey, tree);
1554
+ break;
1539
1555
  }
1540
1556
 
1541
1557
  await new Promise(resolve => setTimeout(resolve, 1000));
1542
1558
  }
1543
1559
 
1560
+ // Helper to rename requirement (title and description)
1561
+ async function renameRequirement(req, sectionKey, tree) {
1562
+ const { getRequirementsPath } = require('vibecodingmachine-core');
1563
+ const reqPath = await getRequirementsPath();
1564
+
1565
+ console.log(chalk.cyan('\nāœļø Rename/Edit Requirement\n'));
1566
+ console.log(chalk.gray('Current title:'), chalk.white(req.title));
1567
+ if (req.details.length > 0) {
1568
+ console.log(chalk.gray('Current description:'));
1569
+ req.details.forEach(line => console.log(chalk.white(' ' + line)));
1570
+ }
1571
+ console.log();
1572
+
1573
+ const answers = await inquirer.prompt([
1574
+ {
1575
+ type: 'input',
1576
+ name: 'title',
1577
+ message: 'New title (leave blank to keep current):',
1578
+ default: ''
1579
+ }
1580
+ ]);
1581
+
1582
+ const newTitle = answers.title.trim() || req.title;
1583
+
1584
+ // Ask for description using multi-line input
1585
+ console.log(chalk.gray('\nEnter new description (leave blank to keep current).'));
1586
+ console.log(chalk.gray('Press Enter twice on empty line to finish:\n'));
1587
+
1588
+ const descriptionLines = [];
1589
+ let emptyLineCount = 0;
1590
+ let isFirstLine = true;
1591
+ let newDescription = '';
1592
+ let keptCurrent = false;
1593
+
1594
+ while (true) {
1595
+ try {
1596
+ const { line } = await inquirer.prompt([{
1597
+ type: 'input',
1598
+ name: 'line',
1599
+ message: isFirstLine ? 'Description:' : ''
1600
+ }]);
1601
+
1602
+ if (isFirstLine && line.trim() === '') {
1603
+ // If first line is empty, keep current description
1604
+ keptCurrent = true;
1605
+ break;
1606
+ }
1607
+
1608
+ isFirstLine = false;
1609
+
1610
+ if (line.trim() === '') {
1611
+ emptyLineCount++;
1612
+ if (emptyLineCount >= 2) break;
1613
+ } else {
1614
+ emptyLineCount = 0;
1615
+ descriptionLines.push(line);
1616
+ }
1617
+ } catch (err) {
1618
+ break;
1619
+ }
1620
+ }
1621
+
1622
+ if (!keptCurrent) {
1623
+ newDescription = descriptionLines.join('\n');
1624
+ }
1625
+
1626
+ // Read the requirements file
1627
+ const content = await fs.readFile(reqPath, 'utf8');
1628
+ const lines = content.split('\n');
1629
+
1630
+ // Find the requirement block
1631
+ let requirementStartIndex = -1;
1632
+ let requirementEndIndex = -1;
1633
+
1634
+ for (let i = 0; i < lines.length; i++) {
1635
+ const line = lines[i].trim();
1636
+ if (line.startsWith('###')) {
1637
+ const title = line.replace(/^###\s*/, '').trim();
1638
+ if (title === req.title) {
1639
+ requirementStartIndex = i;
1640
+ // Find the end of this requirement (next ### or ## header)
1641
+ for (let j = i + 1; j < lines.length; j++) {
1642
+ const nextLine = lines[j].trim();
1643
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
1644
+ requirementEndIndex = j;
1645
+ break;
1646
+ }
1647
+ }
1648
+ if (requirementEndIndex === -1) {
1649
+ requirementEndIndex = lines.length;
1650
+ }
1651
+ break;
1652
+ }
1653
+ }
1654
+ }
1655
+
1656
+ if (requirementStartIndex === -1) {
1657
+ console.log(chalk.yellow('āš ļø Could not find requirement to rename'));
1658
+ return;
1659
+ }
1660
+
1661
+ // Build new requirement block
1662
+ const newBlock = [`### ${newTitle}`];
1663
+
1664
+ // If new description provided, use it; otherwise keep existing details
1665
+ if (newDescription) {
1666
+ newDescription.split('\n').forEach(line => {
1667
+ if (line.trim()) {
1668
+ newBlock.push(line);
1669
+ }
1670
+ });
1671
+ } else {
1672
+ // Keep existing details (skip the ### header line)
1673
+ for (let i = requirementStartIndex + 1; i < requirementEndIndex; i++) {
1674
+ const line = lines[i];
1675
+ // Skip empty lines at the start and PACKAGE lines if we want to preserve them
1676
+ if (line.trim()) {
1677
+ newBlock.push(line);
1678
+ }
1679
+ }
1680
+ }
1681
+
1682
+ newBlock.push(''); // Blank line after requirement
1683
+
1684
+ // Replace the old block with the new one
1685
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex, ...newBlock);
1686
+
1687
+ // Save
1688
+ await fs.writeFile(reqPath, lines.join('\n'));
1689
+ console.log(chalk.green('\nāœ“ Requirement renamed/updated\n'));
1690
+
1691
+ // Update the tree data
1692
+ const reqList = getRequirementList(tree, sectionKey);
1693
+ const reqIndex = reqList.findIndex(r => r.title === req.title);
1694
+ if (reqIndex !== -1) {
1695
+ reqList[reqIndex].title = newTitle;
1696
+ if (newDescription) {
1697
+ reqList[reqIndex].details = newDescription.split('\n').filter(line => line.trim());
1698
+ }
1699
+ }
1700
+ }
1701
+
1544
1702
  // Helper to move requirement to recycled section (used to delete)
1545
1703
  async function deleteRequirement(req, sectionKey, tree) {
1546
1704
  const reqList = getRequirementList(tree, sectionKey);
@@ -1560,6 +1718,61 @@ async function deleteRequirement(req, sectionKey, tree) {
1560
1718
  }
1561
1719
  }
1562
1720
 
1721
+ // Helper to permanently delete requirement from file (used for recycled items)
1722
+ async function permanentlyDeleteRequirement(req, sectionKey, tree) {
1723
+ const reqList = getRequirementList(tree, sectionKey);
1724
+ const reqIndex = reqList.findIndex(r => r.title === req.title);
1725
+
1726
+ if (reqIndex === -1) return;
1727
+
1728
+ const { getRequirementsPath } = require('vibecodingmachine-core');
1729
+ const reqPath = await getRequirementsPath();
1730
+
1731
+ const truncatedTitle = req.title.substring(0, 50) + (req.title.length > 50 ? '...' : '');
1732
+ if (await confirmAction(`Permanently delete? (r/y/N)`)) {
1733
+ const content = await fs.readFile(reqPath, 'utf8');
1734
+ const lines = content.split('\n');
1735
+
1736
+ // Find the requirement block (### header format)
1737
+ let requirementStartIndex = -1;
1738
+ let requirementEndIndex = -1;
1739
+
1740
+ for (let i = 0; i < lines.length; i++) {
1741
+ const line = lines[i].trim();
1742
+ if (line.startsWith('###')) {
1743
+ const title = line.replace(/^###\s*/, '').trim();
1744
+ if (title && title === req.title) {
1745
+ requirementStartIndex = i;
1746
+ // Find the end of this requirement (next ### or ## header)
1747
+ for (let j = i + 1; j < lines.length; j++) {
1748
+ const nextLine = lines[j].trim();
1749
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
1750
+ requirementEndIndex = j;
1751
+ break;
1752
+ }
1753
+ }
1754
+ if (requirementEndIndex === -1) {
1755
+ requirementEndIndex = lines.length;
1756
+ }
1757
+ break;
1758
+ }
1759
+ }
1760
+ }
1761
+
1762
+ if (requirementStartIndex === -1) {
1763
+ console.log(chalk.yellow('āš ļø Could not find requirement to delete'));
1764
+ return;
1765
+ }
1766
+
1767
+ // Remove the requirement from the file completely
1768
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
1769
+
1770
+ // Save
1771
+ await fs.writeFile(reqPath, lines.join('\n'));
1772
+ reqList.splice(reqIndex, 1);
1773
+ }
1774
+ }
1775
+
1563
1776
  // Helper to move requirement to recycled section
1564
1777
  async function moveRequirementToRecycled(reqPath, requirementTitle, fromSection) {
1565
1778
  const content = await fs.readFile(reqPath, 'utf8');
@@ -1573,7 +1786,7 @@ async function moveRequirementToRecycled(reqPath, requirementTitle, fromSection)
1573
1786
  const line = lines[i].trim();
1574
1787
  if (line.startsWith('###')) {
1575
1788
  const title = line.replace(/^###\s*/, '').trim();
1576
- if (title && title.includes(requirementTitle)) {
1789
+ if (title && title === requirementTitle) {
1577
1790
  requirementStartIndex = i;
1578
1791
  // Find the end of this requirement (next ### or ## header)
1579
1792
  for (let j = i + 1; j < lines.length; j++) {
@@ -1600,8 +1813,25 @@ async function moveRequirementToRecycled(reqPath, requirementTitle, fromSection)
1600
1813
  const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
1601
1814
 
1602
1815
  // Remove the requirement from its current location
1816
+ // Also remove any trailing blank lines that might be left behind
1603
1817
  lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
1604
1818
 
1819
+ // Clean up: remove any orphaned blank lines or malformed entries left after removal
1820
+ // Check if there's a blank line followed by a malformed requirement (just "cli", "core", etc.)
1821
+ if (requirementStartIndex < lines.length) {
1822
+ const nextLine = lines[requirementStartIndex]?.trim();
1823
+ const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
1824
+ // If the next line is just a package name (likely orphaned), remove it
1825
+ if (nextLine && packageNames.includes(nextLine.toLowerCase()) &&
1826
+ !nextLine.startsWith('###') && !nextLine.startsWith('PACKAGE:')) {
1827
+ lines.splice(requirementStartIndex, 1);
1828
+ }
1829
+ // Remove any blank lines left after removal
1830
+ while (requirementStartIndex < lines.length && lines[requirementStartIndex]?.trim() === '') {
1831
+ lines.splice(requirementStartIndex, 1);
1832
+ }
1833
+ }
1834
+
1605
1835
  // Find or create Recycled section
1606
1836
  let recycledIndex = -1;
1607
1837
  for (let i = 0; i < lines.length; i++) {
@@ -1703,8 +1933,41 @@ async function demoteRequirement(reqTitle, sectionKey, tree, loadSection, loadVe
1703
1933
  const reqPath = await getRequirementsPath();
1704
1934
 
1705
1935
  if (sectionKey === 'verify') {
1706
- // TO VERIFY -> TODO: Use shared function
1707
- const success = await demoteVerifyToTodo(reqPath, reqTitle);
1936
+ // Prompt for explanation of what went wrong
1937
+ // Prompt for explanation of what went wrong
1938
+ console.log(chalk.gray('\nWhat went wrong? (This will be added to help the AI agent)'));
1939
+ console.log(chalk.gray('Press Enter twice on empty line to finish:\n'));
1940
+
1941
+ const explanationLines = [];
1942
+ let emptyLineCount = 0;
1943
+ let isFirstLine = true;
1944
+
1945
+ while (true) {
1946
+ try {
1947
+ const { line } = await inquirer.prompt([{
1948
+ type: 'input',
1949
+ name: 'line',
1950
+ message: isFirstLine ? 'Explanation:' : ''
1951
+ }]);
1952
+
1953
+ isFirstLine = false;
1954
+
1955
+ if (line.trim() === '') {
1956
+ emptyLineCount++;
1957
+ if (emptyLineCount >= 2) break;
1958
+ } else {
1959
+ emptyLineCount = 0;
1960
+ explanationLines.push(line);
1961
+ }
1962
+ } catch (err) {
1963
+ break;
1964
+ }
1965
+ }
1966
+
1967
+ const explanation = explanationLines.join('\n');
1968
+
1969
+ // TO VERIFY -> TODO: Use shared function with explanation
1970
+ const success = await demoteVerifyToTodo(reqPath, reqTitle, explanation);
1708
1971
  if (success) {
1709
1972
  // Reload sections
1710
1973
  tree.todoReqs = await loadSection('todo', 'ā³ Requirements not yet completed');
@@ -1734,86 +1997,104 @@ async function handleAddRequirement(type) {
1734
1997
  // Ensure it's an array
1735
1998
  if (typeof selectedPackage === 'string') selectedPackage = [selectedPackage];
1736
1999
 
1737
- let askPackage = !config.lastPackage;
1738
- let name = '';
1739
- let pkg = selectedPackage;
1740
-
1741
2000
  while (true) {
1742
- if (askPackage) {
1743
- const answer = await inquirer.prompt([{
2001
+ const pkgDisplay = selectedPackage.join(', ');
2002
+
2003
+ // Custom menu to allow "Up arrow" to select package
2004
+ // Default to "Enter Requirement Name" (index 1) so user can just type
2005
+ const { action } = await inquirer.prompt([{
2006
+ type: 'list',
2007
+ name: 'action',
2008
+ message: 'New Requirement:',
2009
+ choices: [
2010
+ { name: `šŸ“¦ Package: ${pkgDisplay}`, value: 'package' },
2011
+ { name: 'šŸ“ Enter Requirement Name', value: 'name' },
2012
+ { name: 'āŒ Cancel', value: 'cancel' }
2013
+ ],
2014
+ default: 1
2015
+ }]);
2016
+
2017
+ if (action === 'cancel') return;
2018
+
2019
+ if (action === 'package') {
2020
+ // When showing checkboxes, if "all" is currently selected along with specific packages,
2021
+ // unselect "all" first so user sees only specific packages checked
2022
+ let displayPackages = selectedPackage;
2023
+ if (selectedPackage.includes('all') && selectedPackage.length > 1) {
2024
+ displayPackages = selectedPackage.filter(p => p !== 'all');
2025
+ }
2026
+
2027
+ const { pkg } = await inquirer.prompt([{
1744
2028
  type: 'checkbox',
1745
2029
  name: 'pkg',
1746
2030
  message: 'Select package(s):',
1747
2031
  choices: packages.map(p => ({
1748
2032
  name: p,
1749
2033
  value: p,
1750
- checked: pkg.includes(p)
2034
+ checked: displayPackages.includes(p)
1751
2035
  })),
1752
2036
  validate: (answer) => {
1753
2037
  if (answer.length < 1) return 'You must choose at least one package.';
1754
2038
  return true;
1755
2039
  }
1756
2040
  }]);
1757
- pkg = answer.pkg;
1758
2041
 
1759
- // Save to config
1760
- config.lastPackage = pkg;
1761
- await writeConfig(config);
1762
- askPackage = false;
1763
- }
1764
-
1765
- // Ask for requirement name
1766
- const pkgDisplay = pkg.join(', ');
1767
- const nameAnswer = await inquirer.prompt([{
1768
- type: 'input',
1769
- name: 'name',
1770
- message: `Enter requirement name (Package: ${pkgDisplay}) [Type < to change package]:`
1771
- }]);
1772
-
1773
- name = nameAnswer.name;
2042
+ // When selecting specific packages, unselect "all"
2043
+ let finalPackage = pkg;
2044
+ // If both "all" and specific packages are selected, keep only specific packages
2045
+ if (pkg.includes('all') && pkg.length > 1) {
2046
+ finalPackage = pkg.filter(p => p !== 'all');
2047
+ }
1774
2048
 
1775
- if (name === '<') {
1776
- askPackage = true;
2049
+ selectedPackage = finalPackage;
2050
+ config.lastPackage = selectedPackage;
2051
+ await writeConfig(config);
1777
2052
  continue;
1778
2053
  }
1779
2054
 
1780
- break;
1781
- }
1782
-
1783
- // Then ask for multiline description (press Enter twice to finish)
1784
- console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
1785
- const descriptionLines = [];
1786
- let emptyLineCount = 0;
1787
- let isFirstLine = true;
1788
-
1789
- while (true) {
1790
- try {
1791
- const { line } = await inquirer.prompt([{
2055
+ if (action === 'name') {
2056
+ const { name } = await inquirer.prompt([{
1792
2057
  type: 'input',
1793
- name: 'line',
1794
- message: isFirstLine ? 'Description:' : ''
2058
+ name: 'name',
2059
+ message: `Enter requirement name (Package: ${pkgDisplay}):`
1795
2060
  }]);
1796
2061
 
1797
- isFirstLine = false;
2062
+ if (!name || !name.trim()) continue;
1798
2063
 
1799
- if (line.trim() === '') {
1800
- emptyLineCount++;
1801
- if (emptyLineCount >= 2) {
1802
- break; // Two empty lines = done
2064
+ // Ask for description
2065
+ console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
2066
+ const descriptionLines = [];
2067
+ let emptyLineCount = 0;
2068
+ let isFirstLine = true;
2069
+
2070
+ while (true) {
2071
+ try {
2072
+ const { line } = await inquirer.prompt([{
2073
+ type: 'input',
2074
+ name: 'line',
2075
+ message: isFirstLine ? 'Description:' : ''
2076
+ }]);
2077
+
2078
+ isFirstLine = false;
2079
+
2080
+ if (line.trim() === '') {
2081
+ emptyLineCount++;
2082
+ if (emptyLineCount >= 2) break;
2083
+ } else {
2084
+ emptyLineCount = 0;
2085
+ descriptionLines.push(line);
2086
+ }
2087
+ } catch (err) {
2088
+ break;
1803
2089
  }
1804
- } else {
1805
- emptyLineCount = 0;
1806
- descriptionLines.push(line);
1807
2090
  }
1808
- } catch (err) {
1809
- break; // ESC pressed
2091
+
2092
+ const description = descriptionLines.join('\n');
2093
+ await reqCommands.add(name, selectedPackage, description);
2094
+ await new Promise(resolve => setTimeout(resolve, 1000));
2095
+ return;
1810
2096
  }
1811
2097
  }
1812
-
1813
- const description = descriptionLines.join('\n');
1814
- await reqCommands.add(name, pkg, description);
1815
- // Message already printed by reqCommands.add()
1816
- await new Promise(resolve => setTimeout(resolve, 1000));
1817
2098
  } catch (err) {
1818
2099
  // ESC pressed
1819
2100
  }
@@ -1826,7 +2107,7 @@ async function handleAddRequirement(type) {
1826
2107
  while (!done) {
1827
2108
  try {
1828
2109
  // Ask for package
1829
- const { package } = await inquirer.prompt([{
2110
+ const { package: pkg } = await inquirer.prompt([{
1830
2111
  type: 'list',
1831
2112
  name: 'package',
1832
2113
  message: `Package for requirement ${requirements.length + 1}:`,
@@ -1875,7 +2156,7 @@ async function handleAddRequirement(type) {
1875
2156
  }
1876
2157
 
1877
2158
  const description = descriptionLines.join('\n');
1878
- requirements.push({ name, package, description });
2159
+ requirements.push({ name, package: pkg, description });
1879
2160
  }
1880
2161
  } catch (err) {
1881
2162
  done = true;
@@ -1986,6 +2267,7 @@ async function showRequirementsBySection(sectionTitle) {
1986
2267
  { name: 'šŸ‘Ž Thumbs down (deprioritize)', value: 'thumbs-down' },
1987
2268
  { name: 'ā¬†ļø Move up', value: 'move-up' },
1988
2269
  { name: 'ā¬‡ļø Move down', value: 'move-down' },
2270
+ { name: 'ā™»ļø Recycle (move to Recycled)', value: 'recycle' },
1989
2271
  { name: 'šŸ—‘ļø Delete', value: 'delete' }
1990
2272
  ]
1991
2273
  }]);
@@ -2026,6 +2308,15 @@ async function showRequirementsBySection(sectionTitle) {
2026
2308
  console.log(chalk.yellow('\n⚠ Already at bottom\n'));
2027
2309
  }
2028
2310
  break;
2311
+ case 'recycle':
2312
+ const recycledReq = requirements.splice(selectedIndex, 1)[0];
2313
+ await moveToRecycled(reqPath, recycledReq.title, sectionTitle);
2314
+ console.log(chalk.cyan('\nāœ“ Moved to Recycled section\n'));
2315
+ if (requirements.length === 0) {
2316
+ console.log(chalk.gray('No more requirements in this section.\n'));
2317
+ inRequirementsList = false;
2318
+ }
2319
+ break;
2029
2320
  case 'delete':
2030
2321
  const { confirmDelete } = await inquirer.prompt([{
2031
2322
  type: 'confirm',
@@ -2162,7 +2453,6 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2162
2453
  if (selectedIndex >= items.length) selectedIndex = 0;
2163
2454
 
2164
2455
  let isFirstRender = true;
2165
- let lastLinesPrinted = 0;
2166
2456
 
2167
2457
  // Helper to calculate visual lines occupied by text
2168
2458
  const getVisualLineCount = (text) => {
@@ -2187,12 +2477,18 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2187
2477
  return lineCount;
2188
2478
  };
2189
2479
 
2190
- const displayMenu = () => {
2191
- // Clear previous menu (move cursor up and clear lines) - but not on first render
2192
- if (!isFirstRender && lastLinesPrinted > 0) {
2193
- // Move cursor up by the number of lines we printed last time
2194
- readline.moveCursor(process.stdout, 0, -lastLinesPrinted);
2195
- readline.clearScreenDown(process.stdout);
2480
+ const displayMenu = async () => {
2481
+ // Clear entire screen on navigation to prevent text overlap with banner
2482
+ if (!isFirstRender) {
2483
+ // No need to console.clear() here as showWelcomeScreen does it,
2484
+ // but ensuring it clears before we start waiting is fine too.
2485
+ console.clear();
2486
+ // Reprint the banner and status info
2487
+ try {
2488
+ await showWelcomeScreen();
2489
+ } catch (err) {
2490
+ console.error('Error displaying banner:', err);
2491
+ }
2196
2492
  }
2197
2493
  isFirstRender = false;
2198
2494
 
@@ -2296,7 +2592,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2296
2592
  process.stdin.setRawMode(true);
2297
2593
  }
2298
2594
 
2299
- const onKeypress = (str, key) => {
2595
+ const onKeypress = async (str, key) => {
2300
2596
  if (!key) return;
2301
2597
 
2302
2598
  // Ctrl+C to exit
@@ -2342,21 +2638,27 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2342
2638
 
2343
2639
  // Arrow keys for navigation
2344
2640
  if (key.name === 'up') {
2345
- // Skip blank and info lines when navigating
2346
- let newIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1;
2347
- while ((items[newIndex].type === 'blank' || items[newIndex].type === 'info') && newIndex !== selectedIndex) {
2348
- newIndex = newIndex > 0 ? newIndex - 1 : items.length - 1;
2641
+ // Search backwards for the previous selectable item
2642
+ let testIndex = selectedIndex - 1;
2643
+ while (testIndex >= 0) {
2644
+ if (items[testIndex].type !== 'blank' && items[testIndex].type !== 'info') {
2645
+ selectedIndex = testIndex;
2646
+ await displayMenu();
2647
+ break;
2648
+ }
2649
+ testIndex--;
2349
2650
  }
2350
- selectedIndex = newIndex;
2351
- displayMenu();
2352
2651
  } else if (key.name === 'down') {
2353
- // Skip blank and info lines when navigating
2354
- let newIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0;
2355
- while ((items[newIndex].type === 'blank' || items[newIndex].type === 'info') && newIndex !== selectedIndex) {
2356
- newIndex = newIndex < items.length - 1 ? newIndex + 1 : 0;
2652
+ // Search forwards for the next selectable item
2653
+ let testIndex = selectedIndex + 1;
2654
+ while (testIndex < items.length) {
2655
+ if (items[testIndex].type !== 'blank' && items[testIndex].type !== 'info') {
2656
+ selectedIndex = testIndex;
2657
+ await displayMenu();
2658
+ break;
2659
+ }
2660
+ testIndex++;
2357
2661
  }
2358
- selectedIndex = newIndex;
2359
- displayMenu();
2360
2662
  } else if (key.name === 'return' || key.name === 'right') {
2361
2663
  // Don't allow selecting blank or info lines
2362
2664
  if (items[selectedIndex].type !== 'blank' && items[selectedIndex].type !== 'info') {
@@ -2379,44 +2681,129 @@ async function showProviderManagerMenu() {
2379
2681
  let selectedIndex = 0;
2380
2682
  let dirty = false;
2381
2683
 
2382
- // Initialize ProviderManager to check rate limits
2383
- const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
2384
- const providerManager = new ProviderManager();
2684
+ const { fetchQuotaForAgent } = require('vibecodingmachine-core/src/quota-management');
2685
+
2686
+ const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
2385
2687
 
2386
- const render = () => {
2688
+ const formatDuration = (ms) => {
2689
+ if (!ms || ms <= 0) return 'now';
2690
+ const totalSeconds = Math.ceil(ms / 1000);
2691
+ const hours = Math.floor(totalSeconds / 3600);
2692
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
2693
+ const seconds = totalSeconds % 60;
2694
+
2695
+ if (hours > 0) return `${hours}h ${minutes}m`;
2696
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
2697
+ return `${seconds}s`;
2698
+ };
2699
+
2700
+ const render = async () => {
2387
2701
  process.stdout.write('\x1Bc');
2388
2702
  console.log(chalk.bold.cyan('āš™ Provider Order & Availability\n'));
2703
+
2704
+ // Fetch quota info
2705
+ const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
2706
+ const autoConfig = await getAutoConfig();
2707
+ const quotaInfo = await sharedAuth.canRunAutoMode();
2708
+ const remaining = Math.max(0, (quotaInfo.maxIterations || 10) - (quotaInfo.todayUsage || 0));
2709
+
2710
+ // Calculate time until reset (midnight)
2711
+ const now = new Date();
2712
+ const tonight = new Date(now);
2713
+ tonight.setHours(24, 0, 0, 0);
2714
+ const msUntilReset = tonight.getTime() - now.getTime();
2715
+ const hoursUntilReset = Math.floor(msUntilReset / (1000 * 60 * 60));
2716
+ const minsUntilReset = Math.floor((msUntilReset % (1000 * 60 * 60)) / (1000 * 60));
2717
+
2718
+ // Display quota as time-based instead of numeric (0/1 or 1/1 format)
2719
+ let quotaDisplay;
2720
+ if (remaining === 0) {
2721
+ // Rate limit active - show when it resets (in red)
2722
+ quotaDisplay = chalk.gray(' Overall Quota: ') + chalk.red(`ā° Rate limit resets in ${hoursUntilReset}h ${minsUntilReset}m`);
2723
+ } else {
2724
+ // Quota available - show when it resets (in green)
2725
+ quotaDisplay = chalk.gray(' Overall Quota: ') + chalk.green(`āœ“ Available (${remaining}/${quotaInfo.maxIterations})`) + chalk.gray(' • Resets in ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
2726
+ }
2727
+ console.log(quotaDisplay);
2389
2728
  console.log(chalk.gray(' ↑/↓ move selection j/k reorder e enable d disable Space toggle Enter save/select Esc cancel\n'));
2390
2729
 
2391
- order.forEach((id, idx) => {
2730
+ for (let idx = 0; idx < order.length; idx++) {
2731
+ const id = order[idx];
2392
2732
  const def = defMap.get(id);
2393
- if (!def) return;
2733
+ if (!def) continue;
2394
2734
  const isSelected = idx === selectedIndex;
2395
2735
  const isEnabled = enabled[id] !== false;
2396
- const statusLabel = isEnabled ? chalk.green('ENABLED') : chalk.red('DISABLED');
2736
+
2737
+ // Check for Kiro installation
2738
+ let isInstalled = true;
2739
+ if (id === 'kiro') {
2740
+ try {
2741
+ const { isKiroInstalled } = require('./kiro-installer');
2742
+ isInstalled = isKiroInstalled();
2743
+ } catch (e) {
2744
+ // Ignore error provider checks
2745
+ }
2746
+ }
2747
+
2748
+ // Determine status emoji: disabled = red alert, rate limited = green circle, enabled = green circle
2749
+ let statusEmoji;
2750
+ if (id === 'kiro' && !isInstalled) {
2751
+ statusEmoji = '🟔'; // Yellow for not installed
2752
+ } else {
2753
+ statusEmoji = !isEnabled ? '🚨' : '🟢';
2754
+ }
2755
+
2397
2756
  const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
2398
2757
  const prefix = isSelected ? chalk.cyan('āÆ') : ' ';
2399
- let line = `${prefix} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel} ${statusLabel}`;
2400
-
2401
- // Check for rate limits
2402
- const timeUntilReset = providerManager.getTimeUntilReset(id, def.model || id);
2403
- if (timeUntilReset) {
2404
- const resetTime = Date.now() + timeUntilReset;
2405
- const resetDate = new Date(resetTime);
2406
- const timeStr = resetDate.toLocaleString('en-US', {
2407
- weekday: 'short',
2408
- month: 'short',
2409
- day: 'numeric',
2410
- hour: 'numeric',
2411
- minute: '2-digit',
2412
- hour12: true,
2413
- timeZoneName: 'short'
2414
- });
2415
- line += ` ${chalk.red('ā° Rate limited until ' + timeStr)}`;
2758
+ let line = `${prefix} ${statusEmoji} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel}`;
2759
+
2760
+ // Fetch and display specific quota for this agent
2761
+ try {
2762
+ // Find the active model for this provider if possible
2763
+ let model = def.defaultModel || id;
2764
+ if (id === 'groq') model = autoConfig.groqModel || model;
2765
+ else if (id === 'anthropic') model = autoConfig.anthropicModel || model;
2766
+ else if (id === 'ollama') {
2767
+ const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
2768
+ ? autoConfig.llmModel.split('/')[1]
2769
+ : autoConfig.llmModel || autoConfig.aiderModel;
2770
+ model = (preferredModel && preferredModel !== id) ? preferredModel : model;
2771
+ }
2772
+
2773
+ const agentId = `${id}:${model}`;
2774
+ const quota = await fetchQuotaForAgent(agentId);
2775
+
2776
+ if (debugQuota) {
2777
+ const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
2778
+ console.error(`[VCM_DEBUG_QUOTA] provider=${id} model=${model} type=${quota?.type} remaining=${quota?.remaining} limit=${quota?.limit} resetsAt=${quota?.resetsAt ? new Date(quota.resetsAt).toISOString() : 'null'} resetIn=${resetMs !== null ? formatDuration(resetMs) : 'null'}`);
2779
+ }
2780
+
2781
+ if (quota.type === 'infinite') {
2782
+ line += ` ${chalk.gray('[Quota: Infinite]')}`;
2783
+ } else if (quota.type === 'rate-limit') {
2784
+ if (quota.isExceeded()) {
2785
+ if (quota.resetsAt) {
2786
+ const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
2787
+ line += ` ${chalk.red(`[ā³ resets in ${formatDuration(msUntilReset)}]`)}`;
2788
+ } else {
2789
+ line += ` ${chalk.red('[Rate limited]')}`;
2790
+ }
2791
+ } else {
2792
+ // Show time until rate limit starts (when it resets)
2793
+ if (quota.resetsAt) {
2794
+ const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
2795
+ line += ` ${chalk.green(`[āœ“ available • resets in ${formatDuration(msUntilReset)}]`)}`;
2796
+ } else {
2797
+ line += ` ${chalk.green('[Available]')}`;
2798
+ }
2799
+ }
2800
+ }
2801
+ } catch (e) {
2802
+ // Silently skip if quota fetch fails
2416
2803
  }
2417
2804
 
2418
2805
  console.log(line);
2419
- });
2806
+ }
2420
2807
 
2421
2808
  console.log();
2422
2809
  if (dirty) {
@@ -2426,7 +2813,12 @@ async function showProviderManagerMenu() {
2426
2813
  }
2427
2814
  };
2428
2815
 
2429
- return new Promise((resolve) => {
2816
+ if (process.env.VCM_RENDER_ONCE === '1' || process.env.VCM_RENDER_ONCE === 'true') {
2817
+ await render();
2818
+ return;
2819
+ }
2820
+
2821
+ return new Promise(async (resolve) => {
2430
2822
  const cleanup = () => {
2431
2823
  if (process.stdin.isTTY && process.stdin.setRawMode) {
2432
2824
  process.stdin.setRawMode(false);
@@ -2441,6 +2833,18 @@ async function showProviderManagerMenu() {
2441
2833
  await saveProviderPreferences(order, enabled);
2442
2834
  }
2443
2835
  if (selectedId) {
2836
+ // Check for Kiro installation if selected
2837
+ if (selectedId === 'kiro') {
2838
+ try {
2839
+ const { isKiroInstalled, installKiro } = require('./kiro-installer');
2840
+ if (!isKiroInstalled()) {
2841
+ await installKiro();
2842
+ }
2843
+ } catch (e) {
2844
+ console.log(chalk.red('Error checking/installing Kiro: ' + e.message));
2845
+ }
2846
+ }
2847
+
2444
2848
  const { setAutoConfig } = require('./config');
2445
2849
  await setAutoConfig({ agent: selectedId, ide: selectedId });
2446
2850
  const def = defMap.get(selectedId);
@@ -2457,15 +2861,15 @@ async function showProviderManagerMenu() {
2457
2861
  resolve(null);
2458
2862
  };
2459
2863
 
2460
- const moveSelection = (delta) => {
2864
+ const moveSelection = async (delta) => {
2461
2865
  const next = selectedIndex + delta;
2462
2866
  if (next >= 0 && next < order.length) {
2463
2867
  selectedIndex = next;
2464
- render();
2868
+ await render();
2465
2869
  }
2466
2870
  };
2467
2871
 
2468
- const reorder = (delta) => {
2872
+ const reorder = async (delta) => {
2469
2873
  const target = selectedIndex + delta;
2470
2874
  if (target < 0 || target >= order.length) return;
2471
2875
  const temp = order[selectedIndex];
@@ -2473,17 +2877,17 @@ async function showProviderManagerMenu() {
2473
2877
  order[target] = temp;
2474
2878
  selectedIndex = target;
2475
2879
  dirty = true;
2476
- render();
2880
+ await render();
2477
2881
  };
2478
2882
 
2479
- const toggle = (value) => {
2883
+ const toggle = async (value) => {
2480
2884
  const id = order[selectedIndex];
2481
2885
  enabled[id] = value;
2482
2886
  dirty = true;
2483
- render();
2887
+ await render();
2484
2888
  };
2485
2889
 
2486
- const onKeypress = (str, key = {}) => {
2890
+ const onKeypress = async (str, key = {}) => {
2487
2891
  if (key.ctrl && key.name === 'c') {
2488
2892
  cancel();
2489
2893
  return;
@@ -2491,30 +2895,31 @@ async function showProviderManagerMenu() {
2491
2895
 
2492
2896
  switch (key.name) {
2493
2897
  case 'up':
2494
- moveSelection(-1);
2898
+ await moveSelection(-1);
2495
2899
  break;
2496
2900
  case 'down':
2497
- moveSelection(1);
2901
+ await moveSelection(1);
2498
2902
  break;
2499
2903
  case 'j':
2500
- reorder(1);
2904
+ await reorder(1);
2501
2905
  break;
2502
2906
  case 'k':
2503
- reorder(-1);
2907
+ await reorder(-1);
2504
2908
  break;
2505
2909
  case 'e':
2506
- toggle(true);
2910
+ await toggle(true);
2507
2911
  break;
2508
2912
  case 'd':
2509
- toggle(false);
2913
+ await toggle(false);
2510
2914
  break;
2511
2915
  case 'space':
2512
- toggle(!(enabled[order[selectedIndex]] !== false));
2916
+ await toggle(!(enabled[order[selectedIndex]] !== false));
2513
2917
  break;
2514
2918
  case 'return':
2515
2919
  saveAndExit(order[selectedIndex]);
2516
2920
  break;
2517
2921
  case 'escape':
2922
+ case 'left':
2518
2923
  case 'x':
2519
2924
  cancel();
2520
2925
  break;
@@ -2530,11 +2935,11 @@ async function showProviderManagerMenu() {
2530
2935
  process.stdin.on('keypress', onKeypress);
2531
2936
  process.stdin.resume();
2532
2937
 
2533
- render();
2938
+ await render();
2534
2939
  });
2535
2940
  }
2536
2941
 
2537
- async function showSettings() {
2942
+ /* async function showSettings() {
2538
2943
  console.log(chalk.bold.cyan('\nāš™ļø Settings\n'));
2539
2944
 
2540
2945
  const { setConfigValue } = require('vibecodingmachine-core');
@@ -2620,6 +3025,276 @@ async function showSettings() {
2620
3025
  }
2621
3026
  }
2622
3027
 
3028
+ /**
3029
+ * Show cloud sync management menu
3030
+ */
3031
+ async function showCloudSyncMenu() {
3032
+ console.clear();
3033
+ console.log(chalk.bold.cyan('\nā˜ļø Cloud Sync Management\n'));
3034
+
3035
+ // Check if cloud sync is configured
3036
+ try {
3037
+ const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
3038
+ const testEngine = new SyncEngine();
3039
+ await testEngine.initialize();
3040
+ testEngine.stop();
3041
+ } catch (error) {
3042
+ console.log(chalk.yellow('āš ļø Cloud sync is not configured.\n'));
3043
+ console.log(chalk.white('To set up cloud sync:\n'));
3044
+ console.log(chalk.gray('1. Run: ') + chalk.cyan('./scripts/setup-cloud-sync.sh'));
3045
+ console.log(chalk.gray('2. Add AWS configuration to your .env file'));
3046
+ console.log(chalk.gray('3. Restart vcm\n'));
3047
+ console.log(chalk.gray('For more info, see: ') + chalk.cyan('docs/CLOUD_SYNC.md\n'));
3048
+
3049
+ console.log(chalk.gray('Press Enter to return to main menu...'));
3050
+ await new Promise(resolve => {
3051
+ const rl = readline.createInterface({
3052
+ input: process.stdin,
3053
+ output: process.stdout
3054
+ });
3055
+ rl.question('', () => {
3056
+ rl.close();
3057
+ resolve();
3058
+ });
3059
+ });
3060
+ return;
3061
+ }
3062
+
3063
+ const computerCommands = require('../commands/computers');
3064
+ const syncCommands = require('../commands/sync');
3065
+
3066
+ const choices = [
3067
+ { name: 'šŸ“Š View All Computers', value: 'computers' },
3068
+ { name: 'šŸ–„ļø Manage Another Computer\'s Requirements', value: 'manage-remote' },
3069
+ { name: 'šŸ”„ Sync Now', value: 'sync-now' },
3070
+ { name: 'šŸ“ˆ Sync Status', value: 'sync-status' },
3071
+ { name: 'šŸ“œ Sync History', value: 'sync-history' },
3072
+ { name: 'šŸ“‹ View Offline Queue', value: 'sync-queue' },
3073
+ { name: 'šŸ–„ļø Register This Computer', value: 'register' },
3074
+ { name: 'šŸŽÆ Update Focus Area', value: 'update-focus' },
3075
+ { name: chalk.gray('← Back to Main Menu'), value: 'back' }
3076
+ ];
3077
+
3078
+ const { action } = await inquirer.prompt([
3079
+ {
3080
+ type: 'list',
3081
+ name: 'action',
3082
+ message: 'Select an option:',
3083
+ choices: choices
3084
+ }
3085
+ ]);
3086
+
3087
+ switch (action) {
3088
+ case 'computers':
3089
+ try {
3090
+ await computerCommands.listComputers();
3091
+ } catch (error) {
3092
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3093
+ console.log(chalk.gray('\nTip: Make sure AWS credentials are configured and DynamoDB tables exist.'));
3094
+ }
3095
+ console.log(chalk.gray('\nPress Enter to continue...'));
3096
+ await new Promise(resolve => {
3097
+ const rl = readline.createInterface({
3098
+ input: process.stdin,
3099
+ output: process.stdout
3100
+ });
3101
+ rl.question('', () => {
3102
+ rl.close();
3103
+ resolve();
3104
+ });
3105
+ });
3106
+ await showCloudSyncMenu();
3107
+ break;
3108
+
3109
+ case 'manage-remote':
3110
+ try {
3111
+ // First, get list of computers
3112
+ const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
3113
+ const { ScanCommand } = require('@aws-sdk/lib-dynamodb');
3114
+
3115
+ const syncEngine = new SyncEngine();
3116
+ await syncEngine.initialize();
3117
+
3118
+ const tableName = 'vibecodingmachine-computers';
3119
+ const command = new ScanCommand({ TableName: tableName });
3120
+ const response = await syncEngine.dynamoClient.send(command);
3121
+ const computers = response.Items || [];
3122
+
3123
+ syncEngine.stop();
3124
+
3125
+ if (computers.length === 0) {
3126
+ console.log(chalk.yellow('\n⚠ No computers registered yet.\n'));
3127
+ } else {
3128
+ // Let user select a computer
3129
+ const computerChoices = computers.map(c => ({
3130
+ name: `${c.hostname || c.computerId} - ${c.focusArea || 'No focus'}`,
3131
+ value: c.computerId
3132
+ }));
3133
+ computerChoices.push({ name: chalk.gray('← Cancel'), value: null });
3134
+
3135
+ const { selectedComputer } = await inquirer.prompt([
3136
+ {
3137
+ type: 'list',
3138
+ name: 'selectedComputer',
3139
+ message: 'Select computer to manage:',
3140
+ choices: computerChoices
3141
+ }
3142
+ ]);
3143
+
3144
+ if (selectedComputer) {
3145
+ const remoteReqCommands = require('../commands/requirements-remote');
3146
+ await remoteReqCommands.manageRemoteRequirements(selectedComputer);
3147
+ }
3148
+ }
3149
+ } catch (error) {
3150
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3151
+ }
3152
+ await showCloudSyncMenu();
3153
+ break;
3154
+
3155
+ case 'sync-now':
3156
+ try {
3157
+ await syncCommands.syncNow();
3158
+ } catch (error) {
3159
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3160
+ }
3161
+ console.log(chalk.gray('\nPress Enter to continue...'));
3162
+ await new Promise(resolve => {
3163
+ const rl = readline.createInterface({
3164
+ input: process.stdin,
3165
+ output: process.stdout
3166
+ });
3167
+ rl.question('', () => {
3168
+ rl.close();
3169
+ resolve();
3170
+ });
3171
+ });
3172
+ await showCloudSyncMenu();
3173
+ break;
3174
+
3175
+ case 'sync-status':
3176
+ try {
3177
+ await syncCommands.syncStatus();
3178
+ } catch (error) {
3179
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3180
+ }
3181
+ console.log(chalk.gray('\nPress Enter to continue...'));
3182
+ await new Promise(resolve => {
3183
+ const rl = readline.createInterface({
3184
+ input: process.stdin,
3185
+ output: process.stdout
3186
+ });
3187
+ rl.question('', () => {
3188
+ rl.close();
3189
+ resolve();
3190
+ });
3191
+ });
3192
+ await showCloudSyncMenu();
3193
+ break;
3194
+
3195
+ case 'sync-history':
3196
+ try {
3197
+ await syncCommands.viewHistory({ limit: 50 });
3198
+ } catch (error) {
3199
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3200
+ }
3201
+ console.log(chalk.gray('\nPress Enter to continue...'));
3202
+ await new Promise(resolve => {
3203
+ const rl = readline.createInterface({
3204
+ input: process.stdin,
3205
+ output: process.stdout
3206
+ });
3207
+ rl.question('', () => {
3208
+ rl.close();
3209
+ resolve();
3210
+ });
3211
+ });
3212
+ await showCloudSyncMenu();
3213
+ break;
3214
+
3215
+ case 'sync-queue':
3216
+ try {
3217
+ await syncCommands.viewQueue();
3218
+ } catch (error) {
3219
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3220
+ }
3221
+ console.log(chalk.gray('\nPress Enter to continue...'));
3222
+ await new Promise(resolve => {
3223
+ const rl = readline.createInterface({
3224
+ input: process.stdin,
3225
+ output: process.stdout
3226
+ });
3227
+ rl.question('', () => {
3228
+ rl.close();
3229
+ resolve();
3230
+ });
3231
+ });
3232
+ await showCloudSyncMenu();
3233
+ break;
3234
+
3235
+ case 'register':
3236
+ try {
3237
+ const { focusArea } = await inquirer.prompt([
3238
+ {
3239
+ type: 'input',
3240
+ name: 'focusArea',
3241
+ message: 'Enter focus area for this computer:',
3242
+ default: 'General Development'
3243
+ }
3244
+ ]);
3245
+ await computerCommands.registerComputer(focusArea);
3246
+ } catch (error) {
3247
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3248
+ }
3249
+ console.log(chalk.gray('\nPress Enter to continue...'));
3250
+ await new Promise(resolve => {
3251
+ const rl = readline.createInterface({
3252
+ input: process.stdin,
3253
+ output: process.stdout
3254
+ });
3255
+ rl.question('', () => {
3256
+ rl.close();
3257
+ resolve();
3258
+ });
3259
+ });
3260
+ await showCloudSyncMenu();
3261
+ break;
3262
+
3263
+ case 'update-focus':
3264
+ try {
3265
+ const { newFocus } = await inquirer.prompt([
3266
+ {
3267
+ type: 'input',
3268
+ name: 'newFocus',
3269
+ message: 'Enter new focus area:'
3270
+ }
3271
+ ]);
3272
+ if (newFocus) {
3273
+ await computerCommands.updateFocus(newFocus);
3274
+ }
3275
+ } catch (error) {
3276
+ console.log(chalk.red('\nāœ— Error: ') + error.message);
3277
+ }
3278
+ console.log(chalk.gray('\nPress Enter to continue...'));
3279
+ await new Promise(resolve => {
3280
+ const rl = readline.createInterface({
3281
+ input: process.stdin,
3282
+ output: process.stdout
3283
+ });
3284
+ rl.question('', () => {
3285
+ rl.close();
3286
+ resolve();
3287
+ });
3288
+ });
3289
+ await showCloudSyncMenu();
3290
+ break;
3291
+
3292
+ case 'back':
3293
+ // Return to main menu
3294
+ break;
3295
+ }
3296
+ }
3297
+
2623
3298
  async function startInteractive() {
2624
3299
  // STRICT AUTH CHECK (only if enabled)
2625
3300
  const authEnabled = process.env.AUTH_ENABLED === 'true';
@@ -2660,6 +3335,11 @@ async function startInteractive() {
2660
3335
 
2661
3336
  await showWelcomeScreen();
2662
3337
 
3338
+ if (process.env.VCM_OPEN_PROVIDER_MENU === '1' || process.env.VCM_OPEN_PROVIDER_MENU === 'true') {
3339
+ await showProviderManagerMenu();
3340
+ return;
3341
+ }
3342
+
2663
3343
  let exit = false;
2664
3344
  let lastSelectedIndex = 0; // Track last selected menu item
2665
3345
  while (!exit) {
@@ -2679,12 +3359,22 @@ async function startInteractive() {
2679
3359
  // Build dynamic menu items - settings at top (gray, no letters), actions below (with letters)
2680
3360
  const items = [];
2681
3361
 
2682
- // Get current agent (unified IDE + LLM)
2683
- const currentAgent = autoConfig.agent || autoConfig.ide || 'ollama';
2684
- let agentDisplay = `Current Agent: ${chalk.cyan(getAgentDisplayName(currentAgent))}`;
3362
+ // Get first ENABLED agent from provider preferences
3363
+ const { getProviderPreferences } = require('../utils/provider-registry');
3364
+ const prefs = await getProviderPreferences();
3365
+ let firstEnabledAgent = null;
3366
+ for (const agentId of prefs.order) {
3367
+ if (prefs.enabled[agentId] !== false) {
3368
+ firstEnabledAgent = agentId;
3369
+ break;
3370
+ }
3371
+ }
3372
+ // Fallback to current agent if no enabled agents found
3373
+ const displayAgent = firstEnabledAgent || autoConfig.agent || autoConfig.ide || 'ollama';
3374
+ let agentDisplay = `First Agent: ${chalk.cyan(getAgentDisplayName(displayAgent))}`;
2685
3375
 
2686
3376
  // Check for rate limits (for LLM-based agents and Claude Code)
2687
- if (currentAgent === 'ollama' || currentAgent === 'groq' || currentAgent === 'anthropic' || currentAgent === 'bedrock' || currentAgent === 'claude-code') {
3377
+ if (displayAgent === 'ollama' || displayAgent === 'groq' || displayAgent === 'anthropic' || displayAgent === 'bedrock' || displayAgent === 'claude-code') {
2688
3378
  try {
2689
3379
  const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
2690
3380
  const providerManager = new ProviderManager();
@@ -2699,44 +3389,47 @@ async function startInteractive() {
2699
3389
 
2700
3390
  // Get the model based on the current agent type
2701
3391
  let model;
2702
- if (currentAgent === 'groq') {
3392
+ if (displayAgent === 'groq') {
2703
3393
  model = config.auto?.groqModel || config.auto?.aiderModel || config.auto?.llmModel;
2704
3394
  // Remove groq/ prefix if present
2705
3395
  if (model && model.includes('groq/')) {
2706
3396
  model = model.split('/')[1];
2707
3397
  }
2708
- } else if (currentAgent === 'anthropic') {
3398
+ } else if (displayAgent === 'anthropic') {
2709
3399
  model = config.auto?.anthropicModel || config.auto?.aiderModel || config.auto?.llmModel;
2710
- } else if (currentAgent === 'ollama') {
3400
+ } else if (displayAgent === 'ollama') {
2711
3401
  const rawModel = config.auto?.llmModel || config.auto?.aiderModel;
2712
3402
  // Only use if it doesn't have groq/ prefix
2713
3403
  model = rawModel && !rawModel.includes('groq/') ? rawModel : null;
2714
- } else if (currentAgent === 'bedrock') {
3404
+ } else if (displayAgent === 'bedrock') {
2715
3405
  model = 'anthropic.claude-sonnet-4-v1';
2716
- } else if (currentAgent === 'claude-code') {
3406
+ } else if (displayAgent === 'claude-code') {
2717
3407
  model = 'claude-code-cli';
2718
3408
  }
2719
3409
 
2720
3410
  // For Claude Code, use fixed model name
2721
- const checkModel = currentAgent === 'claude-code' ? 'claude-code-cli' : model;
2722
- const provider = currentAgent === 'ollama' ? 'ollama' : currentAgent;
3411
+ const checkModel = displayAgent === 'claude-code' ? 'claude-code-cli' : model;
3412
+ const provider = displayAgent === 'ollama' ? 'ollama' : displayAgent;
2723
3413
 
2724
3414
  if (checkModel) {
2725
3415
  const timeUntilReset = providerManager.getTimeUntilReset(provider, checkModel);
2726
3416
 
2727
3417
  if (timeUntilReset) {
2728
- const resetTime = Date.now() + timeUntilReset;
2729
- const resetDate = new Date(resetTime);
2730
- const timeStr = resetDate.toLocaleString('en-US', {
2731
- weekday: 'short',
2732
- month: 'short',
2733
- day: 'numeric',
2734
- hour: 'numeric',
2735
- minute: '2-digit',
2736
- hour12: true,
2737
- timeZoneName: 'short'
2738
- });
2739
- agentDisplay += ` ${chalk.red('ā° Rate limited until ' + timeStr)}`;
3418
+ // Format time remaining in human-readable format
3419
+ const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
3420
+ const minutes = Math.floor((timeUntilReset % (1000 * 60 * 60)) / (1000 * 60));
3421
+ const seconds = Math.floor((timeUntilReset % (1000 * 60)) / 1000);
3422
+
3423
+ let timeStr = '';
3424
+ if (hours > 0) {
3425
+ timeStr = `${hours}h ${minutes}m`;
3426
+ } else if (minutes > 0) {
3427
+ timeStr = `${minutes}m ${seconds}s`;
3428
+ } else {
3429
+ timeStr = `${seconds}s`;
3430
+ }
3431
+
3432
+ agentDisplay += ` ${chalk.red('ā° Rate limit resets in ' + timeStr)}`;
2740
3433
  }
2741
3434
  }
2742
3435
  }
@@ -2754,9 +3447,6 @@ async function startInteractive() {
2754
3447
  autoConfig.maxChats ? `Stop after ${autoConfig.maxChats}` :
2755
3448
  'Never Stop';
2756
3449
 
2757
- // Get restart CLI setting from autoConfig
2758
- const restartCLI = autoConfig.restartCLI ? chalk.green('Enabled āœ“') : chalk.yellow('Disabled ā—‹');
2759
-
2760
3450
  if (autoStatus.running) {
2761
3451
  items.push({
2762
3452
  type: 'setting',
@@ -2778,45 +3468,27 @@ async function startInteractive() {
2778
3468
  value: 'setting:auto-stop-condition'
2779
3469
  });
2780
3470
 
2781
- // Add restart CLI setting
2782
- items.push({
2783
- type: 'setting',
2784
- name: ` └─ Restart CLI after each completed requirement: ${restartCLI}`,
2785
- value: 'setting:restart-cli'
2786
- });
2787
-
2788
- // Add setup alias setting
2789
- items.push({
2790
- type: 'setting',
2791
- name: ` └─ Setup 'vcm' alias`,
2792
- value: 'setting:setup-alias'
2793
- });
2794
-
2795
- // Add current agent setting
3471
+ // Add current agent setting (rate limit info already included in agentDisplay if applicable)
2796
3472
  items.push({
2797
3473
  type: 'setting',
2798
3474
  name: ` └─ ${agentDisplay}`,
2799
3475
  value: 'setting:agent'
2800
3476
  });
2801
3477
 
2802
-
2803
-
2804
3478
  // Add Requirements as a selectable setting with counts
2805
3479
  const hasRequirements = await requirementsExists();
2806
3480
  const counts = hasRequirements ? await countRequirements() : null;
2807
3481
  let requirementsText = 'Requirements: ';
2808
3482
  if (counts) {
3483
+ // Calculate actual iterations: lesser of stop after number and TODO requirements
3484
+ const actualIterations = autoConfig.neverStop ? counts.todoCount :
3485
+ Math.min(autoConfig.maxChats || counts.todoCount, counts.todoCount);
2809
3486
  const total = counts.todoCount + counts.toVerifyCount + counts.verifiedCount;
2810
3487
  if (total > 0) {
2811
- const todoPercent = Math.round((counts.todoCount / total) * 100);
3488
+ const todoPercent = Math.round((actualIterations / total) * 100);
2812
3489
  const toVerifyPercent = Math.round((counts.toVerifyCount / total) * 100);
2813
3490
  const verifiedPercent = Math.round((counts.verifiedCount / total) * 100);
2814
- requirementsText += `${chalk.yellow(counts.todoCount + ' (' + todoPercent + '%) TODO')}, ${chalk.cyan(counts.toVerifyCount + ' (' + toVerifyPercent + '%) TO VERIFY')}, ${chalk.green(counts.verifiedCount + ' (' + verifiedPercent + '%) VERIFIED')}`;
2815
-
2816
- // Add warning if no TODO requirements
2817
- if (counts.todoCount === 0) {
2818
- requirementsText += ` ${chalk.red('āš ļø No requirements to work on')}`;
2819
- }
3491
+ requirementsText += `${chalk.yellow(actualIterations + ' (' + todoPercent + '%) TODO')}, ${chalk.cyan(counts.toVerifyCount + ' (' + toVerifyPercent + '%) TO VERIFY')}, ${chalk.green(counts.verifiedCount + ' (' + verifiedPercent + '%) VERIFIED')}`;
2820
3492
  } else {
2821
3493
  requirementsText = '';
2822
3494
  }
@@ -2836,10 +3508,57 @@ async function startInteractive() {
2836
3508
 
2837
3509
  items.push({
2838
3510
  type: 'setting',
2839
- name: ` └─ Use Hostname in Req File: ${useHostname ? chalk.green('Enabled āœ“') : chalk.yellow('Disabled ā—‹')}`,
3511
+ name: ` └─ Use Hostname in Req File: ${useHostname ? chalk.green('āœ“') : chalk.red('šŸ›‘')} ${useHostname ? 'āœ“ ENABLED' : 'šŸ›‘ DISABLED'}`,
2840
3512
  value: 'setting:hostname'
2841
3513
  });
2842
3514
 
3515
+ // Add Stages configuration
3516
+ const { getStages } = require('./config');
3517
+ const configuredStages = await getStages();
3518
+ const stagesCount = configuredStages.length;
3519
+ items.push({
3520
+ type: 'setting',
3521
+ name: ` └─ Configure Stages: ${chalk.cyan(stagesCount + ' stages')}`,
3522
+ value: 'setting:stages'
3523
+ });
3524
+
3525
+ // Cloud Sync Status
3526
+ try {
3527
+ const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
3528
+ const syncEngine = new SyncEngine();
3529
+
3530
+ // Try to initialize, but don't fail if AWS not configured
3531
+ try {
3532
+ await syncEngine.initialize();
3533
+ const syncStatus = syncEngine.getStatus();
3534
+ syncEngine.stop();
3535
+
3536
+ const onlineIcon = syncStatus.isOnline ? chalk.green('ā—') : chalk.red('ā—');
3537
+ const onlineText = syncStatus.isOnline ? 'Online' : 'Offline';
3538
+ const queueText = syncStatus.queuedChanges > 0 ? chalk.yellow(` (${syncStatus.queuedChanges} queued)`) : '';
3539
+
3540
+ items.push({
3541
+ type: 'setting',
3542
+ name: `Cloud Sync: ${onlineIcon} ${onlineText}${queueText}`,
3543
+ value: 'setting:cloud-sync'
3544
+ });
3545
+ } catch (initError) {
3546
+ // Initialization failed - AWS not configured or credentials missing
3547
+ items.push({
3548
+ type: 'setting',
3549
+ name: `Cloud Sync: ${chalk.gray('ā— Not configured')}`,
3550
+ value: 'setting:cloud-sync-setup'
3551
+ });
3552
+ }
3553
+ } catch (error) {
3554
+ // Module not found or other error - show disabled
3555
+ items.push({
3556
+ type: 'setting',
3557
+ name: `Cloud Sync: ${chalk.gray('ā— Disabled')}`,
3558
+ value: 'setting:cloud-sync-setup'
3559
+ });
3560
+ }
3561
+
2843
3562
  // Add "Next TODO Requirement" as a separate menu item if there are TODO items
2844
3563
  if (counts && counts.todoCount > 0) {
2845
3564
  // Get the actual next requirement text (new header format)
@@ -2886,32 +3605,32 @@ async function startInteractive() {
2886
3605
  if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
2887
3606
  break;
2888
3607
  }
3608
+
2889
3609
  description += nextLine + '\n';
2890
3610
  }
2891
- nextReqText = title;
3611
+ nextReqText = title + '\n' + description;
2892
3612
  break;
2893
3613
  }
2894
3614
  }
2895
3615
  }
3616
+ items.push({
3617
+ type: 'info',
3618
+ name: ` └─ Next TODO Requirement: ${nextReqText}`,
3619
+ value: 'info:next-requirement'
3620
+ });
2896
3621
  }
2897
3622
  } catch (err) {
2898
- console.error('Error getting next requirement:', err.message);
3623
+ console.error('Error reading requirements file:', err.message);
2899
3624
  }
2900
-
2901
- // Add "Next TODO Requirement" to the menu
2902
- items.push({
2903
- type: 'info',
2904
- name: `Next TODO Requirement: ${nextReqText}`,
2905
- value: 'next-req'
2906
- });
2907
3625
  }
2908
3626
 
3627
+
2909
3628
  // Add warning message if no TODO requirements and Auto Mode is stopped
2910
3629
  if (counts && counts.todoCount === 0 && !autoStatus.running) {
2911
3630
  items.push({
2912
- type: 'setting',
3631
+ type: 'info',
2913
3632
  name: chalk.red(' āš ļø No requirements to work on - cannot start Auto Mode'),
2914
- value: 'setting:no-requirements'
3633
+ value: 'info:no-requirements'
2915
3634
  });
2916
3635
  }
2917
3636
 
@@ -2924,6 +3643,8 @@ async function startInteractive() {
2924
3643
  items.push({ type: 'action', name: 'Initialize repository (.vibecodingmachine)', value: 'repo:init' });
2925
3644
  }
2926
3645
 
3646
+ items.push({ type: 'action', name: 'View All Computers', value: 'computers:list' });
3647
+ items.push({ type: 'action', name: 'Sync Now', value: 'sync:now' });
2927
3648
  items.push({ type: 'action', name: 'Logout', value: 'logout' });
2928
3649
  items.push({ type: 'action', name: 'Exit', value: 'exit' });
2929
3650
 
@@ -2968,7 +3689,6 @@ async function startInteractive() {
2968
3689
  const path = require('path');
2969
3690
  const os = require('os');
2970
3691
  const yaml = require('js-yaml');
2971
- const { spawn } = require('child_process');
2972
3692
  const configPath = path.join(os.homedir(), '.continue', 'config.yaml');
2973
3693
 
2974
3694
  if (fs.existsSync(configPath)) {
@@ -3159,6 +3879,53 @@ async function startInteractive() {
3159
3879
  await showWelcomeScreen();
3160
3880
  break;
3161
3881
  }
3882
+ case 'setting:stages': {
3883
+ // Configure stages
3884
+ const { getStages, setStages, DEFAULT_STAGES } = require('./config');
3885
+ const inquirer = require('inquirer');
3886
+
3887
+ const currentStages = await getStages();
3888
+
3889
+ console.log(chalk.cyan('\nšŸ”Ø Configure Workflow Stages\n'));
3890
+ console.log(chalk.gray('Select the stages you want to include in the auto-mode workflow.'));
3891
+ console.log(chalk.gray('Stages will be executed in the order shown.\n'));
3892
+
3893
+ const { selectedStages } = await inquirer.prompt([
3894
+ {
3895
+ type: 'checkbox',
3896
+ name: 'selectedStages',
3897
+ message: 'Select stages:',
3898
+ choices: DEFAULT_STAGES.map(stage => ({
3899
+ name: stage,
3900
+ checked: currentStages.includes(stage)
3901
+ })),
3902
+ validate: (answer) => {
3903
+ if (answer.length < 1) {
3904
+ return 'You must select at least one stage.';
3905
+ }
3906
+ return true;
3907
+ },
3908
+ loop: false,
3909
+ pageSize: 15
3910
+ }
3911
+ ]);
3912
+
3913
+ // Preserve order from DEFAULT_STAGES for selected items
3914
+ // This ensures stages always run in the correct logical order
3915
+ const newStages = DEFAULT_STAGES.filter(stage => selectedStages.includes(stage));
3916
+
3917
+ await setStages(newStages);
3918
+ console.log(chalk.green('\nāœ“'), `Stages configuration updated: ${newStages.join(' → ')}\n`);
3919
+
3920
+ const { continue: _ } = await inquirer.prompt([{
3921
+ type: 'input',
3922
+ name: 'continue',
3923
+ message: 'Press Enter to return to menu...'
3924
+ }]);
3925
+
3926
+ await showWelcomeScreen();
3927
+ break;
3928
+ }
3162
3929
  case 'setting:auto-start': {
3163
3930
  try {
3164
3931
  console.log(chalk.bold.cyan('\nšŸš€ Starting Auto Mode...\n'));
@@ -3313,13 +4080,13 @@ async function startInteractive() {
3313
4080
  const { maxChats } = await inquirer.prompt([{
3314
4081
  type: 'input',
3315
4082
  name: 'maxChats',
3316
- message: 'Max chats (leave empty for never stop):',
4083
+ message: 'Max chats (0 for never stop):',
3317
4084
  default: defaultMaxChats
3318
4085
  }]);
3319
4086
 
3320
4087
  // Update config
3321
4088
  const newConfig = { ...currentConfig };
3322
- if (maxChats && maxChats.trim() !== '') {
4089
+ if (maxChats && maxChats.trim() !== '' && maxChats.trim() !== '0') {
3323
4090
  newConfig.maxChats = parseInt(maxChats);
3324
4091
  newConfig.neverStop = false;
3325
4092
  console.log(chalk.green('\nāœ“'), `Stop condition updated: ${chalk.cyan(`Stop after ${newConfig.maxChats}`)}\n`);
@@ -3336,43 +4103,82 @@ async function startInteractive() {
3336
4103
  await showWelcomeScreen();
3337
4104
  break;
3338
4105
  }
3339
- case 'setting:restart-cli': {
3340
- // Toggle restart CLI setting
3341
- try {
3342
- const { getAutoConfig, setAutoConfig } = require('./config');
3343
- const currentConfig = await getAutoConfig();
3344
- const newConfig = { ...currentConfig, restartCLI: !currentConfig.restartCLI };
3345
- await setAutoConfig(newConfig);
3346
- const statusText = newConfig.restartCLI ? chalk.green('enabled') : chalk.yellow('disabled');
3347
- console.log(chalk.green('\nāœ“'), `Restart CLI after each completed requirement ${statusText}\n`);
3348
- } catch (error) {
3349
- console.log(chalk.red('\nāœ— Error updating restart CLI setting:', error.message));
3350
- }
4106
+ case 'setting:requirements': {
4107
+ // Show tree-style requirements navigator
4108
+ await showRequirementsTree();
3351
4109
  await showWelcomeScreen();
3352
4110
  break;
3353
4111
  }
3354
- case 'setting:setup-alias': {
3355
- const { setupAlias } = require('../commands/setup');
3356
- await setupAlias();
3357
- console.log(chalk.gray('\nPress Enter to return to menu...'));
3358
- const inquirer = require('inquirer');
3359
- await inquirer.prompt([{
3360
- type: 'input',
3361
- name: 'continue',
3362
- message: ''
3363
- }]);
4112
+ case 'repo:init':
4113
+ await repo.initRepo();
4114
+ break;
4115
+
4116
+ case 'computers:list': {
4117
+ const computerCommands = require('../commands/computers');
4118
+ await computerCommands.listComputers();
4119
+ console.log(chalk.gray('\nPress Enter to continue...'));
4120
+ await new Promise(resolve => {
4121
+ const rl = readline.createInterface({
4122
+ input: process.stdin,
4123
+ output: process.stdout
4124
+ });
4125
+ rl.question('', () => {
4126
+ rl.close();
4127
+ resolve();
4128
+ });
4129
+ });
3364
4130
  await showWelcomeScreen();
3365
4131
  break;
3366
4132
  }
3367
- case 'setting:requirements': {
3368
- // Show tree-style requirements navigator
3369
- await showRequirementsTree();
4133
+
4134
+ case 'sync:now': {
4135
+ const syncCommands = require('../commands/sync');
4136
+ await syncCommands.syncNow();
4137
+ console.log(chalk.gray('\nPress Enter to continue...'));
4138
+ await new Promise(resolve => {
4139
+ const rl = readline.createInterface({
4140
+ input: process.stdin,
4141
+ output: process.stdout
4142
+ });
4143
+ rl.question('', () => {
4144
+ rl.close();
4145
+ resolve();
4146
+ });
4147
+ });
3370
4148
  await showWelcomeScreen();
3371
4149
  break;
3372
4150
  }
3373
- case 'repo:init':
3374
- await repo.initRepo();
4151
+
4152
+ case 'setting:cloud-sync': {
4153
+ await showCloudSyncMenu();
4154
+ await showWelcomeScreen();
3375
4155
  break;
4156
+ }
4157
+
4158
+ case 'setting:cloud-sync-setup': {
4159
+ console.clear();
4160
+ console.log(chalk.bold.cyan('\nā˜ļø Cloud Sync Setup\n'));
4161
+ console.log(chalk.yellow('Cloud sync is not configured yet.\n'));
4162
+ console.log(chalk.white('To set up cloud sync:\n'));
4163
+ console.log(chalk.gray('1. Run: ') + chalk.cyan('./scripts/setup-cloud-sync.sh'));
4164
+ console.log(chalk.gray('2. Add AWS configuration to your .env file'));
4165
+ console.log(chalk.gray('3. Register this computer with: ') + chalk.cyan('vcm computer:register "<focus>"'));
4166
+ console.log(chalk.gray('\nFor more info, see: ') + chalk.cyan('docs/CLOUD_SYNC.md\n'));
4167
+
4168
+ console.log(chalk.gray('Press Enter to continue...'));
4169
+ await new Promise(resolve => {
4170
+ const rl = readline.createInterface({
4171
+ input: process.stdin,
4172
+ output: process.stdout
4173
+ });
4174
+ rl.question('', () => {
4175
+ rl.close();
4176
+ resolve();
4177
+ });
4178
+ });
4179
+ await showWelcomeScreen();
4180
+ break;
4181
+ }
3376
4182
  case 'auto:start': {
3377
4183
  const { ide, maxChats } = await inquirer.prompt([
3378
4184
  {
@@ -3393,12 +4199,12 @@ async function startInteractive() {
3393
4199
  {
3394
4200
  type: 'input',
3395
4201
  name: 'maxChats',
3396
- message: 'Max chats (leave empty for never stop):',
4202
+ message: 'Max chats (0 for never stop):',
3397
4203
  default: ''
3398
4204
  }
3399
4205
  ]);
3400
4206
  const options = { ide };
3401
- if (maxChats) {
4207
+ if (maxChats && maxChats.trim() !== '0') {
3402
4208
  options.maxChats = parseInt(maxChats);
3403
4209
  } else {
3404
4210
  options.neverStop = true;