@ariacode/cli 0.2.2 → 0.2.3

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 (54) hide show
  1. package/README.md +60 -7
  2. package/dist/actions/db-ask.d.ts +2 -0
  3. package/dist/actions/db-ask.js +23 -6
  4. package/dist/actions/db-ask.js.map +1 -1
  5. package/dist/actions/db-explain.d.ts +2 -0
  6. package/dist/actions/db-explain.js +23 -5
  7. package/dist/actions/db-explain.js.map +1 -1
  8. package/dist/actions/db-schema.d.ts +2 -0
  9. package/dist/actions/db-schema.js +15 -2
  10. package/dist/actions/db-schema.js.map +1 -1
  11. package/dist/actions/upgrade-deps.d.ts +2 -0
  12. package/dist/actions/upgrade-deps.js +18 -0
  13. package/dist/actions/upgrade-deps.js.map +1 -1
  14. package/dist/actions.d.ts +86 -71
  15. package/dist/actions.js +382 -182
  16. package/dist/actions.js.map +1 -1
  17. package/dist/agent.d.ts +0 -1
  18. package/dist/agent.js +1 -2
  19. package/dist/agent.js.map +1 -1
  20. package/dist/app.d.ts +3 -3
  21. package/dist/app.js +3 -3
  22. package/dist/cli.d.ts +0 -1
  23. package/dist/cli.js +15 -3
  24. package/dist/cli.js.map +1 -1
  25. package/dist/config.d.ts +1 -1
  26. package/dist/config.js +1 -1
  27. package/dist/output/schemas.d.ts +92 -0
  28. package/dist/output/schemas.js +142 -0
  29. package/dist/output/schemas.js.map +1 -0
  30. package/dist/parser.d.ts +8 -3
  31. package/dist/parser.js +51 -5
  32. package/dist/parser.js.map +1 -1
  33. package/dist/repo.d.ts +0 -1
  34. package/dist/repo.js +6 -9
  35. package/dist/repo.js.map +1 -1
  36. package/dist/safety.d.ts +0 -4
  37. package/dist/safety.js +0 -4
  38. package/dist/safety.js.map +1 -1
  39. package/dist/storage/queries.d.ts +39 -0
  40. package/dist/storage/queries.js +211 -0
  41. package/dist/storage/queries.js.map +1 -0
  42. package/dist/tools.d.ts +6 -7
  43. package/dist/tools.js +20 -7
  44. package/dist/tools.js.map +1 -1
  45. package/dist/ui/diff-renderer.d.ts +28 -0
  46. package/dist/ui/diff-renderer.js +388 -0
  47. package/dist/ui/diff-renderer.js.map +1 -0
  48. package/dist/ui/highlight.d.ts +25 -0
  49. package/dist/ui/highlight.js +239 -0
  50. package/dist/ui/highlight.js.map +1 -0
  51. package/dist/ui.d.ts +3 -16
  52. package/dist/ui.js +7 -28
  53. package/dist/ui.js.map +1 -1
  54. package/package.json +1 -1
package/dist/actions.js CHANGED
@@ -19,12 +19,15 @@ import { getConfig } from "./config.js";
19
19
  import { detectProjectType } from "./repo.js";
20
20
  import { createProvider, ProviderError } from "./provider.js";
21
21
  import { initializeDatabase, createSession, updateSessionStatus, getSession, logMessage, listSessions, } from "./storage.js";
22
- import { readFileTool, listDirectoryTool, searchCodeTool, readPackageJsonTool, readPrismaSchemaTool, proposeDiffTool, applyDiffTool, } from "./tools.js";
22
+ import { searchSessions, filterSessions, exportSessionMarkdown, } from "./storage/queries.js";
23
+ import { readFileTool, listDirectoryTool, searchCodeTool, readPackageJsonTool, readPrismaSchemaTool, proposeDiffTool, applyDiffTool, setDiffRenderOptions, } from "./tools.js";
23
24
  import { agentLoop, UserCancelledError } from "./agent.js";
24
25
  import prompts from "prompts";
25
- import { initUI, info, print, error as uiError, bold, yellow, green, dim, cyan, red, renderTable, generateAndRenderDiff, confirm, ConfirmCancelledError, } from "./ui.js";
26
+ import { initUI, info, print, error as uiError, bold, yellow, green, dim, cyan, red, stripAnsi, renderTable, generateAndRenderDiff, confirm, ConfirmCancelledError, } from "./ui.js";
27
+ import { renderDiff as renderDiffEnhanced } from "./ui/diff-renderer.js";
28
+ import { formatOutput, AskOutputSchema, PlanOutputSchema, ReviewOutputSchema, ExploreOutputSchema, HistoryOutputSchema, DoctorOutputSchema, } from "./output/schemas.js";
26
29
  import { loadConfig, validateConfig } from "./config.js";
27
- import { getShellRcPath } from "./fs-helpers.js";
30
+ import { getShellRcPath, writeFileAtomic } from "./fs-helpers.js";
28
31
  const __dirname = dirname(fileURLToPath(import.meta.url));
29
32
  // ---------------------------------------------------------------------------
30
33
  // Provider resolution with interactive setup
@@ -212,7 +215,6 @@ function saveProviderChoice(config) {
212
215
  // ---------------------------------------------------------------------------
213
216
  /**
214
217
  * The five read-only tools exposed to the ask command.
215
- * Requirements: 9.4
216
218
  */
217
219
  const READ_ONLY_TOOLS = [
218
220
  readFileTool,
@@ -340,19 +342,18 @@ function buildSystemPrompt(templateName, ctx, extraVars = {}) {
340
342
  * Execute the ask command.
341
343
  *
342
344
  * Flow:
343
- * 1. Load configuration and detect project type (Req 9.1)
344
- * 2. Create or resume session with mode: "plan" (Req 9.2, 9.9)
345
- * 3. Build system prompt from ask.md template (Req 9.3)
346
- * 4. Expose only read-only tools (Req 9.4)
347
- * 5. Execute agent loop (Req 9.5, 9.6)
348
- * 6. Render response to terminal (Req 9.7)
349
- * 7. Persist session to database (Req 9.8)
345
+ * 1. Load configuration and detect project type
346
+ * 2. Create or resume session with mode: "plan"
347
+ * 3. Build system prompt from ask.md template
348
+ * 4. Expose only read-only tools
349
+ * 5. Execute agent loop
350
+ * 6. Render response to terminal
351
+ * 7. Persist session to database
350
352
  *
351
- * Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9
352
353
  */
353
354
  export async function runAsk(options) {
354
355
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
355
- // 1. Load configuration (Req 9.1)
356
+ // 1. Load configuration
356
357
  const config = getConfig(projectRoot, {
357
358
  quiet: options.quiet,
358
359
  maxTokens: options.maxTokens,
@@ -360,15 +361,15 @@ export async function runAsk(options) {
360
361
  model: options.model,
361
362
  });
362
363
  // Initialize UI with config settings
363
- initUI(config.ui.color, config.ui.quiet);
364
- // Detect project type early to fail fast if package.json is missing (Req 9.1)
364
+ initUI(config.ui.color, config.ui.quiet || (options.format === 'json' || options.format === 'ndjson'));
365
+ // Detect project type early to fail fast if package.json is missing
365
366
  detectProjectType(projectRoot);
366
- // 2. Initialize database and create/resume session (Req 9.2, 9.8, 9.9)
367
+ // 2. Initialize database and create/resume session
367
368
  const db = initializeDatabase();
368
369
  let sessionId;
369
370
  let resumedMessages = [];
370
371
  if (options.session) {
371
- // Resume existing session (Req 9.9)
372
+ // Resume existing session
372
373
  const existing = getSession(db, options.session);
373
374
  if (!existing) {
374
375
  uiError(`Session not found: ${options.session}`);
@@ -383,7 +384,7 @@ export async function runAsk(options) {
383
384
  resumedMessages = rows;
384
385
  }
385
386
  else {
386
- // Create new session (Req 9.2)
387
+ // Create new session
387
388
  sessionId = randomUUID();
388
389
  createSession(db, {
389
390
  id: sessionId,
@@ -408,7 +409,7 @@ export async function runAsk(options) {
408
409
  updateSessionStatus(db, sessionId, "failed", String(err));
409
410
  process.exit(4);
410
411
  }
411
- // 4. Build execution context with mode: "plan" (Req 9.2)
412
+ // 4. Build execution context with mode: "plan"
412
413
  const ctx = {
413
414
  projectRoot,
414
415
  sessionId,
@@ -424,7 +425,7 @@ export async function runAsk(options) {
424
425
  if (options.maxTokens !== undefined) {
425
426
  config.provider.maxTokens = options.maxTokens;
426
427
  }
427
- // 5. Build system prompt from ask.md template (Req 9.3)
428
+ // 5. Build system prompt from ask.md template
428
429
  const systemPrompt = buildSystemPrompt("ask", ctx);
429
430
  // Log system prompt as a system message
430
431
  logMessage(db, sessionId, "system", systemPrompt);
@@ -441,15 +442,34 @@ export async function runAsk(options) {
441
442
  .join("\n\n");
442
443
  userRequest = `[Resumed session context]\n${priorContext}\n\n[New question]: ${options.question}`;
443
444
  }
444
- // 6. Execute agent loop (Req 9.5, 9.6)
445
- // agentLoop streams the response to stdout as it arrives (Req 9.7)
445
+ // 6. Execute agent loop
446
+ // agentLoop streams the response to stdout as it arrives
446
447
  try {
447
- await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "ask", db, systemPrompt);
448
- // 7. Mark session as completed (Req 9.8)
448
+ const answer = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "ask", db, systemPrompt);
449
+ // v0.2.3: structured output
450
+ if (options.format === 'json' || options.format === 'ndjson') {
451
+ process.stdout.write(formatOutput({ version: '1', answer, sessionId }, options.format, AskOutputSchema));
452
+ }
453
+ else if (options.format === 'plain') {
454
+ // Strip ANSI codes and write plain text
455
+ process.stdout.write(stripAnsi(answer) + '\n');
456
+ }
457
+ // 7. Mark session as completed
449
458
  updateSessionStatus(db, sessionId, "completed");
450
459
  }
451
460
  catch (err) {
452
461
  const message = err instanceof Error ? err.message : String(err);
462
+ // v0.2.3: structured error output
463
+ if (options.format === 'json') {
464
+ process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
465
+ updateSessionStatus(db, sessionId, 'failed', message);
466
+ process.exit(1);
467
+ }
468
+ else if (options.format === 'ndjson') {
469
+ process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
470
+ updateSessionStatus(db, sessionId, 'failed', message);
471
+ process.exit(1);
472
+ }
453
473
  uiError(message);
454
474
  updateSessionStatus(db, sessionId, "failed", message);
455
475
  process.exit(1);
@@ -459,35 +479,34 @@ export async function runAsk(options) {
459
479
  * Execute the plan command.
460
480
  *
461
481
  * Flow:
462
- * 1. Load configuration and detect project type (Req 10.1)
463
- * 2. Create or resume session with mode: "plan" (Req 10.2, 10.8)
464
- * 3. Build system prompt from plan.md template (Req 10.3)
465
- * 4. Expose only read-only tools (Req 10.4)
466
- * 5. Execute agent loop (Req 10.5)
467
- * 6. Render structured plan to terminal (Req 10.6)
468
- * 7. Save to file if --output flag provided (Req 10.7)
469
- * 8. Persist session to database (Req 10.8)
482
+ * 1. Load configuration and detect project type
483
+ * 2. Create or resume session with mode: "plan"
484
+ * 3. Build system prompt from plan.md template
485
+ * 4. Expose only read-only tools
486
+ * 5. Execute agent loop
487
+ * 6. Render structured plan to terminal
488
+ * 7. Save to file if --output flag provided
489
+ * 8. Persist session to database
470
490
  *
471
- * Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8
472
491
  */
473
492
  export async function runPlan(options) {
474
493
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
475
- // 1. Load configuration (Req 10.1)
494
+ // 1. Load configuration
476
495
  const config = getConfig(projectRoot, {
477
496
  quiet: options.quiet,
478
497
  provider: options.provider,
479
498
  model: options.model,
480
499
  });
481
500
  // Initialize UI with config settings
482
- initUI(config.ui.color, config.ui.quiet);
483
- // Detect project type early to fail fast if package.json is missing (Req 10.1)
501
+ initUI(config.ui.color, config.ui.quiet || (options.format === 'json' || options.format === 'ndjson'));
502
+ // Detect project type early to fail fast if package.json is missing
484
503
  detectProjectType(projectRoot);
485
- // 2. Initialize database and create/resume session (Req 10.2, 10.8)
504
+ // 2. Initialize database and create/resume session
486
505
  const db = initializeDatabase();
487
506
  let sessionId;
488
507
  let resumedMessages = [];
489
508
  if (options.session) {
490
- // Resume existing session (Req 10.8)
509
+ // Resume existing session
491
510
  const existing = getSession(db, options.session);
492
511
  if (!existing) {
493
512
  uiError(`Session not found: ${options.session}`);
@@ -502,7 +521,7 @@ export async function runPlan(options) {
502
521
  resumedMessages = rows;
503
522
  }
504
523
  else {
505
- // Create new session (Req 10.2)
524
+ // Create new session
506
525
  sessionId = randomUUID();
507
526
  createSession(db, {
508
527
  id: sessionId,
@@ -527,7 +546,7 @@ export async function runPlan(options) {
527
546
  updateSessionStatus(db, sessionId, "failed", String(err));
528
547
  process.exit(4);
529
548
  }
530
- // 4. Build execution context with mode: "plan" (Req 10.2)
549
+ // 4. Build execution context with mode: "plan"
531
550
  const ctx = {
532
551
  projectRoot,
533
552
  sessionId,
@@ -539,7 +558,7 @@ export async function runPlan(options) {
539
558
  maxIterations: config.agent.maxIterations,
540
559
  timeoutSeconds: config.agent.timeoutSeconds,
541
560
  };
542
- // 5. Build system prompt from plan.md template (Req 10.3)
561
+ // 5. Build system prompt from plan.md template
543
562
  const systemPrompt = buildSystemPrompt("plan", ctx, { userGoal: options.goal });
544
563
  // Log system prompt as a system message
545
564
  logMessage(db, sessionId, "system", systemPrompt);
@@ -553,17 +572,21 @@ export async function runPlan(options) {
553
572
  .join("\n\n");
554
573
  userRequest = `[Resumed session context]\n${priorContext}\n\n[New goal]: ${options.goal}`;
555
574
  }
556
- // 6. Execute agent loop — streams response to stdout (Req 10.5, 10.6)
575
+ // 6. Execute agent loop — streams response to stdout
557
576
  try {
558
577
  const planContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "plan", db, systemPrompt);
559
- // 7. Save to file if --output flag provided (Req 10.7)
578
+ // v0.2.3: structured output
579
+ if (options.format === 'json' || options.format === 'ndjson') {
580
+ process.stdout.write(formatOutput({ version: '1', plan: planContent, sessionId, outputPath: options.output }, options.format, PlanOutputSchema));
581
+ }
582
+ // 7. Save to file if --output flag provided
560
583
  if (options.output) {
561
584
  const outputPath = path.resolve(options.output);
562
585
  const outputDir = path.dirname(outputPath);
563
586
  if (!existsSync(outputDir)) {
564
587
  mkdirSync(outputDir, { recursive: true });
565
588
  }
566
- writeFileSync(outputPath, planContent, "utf-8");
589
+ writeFileAtomic(outputPath, planContent);
567
590
  info(`Plan saved to ${options.output}`);
568
591
  }
569
592
  // 8. Mark session as completed (Req 10.8)
@@ -571,6 +594,17 @@ export async function runPlan(options) {
571
594
  }
572
595
  catch (err) {
573
596
  const message = err instanceof Error ? err.message : String(err);
597
+ // v0.2.3: structured error output
598
+ if (options.format === 'json') {
599
+ process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
600
+ updateSessionStatus(db, sessionId, 'failed', message);
601
+ process.exit(1);
602
+ }
603
+ else if (options.format === 'ndjson') {
604
+ process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
605
+ updateSessionStatus(db, sessionId, 'failed', message);
606
+ process.exit(1);
607
+ }
574
608
  uiError(message);
575
609
  updateSessionStatus(db, sessionId, "failed", message);
576
610
  process.exit(1);
@@ -580,25 +614,24 @@ export async function runPlan(options) {
580
614
  * Execute the patch command.
581
615
  *
582
616
  * Flow:
583
- * 1. Load configuration and detect project type (Req 11.1)
584
- * 2. Create session with mode: "build" (Req 11.2)
585
- * 3. Build system prompt from patch.md template (Req 11.3)
586
- * 4. Expose read-only + mutation tools (Req 11.4)
587
- * 5. Execute agent loop — agent calls propose_diff (Req 11.4, 11.5)
588
- * 6. Render diff preview with syntax highlighting (Req 11.6)
589
- * 7. Render mutation summary (Req 11.7)
590
- * 8. If --dry-run, exit with code 0 (Req 11.8, 17.3, 17.4)
591
- * 9. If not --yes, prompt for confirmation (Req 11.9, 17.5)
592
- * 10. Agent calls apply_diff atomically (Req 11.10, 11.11, 11.12)
593
- * 11. Log mutation to database (Req 11.11)
594
- * 12. Display rollback hints (Req 11.12, 17.9)
595
- * 13. Persist session to database (Req 11.13)
617
+ * 1. Load configuration and detect project type
618
+ * 2. Create session with mode: "build"
619
+ * 3. Build system prompt from patch.md template
620
+ * 4. Expose read-only + mutation tools
621
+ * 5. Execute agent loop — agent calls propose_diff
622
+ * 6. Render diff preview with syntax highlighting
623
+ * 7. Render mutation summary
624
+ * 8. If --dry-run, exit with code 0
625
+ * 9. If not --yes, prompt for confirmation
626
+ * 10. Agent calls apply_diff atomically
627
+ * 11. Log mutation to database
628
+ * 12. Display rollback hints
629
+ * 13. Persist session to database
596
630
  *
597
- * Requirements: 11.1–11.13, 17.1–17.9
598
631
  */
599
632
  export async function runPatch(options) {
600
633
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
601
- // 1. Load configuration (Req 11.1)
634
+ // 1. Load configuration
602
635
  const config = getConfig(projectRoot, {
603
636
  quiet: options.quiet,
604
637
  provider: options.provider,
@@ -609,9 +642,14 @@ export async function runPatch(options) {
609
642
  config.agent.mode = "build"; // keep build mode, dryRun handled via ctx
610
643
  // Initialize UI with config settings
611
644
  initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
612
- // Detect project type early to fail fast (Req 11.1)
645
+ // Plain format guard: require --yes for mutation commands
646
+ if (options.format === 'plain' && !options.yes) {
647
+ process.stdout.write('plain format requires --yes for mutation commands\n');
648
+ process.exit(2);
649
+ }
650
+ // Detect project type early to fail fast
613
651
  detectProjectType(projectRoot);
614
- // 2. Initialize database and create session (Req 11.13)
652
+ // 2. Initialize database and create session
615
653
  const db = initializeDatabase();
616
654
  let sessionId;
617
655
  if (options.session) {
@@ -648,7 +686,7 @@ export async function runPatch(options) {
648
686
  updateSessionStatus(db, sessionId, "failed", String(err));
649
687
  process.exit(4);
650
688
  }
651
- // 4. Build execution context with mode: "build" (Req 11.2, 17.1, 17.2)
689
+ // 4. Build execution context with mode: "build"
652
690
  const ctx = {
653
691
  projectRoot,
654
692
  sessionId,
@@ -660,10 +698,10 @@ export async function runPatch(options) {
660
698
  maxIterations: config.agent.maxIterations,
661
699
  timeoutSeconds: config.agent.timeoutSeconds,
662
700
  };
663
- // 5. Build system prompt from patch.md template (Req 11.3)
701
+ // 5. Build system prompt from patch.md template
664
702
  const systemPrompt = buildSystemPrompt("patch", ctx);
665
703
  logMessage(db, sessionId, "system", systemPrompt);
666
- // Expose read-only tools + mutation tools (Req 11.4)
704
+ // Expose read-only tools + mutation tools
667
705
  const patchTools = [
668
706
  readFileTool,
669
707
  listDirectoryTool,
@@ -676,15 +714,25 @@ export async function runPatch(options) {
676
714
  if (options.dryRun) {
677
715
  info(bold("Dry-run mode — changes will be previewed but not applied."));
678
716
  }
679
- // 6. Execute agent loop (Req 11.4, 11.5, 11.6, 11.7, 11.8, 11.9, 11.10)
717
+ // Configure enhanced diff renderer
718
+ const terminalWidth = process.stdout.columns ?? 80;
719
+ const diffRenderOptions = {
720
+ split: options.split ?? false,
721
+ lineNumbers: options.format !== 'plain',
722
+ collapseThreshold: 5,
723
+ terminalWidth,
724
+ ...(options.format === 'plain' ? { language: undefined } : {}),
725
+ };
726
+ setDiffRenderOptions((diffText) => renderDiffEnhanced(diffText, diffRenderOptions));
727
+ // 6. Execute agent loop
680
728
  // The agent loop handles:
681
- // - propose_diff: generates diff + MutationSummary (Req 11.4, 11.5)
682
- // - dry-run enforcement: skips apply_diff (Req 11.8, 17.3, 17.4)
683
- // - confirmation prompt before apply_diff (Req 11.9, 17.5, 17.6)
684
- // - atomic application via apply_diff (Req 11.10, 11.11, 11.12)
729
+ // - propose_diff: generates diff + MutationSummary
730
+ // - dry-run enforcement: skips apply_diff
731
+ // - confirmation prompt before apply_diff
732
+ // - atomic application via apply_diff
685
733
  try {
686
734
  await agentLoop(ctx, options.description, patchTools, provider, config, "patch", db, systemPrompt);
687
- // 12. Display rollback hints after successful application (Req 11.12, 17.9)
735
+ // 12. Display rollback hints after successful application
688
736
  // The agent loop streams the response which includes rollback hints from
689
737
  // the apply_diff result. We add a final summary line here.
690
738
  if (!options.dryRun) {
@@ -696,22 +744,37 @@ export async function runPatch(options) {
696
744
  info("");
697
745
  info(yellow("Dry-run complete — no files were modified."));
698
746
  }
699
- // 13. Mark session as completed (Req 11.13)
747
+ // 13. Mark session as completed
700
748
  updateSessionStatus(db, sessionId, "completed");
701
749
  }
702
750
  catch (err) {
703
751
  const message = err instanceof Error ? err.message : String(err);
704
- // Handle user cancellation (Req 17.6, exit code 130)
752
+ // Handle user cancellation
705
753
  if (err instanceof UserCancelledError || err instanceof ConfirmCancelledError) {
706
754
  info("");
707
755
  info(yellow("Operation cancelled."));
708
756
  updateSessionStatus(db, sessionId, "cancelled");
709
757
  process.exit(130);
710
758
  }
759
+ // v0.2.3: structured error output
760
+ if (options.format === 'json') {
761
+ process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
762
+ updateSessionStatus(db, sessionId, "failed", message);
763
+ process.exit(1);
764
+ }
765
+ else if (options.format === 'ndjson') {
766
+ process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
767
+ updateSessionStatus(db, sessionId, "failed", message);
768
+ process.exit(1);
769
+ }
711
770
  uiError(message);
712
771
  updateSessionStatus(db, sessionId, "failed", message);
713
772
  process.exit(1);
714
773
  }
774
+ finally {
775
+ // Clear diff render options after patch completes
776
+ setDiffRenderOptions(null);
777
+ }
715
778
  }
716
779
  /**
717
780
  * Read git diff based on the provided options.
@@ -720,21 +783,20 @@ export async function runPatch(options) {
720
783
  * - --unstaged: unstaged changes (`git diff`)
721
784
  * - --branch <base>: compare to base branch (`git diff <base>...HEAD`)
722
785
  *
723
- * Requirements: 12.3, 12.4, 12.5
724
786
  */
725
787
  function readGitDiff(options, projectRoot) {
726
788
  try {
727
789
  let args;
728
790
  if (options.branch) {
729
- // Compare current branch to specified base (Req 12.5)
791
+ // Compare current branch to specified base
730
792
  args = ["diff", `${options.branch}...HEAD`];
731
793
  }
732
794
  else if (options.unstaged) {
733
- // Unstaged changes (Req 12.4)
795
+ // Unstaged changes
734
796
  args = ["diff"];
735
797
  }
736
798
  else {
737
- // Staged changes — default (Req 12.3)
799
+ // Staged changes — default
738
800
  args = ["diff", "--cached"];
739
801
  }
740
802
  const output = execFileSync("git", args, {
@@ -754,7 +816,6 @@ function readGitDiff(options, projectRoot) {
754
816
  * Extracts summary, issues (with severity), and suggestions from the
755
817
  * "# Code Review" markdown format defined in review.md.
756
818
  *
757
- * Requirements: 12.7
758
819
  */
759
820
  function parseReviewResponse(content) {
760
821
  const result = {
@@ -805,7 +866,6 @@ function parseReviewResponse(content) {
805
866
  /**
806
867
  * Render the structured review to the terminal in readable format.
807
868
  *
808
- * Requirements: 12.8
809
869
  */
810
870
  function renderReview(review) {
811
871
  info("");
@@ -844,32 +904,31 @@ function renderReview(review) {
844
904
  * Execute the review command.
845
905
  *
846
906
  * Flow:
847
- * 1. Parse flags and load configuration (Req 12.1)
848
- * 2. Detect project type (Req 12.1)
849
- * 3. Create session with mode: "plan" (Req 12.2)
850
- * 4. Read git diff (staged / unstaged / branch) (Req 12.3, 12.4, 12.5)
851
- * 5. Build system prompt from review.md template (Req 12.6)
852
- * 6. Send diff + project context to provider (Req 12.6)
853
- * 7. Parse structured review (summary, issues, suggestions) (Req 12.7)
854
- * 8. Render review to terminal (Req 12.8)
855
- * 9. Output JSON if --format json (Req 12.9)
856
- * 10. Persist session to database (Req 12.10)
907
+ * 1. Parse flags and load configuration
908
+ * 2. Detect project type
909
+ * 3. Create session with mode: "plan"
910
+ * 4. Read git diff (staged / unstaged / branch)
911
+ * 5. Build system prompt from review.md template
912
+ * 6. Send diff + project context to provider
913
+ * 7. Parse structured review (summary, issues, suggestions)
914
+ * 8. Render review to terminal
915
+ * 9. Output JSON if --format json
916
+ * 10. Persist session to database
857
917
  *
858
- * Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9, 12.10
859
918
  */
860
919
  export async function runReview(options) {
861
920
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
862
- // 1. Load configuration (Req 12.1)
921
+ // 1. Load configuration
863
922
  const config = getConfig(projectRoot, {
864
923
  quiet: options.quiet,
865
924
  provider: options.provider,
866
925
  model: options.model,
867
926
  });
868
927
  // Initialize UI with config settings
869
- initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
870
- // 2. Detect project type early to fail fast (Req 12.1)
928
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet) || (options.format === 'json' || options.format === 'ndjson'));
929
+ // 2. Detect project type early to fail fast
871
930
  detectProjectType(projectRoot);
872
- // 3. Initialize database and create session (Req 12.10)
931
+ // 3. Initialize database and create session
873
932
  const db = initializeDatabase();
874
933
  const sessionId = randomUUID();
875
934
  createSession(db, {
@@ -894,7 +953,7 @@ export async function runReview(options) {
894
953
  updateSessionStatus(db, sessionId, "failed", String(err));
895
954
  process.exit(4);
896
955
  }
897
- // Build execution context with mode: "plan" (Req 12.2)
956
+ // Build execution context with mode: "plan"
898
957
  const ctx = {
899
958
  projectRoot,
900
959
  sessionId,
@@ -906,7 +965,7 @@ export async function runReview(options) {
906
965
  maxIterations: config.agent.maxIterations,
907
966
  timeoutSeconds: config.agent.timeoutSeconds,
908
967
  };
909
- // 4. Read git diff (Req 12.3, 12.4, 12.5)
968
+ // 4. Read git diff
910
969
  let diff;
911
970
  try {
912
971
  diff = readGitDiff(options, projectRoot);
@@ -927,10 +986,10 @@ export async function runReview(options) {
927
986
  updateSessionStatus(db, sessionId, "completed");
928
987
  return;
929
988
  }
930
- // 5. Build system prompt from review.md template (Req 12.6)
989
+ // 5. Build system prompt from review.md template
931
990
  const systemPrompt = buildSystemPrompt("review", ctx);
932
991
  logMessage(db, sessionId, "system", systemPrompt);
933
- // 6. Build user request: diff + project context (Req 12.6)
992
+ // 6. Build user request: diff + project context
934
993
  const project = detectProjectType(projectRoot);
935
994
  const diffSource = options.branch
936
995
  ? `branch diff (current vs ${options.branch})`
@@ -950,23 +1009,28 @@ export async function runReview(options) {
950
1009
  ]
951
1010
  .filter((l) => l !== null)
952
1011
  .join("\n");
953
- // Execute agent loop — streams response to stdout (Req 12.6, 12.7, 12.8)
1012
+ // Execute agent loop — streams response to stdout
954
1013
  try {
955
1014
  const reviewContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "review", db, systemPrompt);
956
- // 7. Parse structured review (Req 12.7)
1015
+ // 7. Parse structured review
957
1016
  const review = parseReviewResponse(reviewContent);
958
- // 8 & 9. Render or output JSON (Req 12.8, 12.9)
959
- if (options.format === "json") {
960
- // JSON output to stdout (Req 12.9)
961
- process.stdout.write(JSON.stringify(review, null, 2) + "\n");
1017
+ // 8 & 9. Render or output
1018
+ if (options.format === 'json' || options.format === 'ndjson') {
1019
+ // v0.2.3: structured output via formatOutput
1020
+ process.stdout.write(formatOutput({ version: '1', review: reviewContent, sessionId, branch: options.branch }, options.format, ReviewOutputSchema));
1021
+ }
1022
+ else if (options.format === 'plain') {
1023
+ // Strip ANSI codes for plain output
1024
+ const plainReview = stripAnsi(reviewContent);
1025
+ process.stdout.write(plainReview + '\n');
962
1026
  }
963
1027
  else {
964
- // Render to terminal in readable format (Req 12.8)
1028
+ // Render to terminal in readable format
965
1029
  // Note: agentLoop already streamed the raw response; renderReview
966
1030
  // provides a structured re-render for clarity.
967
1031
  renderReview(review);
968
1032
  }
969
- // 10. Mark session as completed (Req 12.10)
1033
+ // 10. Mark session as completed
970
1034
  updateSessionStatus(db, sessionId, "completed");
971
1035
  }
972
1036
  catch (err) {
@@ -978,6 +1042,17 @@ export async function runReview(options) {
978
1042
  updateSessionStatus(db, sessionId, "cancelled");
979
1043
  process.exit(130);
980
1044
  }
1045
+ // v0.2.3: structured error output
1046
+ if (options.format === 'json') {
1047
+ process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
1048
+ updateSessionStatus(db, sessionId, 'failed', message);
1049
+ process.exit(1);
1050
+ }
1051
+ else if (options.format === 'ndjson') {
1052
+ process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
1053
+ updateSessionStatus(db, sessionId, 'failed', message);
1054
+ process.exit(1);
1055
+ }
981
1056
  uiError(message);
982
1057
  updateSessionStatus(db, sessionId, "failed", message);
983
1058
  process.exit(1);
@@ -987,33 +1062,32 @@ export async function runReview(options) {
987
1062
  * Execute the explore command.
988
1063
  *
989
1064
  * Flow:
990
- * 1. Parse flags and load configuration (Req 13.1)
991
- * 2. Detect project type (Req 13.1)
992
- * 3. Create session with mode: "plan" (Req 13.2)
993
- * 4. Scan repository structure respecting .gitignore (Req 13.3)
994
- * 5. Detect frameworks and key configuration files (Req 13.4)
995
- * 6. Identify entry points based on project type (Req 13.5)
996
- * 7. Build system prompt from explore.md template (Req 13.3–13.6)
997
- * 8. Execute agent loop to summarize structure/patterns (Req 13.6)
998
- * 9. Render exploration summary to terminal (Req 13.7)
999
- * 10. Save to ./.aria/explore.md if --save flag (Req 13.8)
1000
- * 11. Persist session to database (Req 13.10)
1065
+ * 1. Parse flags and load configuration
1066
+ * 2. Detect project type
1067
+ * 3. Create session with mode: "plan"
1068
+ * 4. Scan repository structure respecting .gitignore
1069
+ * 5. Detect frameworks and key configuration files
1070
+ * 6. Identify entry points based on project type
1071
+ * 7. Build system prompt from explore.md template
1072
+ * 8. Execute agent loop to summarize structure/patterns
1073
+ * 9. Render exploration summary to terminal
1074
+ * 10. Save to ./.aria/explore.md if --save flag
1075
+ * 11. Persist session to database
1001
1076
  *
1002
- * Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9, 13.10
1003
1077
  */
1004
1078
  export async function runExplore(options) {
1005
1079
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
1006
- // 1. Load configuration (Req 13.1)
1080
+ // 1. Load configuration
1007
1081
  const config = getConfig(projectRoot, {
1008
1082
  quiet: options.quiet,
1009
1083
  provider: options.provider,
1010
1084
  model: options.model,
1011
1085
  });
1012
1086
  // Initialize UI with config settings
1013
- initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
1014
- // Detect project type early to fail fast (Req 13.1)
1087
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet) || (options.format === 'json' || options.format === 'ndjson'));
1088
+ // Detect project type early to fail fast
1015
1089
  const project = detectProjectType(projectRoot);
1016
- // 2. Initialize database and create session (Req 13.10)
1090
+ // 2. Initialize database and create session
1017
1091
  const db = initializeDatabase();
1018
1092
  const sessionId = randomUUID();
1019
1093
  createSession(db, {
@@ -1038,7 +1112,7 @@ export async function runExplore(options) {
1038
1112
  updateSessionStatus(db, sessionId, "failed", String(err));
1039
1113
  process.exit(4);
1040
1114
  }
1041
- // 4. Build execution context with mode: "plan" (Req 13.2)
1115
+ // 4. Build execution context with mode: "plan"
1042
1116
  const ctx = {
1043
1117
  projectRoot,
1044
1118
  sessionId,
@@ -1050,10 +1124,10 @@ export async function runExplore(options) {
1050
1124
  maxIterations: config.agent.maxIterations,
1051
1125
  timeoutSeconds: config.agent.timeoutSeconds,
1052
1126
  };
1053
- // 5. Build system prompt from explore.md template (Req 13.3–13.6)
1127
+ // 5. Build system prompt from explore.md template
1054
1128
  const systemPrompt = buildSystemPrompt("explore", ctx);
1055
1129
  logMessage(db, sessionId, "system", systemPrompt);
1056
- // 6. Build user request with project context and depth hint (Req 13.9)
1130
+ // 6. Build user request with project context and depth hint
1057
1131
  const frameworkInfo = project.framework
1058
1132
  ? `${project.framework.name}${project.framework.version ? ` ${project.framework.version}` : ""}${project.framework.router ? ` (${project.framework.router} router)` : ""}`
1059
1133
  : "none";
@@ -1077,20 +1151,24 @@ export async function runExplore(options) {
1077
1151
  ]
1078
1152
  .filter((l) => l !== null)
1079
1153
  .join("\n");
1080
- // 7. Execute agent loop — streams response to stdout (Req 13.6, 13.7)
1154
+ // 7. Execute agent loop — streams response to stdout
1081
1155
  try {
1082
1156
  const exploreContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "explore", db, systemPrompt);
1083
- // 8. Save to ./.aria/explore.md if --save flag provided (Req 13.8)
1157
+ // v0.2.3: structured output
1158
+ if (options.format === 'json' || options.format === 'ndjson') {
1159
+ process.stdout.write(formatOutput({ version: '1', summary: exploreContent, sessionId }, options.format, ExploreOutputSchema));
1160
+ }
1161
+ // 8. Save to ./.aria/explore.md if --save flag provided
1084
1162
  if (options.save) {
1085
1163
  const ariaDir = path.join(projectRoot, ".aria");
1086
1164
  const savePath = path.join(ariaDir, "explore.md");
1087
1165
  if (!existsSync(ariaDir)) {
1088
1166
  mkdirSync(ariaDir, { recursive: true });
1089
1167
  }
1090
- writeFileSync(savePath, exploreContent, "utf-8");
1168
+ writeFileAtomic(savePath, exploreContent);
1091
1169
  info(`Exploration summary saved to .aria/explore.md`);
1092
1170
  }
1093
- // 9. Mark session as completed (Req 13.10)
1171
+ // 9. Mark session as completed
1094
1172
  updateSessionStatus(db, sessionId, "completed");
1095
1173
  }
1096
1174
  catch (err) {
@@ -1101,6 +1179,17 @@ export async function runExplore(options) {
1101
1179
  updateSessionStatus(db, sessionId, "cancelled");
1102
1180
  process.exit(130);
1103
1181
  }
1182
+ // v0.2.3: structured error output
1183
+ if (options.format === 'json') {
1184
+ process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
1185
+ updateSessionStatus(db, sessionId, 'failed', message);
1186
+ process.exit(1);
1187
+ }
1188
+ else if (options.format === 'ndjson') {
1189
+ process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
1190
+ updateSessionStatus(db, sessionId, 'failed', message);
1191
+ process.exit(1);
1192
+ }
1104
1193
  uiError(message);
1105
1194
  updateSessionStatus(db, sessionId, "failed", message);
1106
1195
  process.exit(1);
@@ -1110,7 +1199,6 @@ export async function runExplore(options) {
1110
1199
  * Format a SQLite timestamp string into a human-readable relative time.
1111
1200
  * e.g. "2 hours ago", "3 days ago", "just now"
1112
1201
  *
1113
- * Requirements: 14.7
1114
1202
  */
1115
1203
  function formatTimestamp(timestamp) {
1116
1204
  // SQLite CURRENT_TIMESTAMP returns "YYYY-MM-DD HH:MM:SS" (space, no T, no Z).
@@ -1162,8 +1250,20 @@ function colorizeStatus(status) {
1162
1250
  * Render a tool execution tree for a session.
1163
1251
  * Shows tool calls in chronological order with input/output summaries.
1164
1252
  *
1165
- * Requirements: 14.6
1166
1253
  */
1254
+ // Tool type icon sets
1255
+ const READ_TOOLS = new Set(['read_file', 'read_package_json', 'read_prisma_schema']);
1256
+ const SEARCH_TOOLS = new Set(['search_code', 'list_directory']);
1257
+ const MUTATION_TOOLS = new Set(['propose_diff', 'apply_diff', 'apply_schema_change']);
1258
+ function toolIcon(toolName) {
1259
+ if (READ_TOOLS.has(toolName))
1260
+ return '[R]';
1261
+ if (SEARCH_TOOLS.has(toolName))
1262
+ return '[S]';
1263
+ if (MUTATION_TOOLS.has(toolName))
1264
+ return '[W]';
1265
+ return '[.]';
1266
+ }
1167
1267
  function renderToolTree(db, sessionId) {
1168
1268
  const executions = db
1169
1269
  .prepare(`SELECT tool_name, input, output, error, created_at
@@ -1181,7 +1281,8 @@ function renderToolTree(db, sessionId) {
1181
1281
  const prefix = isLast ? "└─" : "├─";
1182
1282
  const childPrefix = isLast ? " " : "│ ";
1183
1283
  const statusIcon = exec.error ? red("✗") : green("✓");
1184
- info(` ${prefix} ${statusIcon} ${bold(exec.tool_name)} ${dim(formatTimestamp(exec.created_at))}`);
1284
+ const typeIcon = toolIcon(exec.tool_name);
1285
+ info(` ${prefix} ${typeIcon} ${statusIcon} ${bold(exec.tool_name)} ${dim(formatTimestamp(exec.created_at))}`);
1185
1286
  // Show a brief summary of the input
1186
1287
  try {
1187
1288
  const inputObj = JSON.parse(exec.input);
@@ -1214,23 +1315,88 @@ function renderToolTree(db, sessionId) {
1214
1315
  * Execute the history command.
1215
1316
  *
1216
1317
  * Flow:
1217
- * 1. If no --session flag: list recent sessions in a table (Req 14.2, 14.3, 14.4)
1218
- * 2. If --session flag: display full session log (Req 14.5)
1219
- * 3. If --tree flag: render tool execution tree (Req 14.6)
1220
- * 4. Format timestamps in human-readable format (Req 14.7)
1221
- * 5. Support pagination for large result sets (Req 14.8)
1318
+ * 1. If no --session flag: list recent sessions in a table
1319
+ * 2. If --session flag: display full session log
1320
+ * 3. If --tree flag: render tool execution tree
1321
+ * 4. Format timestamps in human-readable format
1322
+ * 5. Support pagination for large result sets
1222
1323
  *
1223
- * Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.8
1224
1324
  */
1225
1325
  export async function runHistory(options) {
1226
1326
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
1227
1327
  // Load configuration and initialize UI
1228
1328
  const config = getConfig(projectRoot, { quiet: options.quiet });
1229
- initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
1329
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet) || (options.format === 'json' || options.format === 'ndjson'));
1230
1330
  // Initialize database
1231
1331
  const db = initializeDatabase();
1232
1332
  // ---------------------------------------------------------------------------
1233
- // Case 1: --session flagshow full session log (Req 14.5)
1333
+ // v0.2.3: Structured output for json/ndjson/plain applies to session listing
1334
+ // ---------------------------------------------------------------------------
1335
+ const isStructuredFormat = options.format === 'json' || options.format === 'ndjson';
1336
+ const isPlainFormat = options.format === 'plain';
1337
+ // ---------------------------------------------------------------------------
1338
+ // v0.2.3: Search path — full-text search across session content
1339
+ // ---------------------------------------------------------------------------
1340
+ if (options.search) {
1341
+ const results = searchSessions(db, options.search, { limit: options.limit });
1342
+ if (results.length === 0) {
1343
+ info("No sessions found matching query.");
1344
+ return;
1345
+ }
1346
+ const rows = results.map(({ session, matchedInMessages }) => [
1347
+ dim(session.id.slice(0, 8)),
1348
+ cyan(session.command),
1349
+ colorizeStatus(session.status),
1350
+ formatTimestamp(session.createdAt),
1351
+ matchedInMessages ? dim("message") : dim("metadata"),
1352
+ ]);
1353
+ const table = renderTable({ head: ["ID", "Command", "Status", "Started", "Match"], colWidths: [12, 12, 12, 20, 12] }, rows);
1354
+ info(table);
1355
+ return;
1356
+ }
1357
+ // ---------------------------------------------------------------------------
1358
+ // v0.2.3: Filter path — filter by command, since, status
1359
+ // ---------------------------------------------------------------------------
1360
+ if (options.command || options.since || options.status) {
1361
+ const sessions = filterSessions(db, {
1362
+ command: options.command,
1363
+ since: options.since,
1364
+ status: options.status,
1365
+ limit: options.limit,
1366
+ });
1367
+ if (sessions.length === 0) {
1368
+ info("No sessions found.");
1369
+ return;
1370
+ }
1371
+ const rows = sessions.map((s) => [
1372
+ dim(s.id.slice(0, 8)),
1373
+ cyan(s.command),
1374
+ formatTimestamp(s.createdAt),
1375
+ colorizeStatus(s.status),
1376
+ ]);
1377
+ const table = renderTable({ head: ["ID", "Command", "When", "Status"], colWidths: [12, 12, 20, 12] }, rows);
1378
+ info(table);
1379
+ return;
1380
+ }
1381
+ // ---------------------------------------------------------------------------
1382
+ // v0.2.3: Export path — export session transcript to markdown
1383
+ // ---------------------------------------------------------------------------
1384
+ if (options.export && options.session) {
1385
+ // Validate export path is within project root to prevent path traversal
1386
+ try {
1387
+ const { validatePath } = await import('./safety.js');
1388
+ validatePath(path.resolve(options.export), projectRoot);
1389
+ }
1390
+ catch {
1391
+ uiError(`Export path must be within the project root: ${options.export}`);
1392
+ process.exit(2);
1393
+ }
1394
+ exportSessionMarkdown(db, options.session, options.export);
1395
+ info(`Session exported to ${path.resolve(options.export)}`);
1396
+ return;
1397
+ }
1398
+ // ---------------------------------------------------------------------------
1399
+ // Case 1: --session flag — show full session log
1234
1400
  // ---------------------------------------------------------------------------
1235
1401
  if (options.session) {
1236
1402
  const session = getSession(db, options.session);
@@ -1288,7 +1454,7 @@ export async function runHistory(options) {
1288
1454
  if (toolCount > 0) {
1289
1455
  info(bold(`Tool Executions (${toolCount}):`));
1290
1456
  if (options.tree) {
1291
- // Render as tree (Req 14.6)
1457
+ // Render as tree
1292
1458
  renderToolTree(db, options.session);
1293
1459
  }
1294
1460
  else {
@@ -1309,30 +1475,55 @@ export async function runHistory(options) {
1309
1475
  return;
1310
1476
  }
1311
1477
  // ---------------------------------------------------------------------------
1312
- // Case 2: No --session flag — list recent sessions (Req 14.2, 14.3, 14.4)
1478
+ // Case 2: No --session flag — list recent sessions
1313
1479
  // ---------------------------------------------------------------------------
1314
1480
  const PAGE_SIZE = 20;
1315
1481
  const limit = options.limit ?? PAGE_SIZE;
1316
- // Fetch sessions with pagination support (Req 14.8)
1482
+ // Fetch sessions with pagination support
1317
1483
  const sessions = listSessions(db, { limit });
1318
1484
  if (sessions.length === 0) {
1319
- info("No sessions found. Run a command to create your first session.");
1485
+ if (!isStructuredFormat) {
1486
+ info("No sessions found. Run a command to create your first session.");
1487
+ }
1488
+ if (isStructuredFormat) {
1489
+ process.stdout.write(formatOutput({ version: '1', sessions: [], total: 0 }, options.format, HistoryOutputSchema));
1490
+ }
1491
+ return;
1492
+ }
1493
+ // v0.2.3: structured output
1494
+ if (isStructuredFormat) {
1495
+ const outputSessions = sessions.map((s) => ({
1496
+ id: s.id,
1497
+ command: s.command,
1498
+ status: s.status,
1499
+ createdAt: s.createdAt,
1500
+ completedAt: s.completedAt ?? null,
1501
+ }));
1502
+ process.stdout.write(formatOutput({ version: '1', sessions: outputSessions, total: sessions.length }, options.format, HistoryOutputSchema));
1503
+ return;
1504
+ }
1505
+ // v0.2.3: plain format — tab-separated, no cli-table3 borders
1506
+ if (isPlainFormat) {
1507
+ process.stdout.write('ID\tCommand\tWhen\tStatus\n');
1508
+ for (const s of sessions) {
1509
+ process.stdout.write(`${s.id.slice(0, 8)}\t${s.command}\t${s.createdAt}\t${s.status}\n`);
1510
+ }
1320
1511
  return;
1321
1512
  }
1322
- // Build table rows (Req 14.3)
1513
+ // Build table rows
1323
1514
  const rows = sessions.map((s) => [
1324
1515
  dim(s.id.slice(0, 8)), // abbreviated ID
1325
1516
  cyan(s.command),
1326
- formatTimestamp(s.createdAt), // human-readable timestamp (Req 14.7)
1517
+ formatTimestamp(s.createdAt), // human-readable timestamp
1327
1518
  colorizeStatus(s.status),
1328
1519
  ]);
1329
- // Render table with cli-table3 (Req 14.3, 20.4)
1520
+ // Render table with cli-table3
1330
1521
  const table = renderTable({
1331
1522
  head: ["ID", "Command", "When", "Status"],
1332
1523
  colWidths: [12, 12, 20, 12],
1333
1524
  }, rows);
1334
1525
  info(table);
1335
- // Show pagination hint if there may be more results (Req 14.8)
1526
+ // Show pagination hint if there may be more results
1336
1527
  if (sessions.length === limit && !options.limit) {
1337
1528
  info(dim(`\nShowing ${limit} most recent sessions. Use --limit <n> to see more.`));
1338
1529
  }
@@ -1423,7 +1614,6 @@ function parseConfigValue(value) {
1423
1614
  }
1424
1615
  /**
1425
1616
  * Display the effective configuration with precedence sources.
1426
- * Requirements: 15.2
1427
1617
  */
1428
1618
  function displayEffectiveConfig(projectRoot, config) {
1429
1619
  const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
@@ -1464,13 +1654,12 @@ function displayEffectiveConfig(projectRoot, config) {
1464
1654
  * Execute the config command.
1465
1655
  *
1466
1656
  * Subcommands:
1467
- * - (none): Display effective configuration with precedence sources (Req 15.2)
1468
- * - get <key>: Display value for specified key (Req 15.3)
1469
- * - set <key> <value>: Write key-value to ~/.aria/config.toml (Req 15.4–15.6, 15.10)
1470
- * - path: Display configuration file resolution paths (Req 15.7)
1471
- * - init: Create ./.aria.toml with default values (Req 15.8, 15.9)
1657
+ * - (none): Display effective configuration with precedence sources
1658
+ * - get <key>: Display value for specified key
1659
+ * - set <key> <value>: Write key-value to ~/.aria/config.toml
1660
+ * - path: Display configuration file resolution paths
1661
+ * - init: Create ./.aria.toml with default values
1472
1662
  *
1473
- * Requirements: 15.1–15.10
1474
1663
  */
1475
1664
  export async function runConfig(options) {
1476
1665
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
@@ -1480,14 +1669,14 @@ export async function runConfig(options) {
1480
1669
  const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
1481
1670
  const projectConfigPath = path.join(projectRoot, ".aria.toml");
1482
1671
  // ---------------------------------------------------------------------------
1483
- // No subcommand: display effective configuration (Req 15.2)
1672
+ // No subcommand: display effective configuration
1484
1673
  // ---------------------------------------------------------------------------
1485
1674
  if (!options.subcommand) {
1486
1675
  displayEffectiveConfig(projectRoot, config);
1487
1676
  return;
1488
1677
  }
1489
1678
  // ---------------------------------------------------------------------------
1490
- // config path: display config file resolution paths (Req 15.7)
1679
+ // config path: display config file resolution paths
1491
1680
  // ---------------------------------------------------------------------------
1492
1681
  if (options.subcommand === "path") {
1493
1682
  info(bold("Configuration file paths:"));
@@ -1500,7 +1689,7 @@ export async function runConfig(options) {
1500
1689
  return;
1501
1690
  }
1502
1691
  // ---------------------------------------------------------------------------
1503
- // config get <key>: display value for key (Req 15.3)
1692
+ // config get <key>: display value for key
1504
1693
  // ---------------------------------------------------------------------------
1505
1694
  if (options.subcommand === "get") {
1506
1695
  const key = options.key;
@@ -1513,14 +1702,14 @@ export async function runConfig(options) {
1513
1702
  return;
1514
1703
  }
1515
1704
  // ---------------------------------------------------------------------------
1516
- // config set <key> <value>: write to user config (Req 15.4–15.6, 15.10)
1705
+ // config set <key> <value>: write to user config
1517
1706
  // ---------------------------------------------------------------------------
1518
1707
  if (options.subcommand === "set") {
1519
1708
  const key = options.key;
1520
1709
  const rawValue = options.value;
1521
1710
  // Parse the value to the appropriate type
1522
1711
  const parsedValue = parseConfigValue(rawValue);
1523
- // Validate by applying to current config and re-validating (Req 15.10)
1712
+ // Validate by applying to current config and re-validating
1524
1713
  const currentMerged = loadConfig(projectRoot);
1525
1714
  const updatedMerged = setNestedValue(currentMerged, key, parsedValue);
1526
1715
  let validatedConfig;
@@ -1536,16 +1725,16 @@ export async function runConfig(options) {
1536
1725
  ? readFileSync(userConfigPath, "utf-8")
1537
1726
  : "";
1538
1727
  const newContent = serializeConfigToToml(validatedConfig);
1539
- // Preview the diff (Req 15.5, 15.6)
1728
+ // Preview the diff
1540
1729
  const diffOutput = generateAndRenderDiff(userConfigPath, oldContent, newContent);
1541
1730
  info(bold("Preview:"));
1542
1731
  info(diffOutput);
1543
- // If --dry-run, exit without writing (Req 15.6)
1732
+ // If --dry-run, exit without writing
1544
1733
  if (options.dryRun) {
1545
1734
  info(yellow("Dry-run mode — no changes written."));
1546
1735
  return;
1547
1736
  }
1548
- // If not --yes, prompt for confirmation (Req 15.5)
1737
+ // If not --yes, prompt for confirmation
1549
1738
  if (!options.yes) {
1550
1739
  let confirmed;
1551
1740
  try {
@@ -1563,7 +1752,7 @@ export async function runConfig(options) {
1563
1752
  process.exit(130);
1564
1753
  }
1565
1754
  }
1566
- // Write to ~/.aria/config.toml (Req 15.4)
1755
+ // Write to ~/.aria/config.toml
1567
1756
  const ariaDir = path.join(os.homedir(), ".aria");
1568
1757
  if (!existsSync(ariaDir)) {
1569
1758
  mkdirSync(ariaDir, { recursive: true });
@@ -1573,7 +1762,7 @@ export async function runConfig(options) {
1573
1762
  return;
1574
1763
  }
1575
1764
  // ---------------------------------------------------------------------------
1576
- // config init: create ./.aria.toml with defaults (Req 15.8, 15.9)
1765
+ // config init: create ./.aria.toml with defaults
1577
1766
  // ---------------------------------------------------------------------------
1578
1767
  if (options.subcommand === "init") {
1579
1768
  // Generate default config content
@@ -1586,12 +1775,12 @@ export async function runConfig(options) {
1586
1775
  const diffOutput = generateAndRenderDiff(projectConfigPath, oldContent, defaultContent);
1587
1776
  info(bold("Preview (.aria.toml):"));
1588
1777
  info(diffOutput);
1589
- // If --dry-run, exit without writing (Req 17.4)
1778
+ // If --dry-run, exit without writing
1590
1779
  if (options.dryRun) {
1591
1780
  info(yellow("Dry-run mode — no file created."));
1592
1781
  return;
1593
1782
  }
1594
- // If not --yes, prompt for confirmation (Req 15.9)
1783
+ // If not --yes, prompt for confirmation
1595
1784
  if (!options.yes) {
1596
1785
  let confirmed;
1597
1786
  try {
@@ -1609,7 +1798,7 @@ export async function runConfig(options) {
1609
1798
  process.exit(130);
1610
1799
  }
1611
1800
  }
1612
- // Write ./.aria.toml (Req 15.8)
1801
+ // Write ./.aria.toml
1613
1802
  writeFileSync(projectConfigPath, defaultContent, "utf-8");
1614
1803
  info(green(`✓ Created ${projectConfigPath}`));
1615
1804
  return;
@@ -1621,7 +1810,6 @@ export async function runConfig(options) {
1621
1810
  * Runs a series of environment diagnostic checks and reports results.
1622
1811
  * Exits with code 1 if any critical check fails.
1623
1812
  *
1624
- * Requirements: 16.1–16.13
1625
1813
  */
1626
1814
  export async function runDoctor(options = {}) {
1627
1815
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
@@ -1636,7 +1824,7 @@ export async function runDoctor(options = {}) {
1636
1824
  }
1637
1825
  const checks = [];
1638
1826
  // -------------------------------------------------------------------------
1639
- // 1. Node.js version >= 20 (Req 16.2) — CRITICAL
1827
+ // 1. Node.js version >= 20
1640
1828
  // -------------------------------------------------------------------------
1641
1829
  {
1642
1830
  const nodeVersion = process.version; // e.g. "v20.11.0"
@@ -1653,7 +1841,7 @@ export async function runDoctor(options = {}) {
1653
1841
  }
1654
1842
  }
1655
1843
  // -------------------------------------------------------------------------
1656
- // 2. git availability (Req 16.3) — WARN only
1844
+ // 2. git availability — WARN only
1657
1845
  // -------------------------------------------------------------------------
1658
1846
  {
1659
1847
  try {
@@ -1665,7 +1853,7 @@ export async function runDoctor(options = {}) {
1665
1853
  }
1666
1854
  }
1667
1855
  // -------------------------------------------------------------------------
1668
- // 3. ripgrep (rg) availability (Req 16.4) — WARN only
1856
+ // 3. ripgrep (rg) availability — WARN only
1669
1857
  // -------------------------------------------------------------------------
1670
1858
  {
1671
1859
  try {
@@ -1677,7 +1865,7 @@ export async function runDoctor(options = {}) {
1677
1865
  }
1678
1866
  }
1679
1867
  // -------------------------------------------------------------------------
1680
- // 4. Config file syntax and schema validation (Req 16.5) — CRITICAL
1868
+ // 4. Config file syntax and schema validation — CRITICAL
1681
1869
  // -------------------------------------------------------------------------
1682
1870
  {
1683
1871
  const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
@@ -1704,7 +1892,7 @@ export async function runDoctor(options = {}) {
1704
1892
  }
1705
1893
  }
1706
1894
  // -------------------------------------------------------------------------
1707
- // 5. History DB accessibility and schema version (Req 16.6) — CRITICAL
1895
+ // 5. History DB accessibility and schema version — CRITICAL
1708
1896
  // -------------------------------------------------------------------------
1709
1897
  {
1710
1898
  try {
@@ -1722,7 +1910,7 @@ export async function runDoctor(options = {}) {
1722
1910
  }
1723
1911
  }
1724
1912
  // -------------------------------------------------------------------------
1725
- // 6. Provider readiness — API key presence (Req 16.7) — CRITICAL for default
1913
+ // 6. Provider readiness — API key presence — CRITICAL for default
1726
1914
  // v0.2.2: report all configured providers, fail only if default key is missing
1727
1915
  // -------------------------------------------------------------------------
1728
1916
  {
@@ -1759,7 +1947,7 @@ export async function runDoctor(options = {}) {
1759
1947
  }
1760
1948
  }
1761
1949
  // -------------------------------------------------------------------------
1762
- // 7. Project type detection (Req 16.8)
1950
+ // 7. Project type detection
1763
1951
  // -------------------------------------------------------------------------
1764
1952
  {
1765
1953
  try {
@@ -1778,7 +1966,7 @@ export async function runDoctor(options = {}) {
1778
1966
  }
1779
1967
  }
1780
1968
  // -------------------------------------------------------------------------
1781
- // 8. Prisma schema existence and model count (Req 16.9 + v0.2.0)
1969
+ // 8. Prisma schema existence and model count
1782
1970
  // -------------------------------------------------------------------------
1783
1971
  {
1784
1972
  try {
@@ -1818,7 +2006,7 @@ export async function runDoctor(options = {}) {
1818
2006
  }
1819
2007
  }
1820
2008
  // -------------------------------------------------------------------------
1821
- // 9. Ollama reachability if Ollama provider selected (Req 16.10) — WARN
2009
+ // 9. Ollama reachability if Ollama provider selected — WARN
1822
2010
  // -------------------------------------------------------------------------
1823
2011
  {
1824
2012
  const provider = config?.provider.default ?? "anthropic";
@@ -1841,10 +2029,22 @@ export async function runDoctor(options = {}) {
1841
2029
  // -------------------------------------------------------------------------
1842
2030
  const criticalNames = new Set(["nodejs", "config", "history_db", "provider"]);
1843
2031
  const hasCriticalFailure = checks.some((c) => c.status === "fail" && criticalNames.has(c.name));
1844
- if (options.format === "json") {
1845
- // JSON output (Req 16.12)
1846
- const allPassed = !checks.some((c) => c.status === "fail");
1847
- process.stdout.write(JSON.stringify({ checks, allPassed }, null, 2) + "\n");
2032
+ const allPassed = !checks.some((c) => c.status === "fail");
2033
+ if (options.format === 'json' || options.format === 'ndjson') {
2034
+ // v0.2.3: structured output via formatOutput
2035
+ const doctorChecks = checks.map((c) => ({
2036
+ name: c.name,
2037
+ passed: c.status !== 'fail',
2038
+ message: c.message,
2039
+ }));
2040
+ process.stdout.write(formatOutput({ version: '1', checks: doctorChecks, allPassed }, options.format, DoctorOutputSchema));
2041
+ }
2042
+ else if (options.format === 'plain') {
2043
+ // Plain text — no ANSI codes, tab-separated
2044
+ for (const check of checks) {
2045
+ const icon = check.status === 'pass' ? 'OK' : check.status === 'warn' ? 'WARN' : 'FAIL';
2046
+ process.stdout.write(`${icon}\t${check.name}\t${check.message}\n`);
2047
+ }
1848
2048
  }
1849
2049
  else {
1850
2050
  // Text output (Req 16.11)
@@ -1870,7 +2070,7 @@ export async function runDoctor(options = {}) {
1870
2070
  info(green("All critical checks passed."));
1871
2071
  }
1872
2072
  }
1873
- // Exit with code 1 if any critical check fails (Req 16.13)
2073
+ // Exit with code 1 if any critical check fails
1874
2074
  if (hasCriticalFailure) {
1875
2075
  process.exit(1);
1876
2076
  }