@ariacode/cli 0.2.1 → 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 (66) hide show
  1. package/README.md +78 -11
  2. package/dist/actions/db-ask.d.ts +7 -1
  3. package/dist/actions/db-ask.js +27 -10
  4. package/dist/actions/db-ask.js.map +1 -1
  5. package/dist/actions/db-explain.d.ts +6 -0
  6. package/dist/actions/db-explain.js +25 -7
  7. package/dist/actions/db-explain.js.map +1 -1
  8. package/dist/actions/db-migrate.d.ts +4 -0
  9. package/dist/actions/db-migrate.js +2 -2
  10. package/dist/actions/db-migrate.js.map +1 -1
  11. package/dist/actions/db-schema.d.ts +3 -1
  12. package/dist/actions/db-schema.js +18 -5
  13. package/dist/actions/db-schema.js.map +1 -1
  14. package/dist/actions/upgrade-deps.d.ts +6 -0
  15. package/dist/actions/upgrade-deps.js +20 -2
  16. package/dist/actions/upgrade-deps.js.map +1 -1
  17. package/dist/actions/upgrade-prisma.d.ts +4 -0
  18. package/dist/actions/upgrade-prisma.js +2 -2
  19. package/dist/actions/upgrade-prisma.js.map +1 -1
  20. package/dist/actions.d.ts +106 -71
  21. package/dist/actions.js +438 -204
  22. package/dist/actions.js.map +1 -1
  23. package/dist/agent.d.ts +0 -1
  24. package/dist/agent.js +1 -2
  25. package/dist/agent.js.map +1 -1
  26. package/dist/app.d.ts +3 -3
  27. package/dist/app.js +3 -3
  28. package/dist/cli.d.ts +0 -1
  29. package/dist/cli.js +37 -5
  30. package/dist/cli.js.map +1 -1
  31. package/dist/config.d.ts +17 -1
  32. package/dist/config.js +40 -1
  33. package/dist/config.js.map +1 -1
  34. package/dist/output/schemas.d.ts +92 -0
  35. package/dist/output/schemas.js +142 -0
  36. package/dist/output/schemas.js.map +1 -0
  37. package/dist/parser.d.ts +10 -3
  38. package/dist/parser.js +70 -8
  39. package/dist/parser.js.map +1 -1
  40. package/dist/provider.d.ts +7 -2
  41. package/dist/provider.js +6 -4
  42. package/dist/provider.js.map +1 -1
  43. package/dist/repo.d.ts +0 -1
  44. package/dist/repo.js +6 -9
  45. package/dist/repo.js.map +1 -1
  46. package/dist/safety.d.ts +0 -4
  47. package/dist/safety.js +0 -4
  48. package/dist/safety.js.map +1 -1
  49. package/dist/storage/queries.d.ts +39 -0
  50. package/dist/storage/queries.js +211 -0
  51. package/dist/storage/queries.js.map +1 -0
  52. package/dist/tools.d.ts +6 -7
  53. package/dist/tools.js +20 -7
  54. package/dist/tools.js.map +1 -1
  55. package/dist/ui/diff-renderer.d.ts +28 -0
  56. package/dist/ui/diff-renderer.js +388 -0
  57. package/dist/ui/diff-renderer.js.map +1 -0
  58. package/dist/ui/highlight.d.ts +25 -0
  59. package/dist/ui/highlight.js +239 -0
  60. package/dist/ui/highlight.js.map +1 -0
  61. package/dist/ui.d.ts +3 -16
  62. package/dist/ui.js +7 -28
  63. package/dist/ui.js.map +1 -1
  64. package/dist/upgrade/prisma-upgrade.js +5 -2
  65. package/dist/upgrade/prisma-upgrade.js.map +1 -1
  66. 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
@@ -48,6 +51,20 @@ const DEFAULT_MODELS = {
48
51
  ollama: "llama3",
49
52
  openrouter: "anthropic/claude-sonnet-4-6",
50
53
  };
54
+ /**
55
+ * Resolve the effective model for the current provider config.
56
+ * Per-provider model overrides take precedence over the global model setting.
57
+ */
58
+ function resolveModel(config) {
59
+ const provider = config.provider.default;
60
+ if (provider === "anthropic" && config.provider.anthropic?.model) {
61
+ return config.provider.anthropic.model;
62
+ }
63
+ if (provider === "openrouter" && config.provider.openrouter?.model) {
64
+ return config.provider.openrouter.model;
65
+ }
66
+ return config.provider.model;
67
+ }
51
68
  /**
52
69
  * Check if the given provider has its API key available.
53
70
  */
@@ -69,7 +86,7 @@ function isProviderReady(providerName) {
69
86
  async function resolveProvider(config) {
70
87
  // 1. Try the configured provider first
71
88
  if (isProviderReady(config.provider.default)) {
72
- return createProvider(config.provider.default);
89
+ return createProvider(config.provider.default, config.provider);
73
90
  }
74
91
  // 2. Check if any other provider is already configured via env
75
92
  for (const [name, envKey] of Object.entries(PROVIDER_ENV_KEYS)) {
@@ -79,7 +96,7 @@ async function resolveProvider(config) {
79
96
  info(dim(`${config.provider.default} not configured, falling back to ${name}`));
80
97
  config.provider.default = name;
81
98
  config.provider.model = DEFAULT_MODELS[name] ?? config.provider.model;
82
- return createProvider(name);
99
+ return createProvider(name, config.provider);
83
100
  }
84
101
  }
85
102
  // 3. No provider ready — interactive setup
@@ -172,7 +189,7 @@ async function resolveProvider(config) {
172
189
  info(yellow(`Could not write to ~/${shellRcName}. Set ${envKey} manually.`));
173
190
  }
174
191
  }
175
- return createProvider(selectedProvider);
192
+ return createProvider(selectedProvider, config.provider);
176
193
  }
177
194
  /**
178
195
  * Save the provider/model choice to ~/.aria/config.toml
@@ -198,7 +215,6 @@ function saveProviderChoice(config) {
198
215
  // ---------------------------------------------------------------------------
199
216
  /**
200
217
  * The five read-only tools exposed to the ask command.
201
- * Requirements: 9.4
202
218
  */
203
219
  const READ_ONLY_TOOLS = [
204
220
  readFileTool,
@@ -326,33 +342,34 @@ function buildSystemPrompt(templateName, ctx, extraVars = {}) {
326
342
  * Execute the ask command.
327
343
  *
328
344
  * Flow:
329
- * 1. Load configuration and detect project type (Req 9.1)
330
- * 2. Create or resume session with mode: "plan" (Req 9.2, 9.9)
331
- * 3. Build system prompt from ask.md template (Req 9.3)
332
- * 4. Expose only read-only tools (Req 9.4)
333
- * 5. Execute agent loop (Req 9.5, 9.6)
334
- * 6. Render response to terminal (Req 9.7)
335
- * 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
336
352
  *
337
- * Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9
338
353
  */
339
354
  export async function runAsk(options) {
340
355
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
341
- // 1. Load configuration (Req 9.1)
356
+ // 1. Load configuration
342
357
  const config = getConfig(projectRoot, {
343
358
  quiet: options.quiet,
344
359
  maxTokens: options.maxTokens,
360
+ provider: options.provider,
361
+ model: options.model,
345
362
  });
346
363
  // Initialize UI with config settings
347
- initUI(config.ui.color, config.ui.quiet);
348
- // 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
349
366
  detectProjectType(projectRoot);
350
- // 2. Initialize database and create/resume session (Req 9.2, 9.8, 9.9)
367
+ // 2. Initialize database and create/resume session
351
368
  const db = initializeDatabase();
352
369
  let sessionId;
353
370
  let resumedMessages = [];
354
371
  if (options.session) {
355
- // Resume existing session (Req 9.9)
372
+ // Resume existing session
356
373
  const existing = getSession(db, options.session);
357
374
  if (!existing) {
358
375
  uiError(`Session not found: ${options.session}`);
@@ -367,14 +384,14 @@ export async function runAsk(options) {
367
384
  resumedMessages = rows;
368
385
  }
369
386
  else {
370
- // Create new session (Req 9.2)
387
+ // Create new session
371
388
  sessionId = randomUUID();
372
389
  createSession(db, {
373
390
  id: sessionId,
374
391
  command: "ask",
375
392
  projectRoot,
376
393
  provider: config.provider.default,
377
- model: config.provider.model,
394
+ model: resolveModel(config),
378
395
  });
379
396
  }
380
397
  // 3. Resolve provider (interactive setup if needed)
@@ -392,12 +409,12 @@ export async function runAsk(options) {
392
409
  updateSessionStatus(db, sessionId, "failed", String(err));
393
410
  process.exit(4);
394
411
  }
395
- // 4. Build execution context with mode: "plan" (Req 9.2)
412
+ // 4. Build execution context with mode: "plan"
396
413
  const ctx = {
397
414
  projectRoot,
398
415
  sessionId,
399
416
  provider: config.provider.default,
400
- model: config.provider.model,
417
+ model: resolveModel(config),
401
418
  mode: "plan", // ask is always read-only
402
419
  dryRun: false,
403
420
  assumeYes: false,
@@ -408,7 +425,7 @@ export async function runAsk(options) {
408
425
  if (options.maxTokens !== undefined) {
409
426
  config.provider.maxTokens = options.maxTokens;
410
427
  }
411
- // 5. Build system prompt from ask.md template (Req 9.3)
428
+ // 5. Build system prompt from ask.md template
412
429
  const systemPrompt = buildSystemPrompt("ask", ctx);
413
430
  // Log system prompt as a system message
414
431
  logMessage(db, sessionId, "system", systemPrompt);
@@ -425,15 +442,34 @@ export async function runAsk(options) {
425
442
  .join("\n\n");
426
443
  userRequest = `[Resumed session context]\n${priorContext}\n\n[New question]: ${options.question}`;
427
444
  }
428
- // 6. Execute agent loop (Req 9.5, 9.6)
429
- // 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
430
447
  try {
431
- await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "ask", db, systemPrompt);
432
- // 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
433
458
  updateSessionStatus(db, sessionId, "completed");
434
459
  }
435
460
  catch (err) {
436
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
+ }
437
473
  uiError(message);
438
474
  updateSessionStatus(db, sessionId, "failed", message);
439
475
  process.exit(1);
@@ -443,33 +479,34 @@ export async function runAsk(options) {
443
479
  * Execute the plan command.
444
480
  *
445
481
  * Flow:
446
- * 1. Load configuration and detect project type (Req 10.1)
447
- * 2. Create or resume session with mode: "plan" (Req 10.2, 10.8)
448
- * 3. Build system prompt from plan.md template (Req 10.3)
449
- * 4. Expose only read-only tools (Req 10.4)
450
- * 5. Execute agent loop (Req 10.5)
451
- * 6. Render structured plan to terminal (Req 10.6)
452
- * 7. Save to file if --output flag provided (Req 10.7)
453
- * 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
454
490
  *
455
- * Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8
456
491
  */
457
492
  export async function runPlan(options) {
458
493
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
459
- // 1. Load configuration (Req 10.1)
494
+ // 1. Load configuration
460
495
  const config = getConfig(projectRoot, {
461
496
  quiet: options.quiet,
497
+ provider: options.provider,
498
+ model: options.model,
462
499
  });
463
500
  // Initialize UI with config settings
464
- initUI(config.ui.color, config.ui.quiet);
465
- // 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
466
503
  detectProjectType(projectRoot);
467
- // 2. Initialize database and create/resume session (Req 10.2, 10.8)
504
+ // 2. Initialize database and create/resume session
468
505
  const db = initializeDatabase();
469
506
  let sessionId;
470
507
  let resumedMessages = [];
471
508
  if (options.session) {
472
- // Resume existing session (Req 10.8)
509
+ // Resume existing session
473
510
  const existing = getSession(db, options.session);
474
511
  if (!existing) {
475
512
  uiError(`Session not found: ${options.session}`);
@@ -484,14 +521,14 @@ export async function runPlan(options) {
484
521
  resumedMessages = rows;
485
522
  }
486
523
  else {
487
- // Create new session (Req 10.2)
524
+ // Create new session
488
525
  sessionId = randomUUID();
489
526
  createSession(db, {
490
527
  id: sessionId,
491
528
  command: "plan",
492
529
  projectRoot,
493
530
  provider: config.provider.default,
494
- model: config.provider.model,
531
+ model: resolveModel(config),
495
532
  });
496
533
  }
497
534
  // 3. Resolve provider (interactive setup if needed)
@@ -509,19 +546,19 @@ export async function runPlan(options) {
509
546
  updateSessionStatus(db, sessionId, "failed", String(err));
510
547
  process.exit(4);
511
548
  }
512
- // 4. Build execution context with mode: "plan" (Req 10.2)
549
+ // 4. Build execution context with mode: "plan"
513
550
  const ctx = {
514
551
  projectRoot,
515
552
  sessionId,
516
553
  provider: config.provider.default,
517
- model: config.provider.model,
554
+ model: resolveModel(config),
518
555
  mode: "plan", // plan is always read-only
519
556
  dryRun: false,
520
557
  assumeYes: false,
521
558
  maxIterations: config.agent.maxIterations,
522
559
  timeoutSeconds: config.agent.timeoutSeconds,
523
560
  };
524
- // 5. Build system prompt from plan.md template (Req 10.3)
561
+ // 5. Build system prompt from plan.md template
525
562
  const systemPrompt = buildSystemPrompt("plan", ctx, { userGoal: options.goal });
526
563
  // Log system prompt as a system message
527
564
  logMessage(db, sessionId, "system", systemPrompt);
@@ -535,17 +572,21 @@ export async function runPlan(options) {
535
572
  .join("\n\n");
536
573
  userRequest = `[Resumed session context]\n${priorContext}\n\n[New goal]: ${options.goal}`;
537
574
  }
538
- // 6. Execute agent loop — streams response to stdout (Req 10.5, 10.6)
575
+ // 6. Execute agent loop — streams response to stdout
539
576
  try {
540
577
  const planContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "plan", db, systemPrompt);
541
- // 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
542
583
  if (options.output) {
543
584
  const outputPath = path.resolve(options.output);
544
585
  const outputDir = path.dirname(outputPath);
545
586
  if (!existsSync(outputDir)) {
546
587
  mkdirSync(outputDir, { recursive: true });
547
588
  }
548
- writeFileSync(outputPath, planContent, "utf-8");
589
+ writeFileAtomic(outputPath, planContent);
549
590
  info(`Plan saved to ${options.output}`);
550
591
  }
551
592
  // 8. Mark session as completed (Req 10.8)
@@ -553,6 +594,17 @@ export async function runPlan(options) {
553
594
  }
554
595
  catch (err) {
555
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
+ }
556
608
  uiError(message);
557
609
  updateSessionStatus(db, sessionId, "failed", message);
558
610
  process.exit(1);
@@ -562,36 +614,42 @@ export async function runPlan(options) {
562
614
  * Execute the patch command.
563
615
  *
564
616
  * Flow:
565
- * 1. Load configuration and detect project type (Req 11.1)
566
- * 2. Create session with mode: "build" (Req 11.2)
567
- * 3. Build system prompt from patch.md template (Req 11.3)
568
- * 4. Expose read-only + mutation tools (Req 11.4)
569
- * 5. Execute agent loop — agent calls propose_diff (Req 11.4, 11.5)
570
- * 6. Render diff preview with syntax highlighting (Req 11.6)
571
- * 7. Render mutation summary (Req 11.7)
572
- * 8. If --dry-run, exit with code 0 (Req 11.8, 17.3, 17.4)
573
- * 9. If not --yes, prompt for confirmation (Req 11.9, 17.5)
574
- * 10. Agent calls apply_diff atomically (Req 11.10, 11.11, 11.12)
575
- * 11. Log mutation to database (Req 11.11)
576
- * 12. Display rollback hints (Req 11.12, 17.9)
577
- * 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
578
630
  *
579
- * Requirements: 11.1–11.13, 17.1–17.9
580
631
  */
581
632
  export async function runPatch(options) {
582
633
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
583
- // 1. Load configuration (Req 11.1)
634
+ // 1. Load configuration
584
635
  const config = getConfig(projectRoot, {
585
636
  quiet: options.quiet,
637
+ provider: options.provider,
638
+ model: options.model,
586
639
  });
587
640
  // Apply flag overrides to config
588
641
  if (options.dryRun)
589
642
  config.agent.mode = "build"; // keep build mode, dryRun handled via ctx
590
643
  // Initialize UI with config settings
591
644
  initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
592
- // 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
593
651
  detectProjectType(projectRoot);
594
- // 2. Initialize database and create session (Req 11.13)
652
+ // 2. Initialize database and create session
595
653
  const db = initializeDatabase();
596
654
  let sessionId;
597
655
  if (options.session) {
@@ -610,7 +668,7 @@ export async function runPatch(options) {
610
668
  command: "patch",
611
669
  projectRoot,
612
670
  provider: config.provider.default,
613
- model: config.provider.model,
671
+ model: resolveModel(config),
614
672
  });
615
673
  }
616
674
  // 3. Resolve provider (interactive setup if needed)
@@ -628,22 +686,22 @@ export async function runPatch(options) {
628
686
  updateSessionStatus(db, sessionId, "failed", String(err));
629
687
  process.exit(4);
630
688
  }
631
- // 4. Build execution context with mode: "build" (Req 11.2, 17.1, 17.2)
689
+ // 4. Build execution context with mode: "build"
632
690
  const ctx = {
633
691
  projectRoot,
634
692
  sessionId,
635
693
  provider: config.provider.default,
636
- model: config.provider.model,
694
+ model: resolveModel(config),
637
695
  mode: "build",
638
696
  dryRun: Boolean(options.dryRun),
639
697
  assumeYes: Boolean(options.yes),
640
698
  maxIterations: config.agent.maxIterations,
641
699
  timeoutSeconds: config.agent.timeoutSeconds,
642
700
  };
643
- // 5. Build system prompt from patch.md template (Req 11.3)
701
+ // 5. Build system prompt from patch.md template
644
702
  const systemPrompt = buildSystemPrompt("patch", ctx);
645
703
  logMessage(db, sessionId, "system", systemPrompt);
646
- // Expose read-only tools + mutation tools (Req 11.4)
704
+ // Expose read-only tools + mutation tools
647
705
  const patchTools = [
648
706
  readFileTool,
649
707
  listDirectoryTool,
@@ -656,15 +714,25 @@ export async function runPatch(options) {
656
714
  if (options.dryRun) {
657
715
  info(bold("Dry-run mode — changes will be previewed but not applied."));
658
716
  }
659
- // 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
660
728
  // The agent loop handles:
661
- // - propose_diff: generates diff + MutationSummary (Req 11.4, 11.5)
662
- // - dry-run enforcement: skips apply_diff (Req 11.8, 17.3, 17.4)
663
- // - confirmation prompt before apply_diff (Req 11.9, 17.5, 17.6)
664
- // - 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
665
733
  try {
666
734
  await agentLoop(ctx, options.description, patchTools, provider, config, "patch", db, systemPrompt);
667
- // 12. Display rollback hints after successful application (Req 11.12, 17.9)
735
+ // 12. Display rollback hints after successful application
668
736
  // The agent loop streams the response which includes rollback hints from
669
737
  // the apply_diff result. We add a final summary line here.
670
738
  if (!options.dryRun) {
@@ -676,22 +744,37 @@ export async function runPatch(options) {
676
744
  info("");
677
745
  info(yellow("Dry-run complete — no files were modified."));
678
746
  }
679
- // 13. Mark session as completed (Req 11.13)
747
+ // 13. Mark session as completed
680
748
  updateSessionStatus(db, sessionId, "completed");
681
749
  }
682
750
  catch (err) {
683
751
  const message = err instanceof Error ? err.message : String(err);
684
- // Handle user cancellation (Req 17.6, exit code 130)
752
+ // Handle user cancellation
685
753
  if (err instanceof UserCancelledError || err instanceof ConfirmCancelledError) {
686
754
  info("");
687
755
  info(yellow("Operation cancelled."));
688
756
  updateSessionStatus(db, sessionId, "cancelled");
689
757
  process.exit(130);
690
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
+ }
691
770
  uiError(message);
692
771
  updateSessionStatus(db, sessionId, "failed", message);
693
772
  process.exit(1);
694
773
  }
774
+ finally {
775
+ // Clear diff render options after patch completes
776
+ setDiffRenderOptions(null);
777
+ }
695
778
  }
696
779
  /**
697
780
  * Read git diff based on the provided options.
@@ -700,21 +783,20 @@ export async function runPatch(options) {
700
783
  * - --unstaged: unstaged changes (`git diff`)
701
784
  * - --branch <base>: compare to base branch (`git diff <base>...HEAD`)
702
785
  *
703
- * Requirements: 12.3, 12.4, 12.5
704
786
  */
705
787
  function readGitDiff(options, projectRoot) {
706
788
  try {
707
789
  let args;
708
790
  if (options.branch) {
709
- // Compare current branch to specified base (Req 12.5)
791
+ // Compare current branch to specified base
710
792
  args = ["diff", `${options.branch}...HEAD`];
711
793
  }
712
794
  else if (options.unstaged) {
713
- // Unstaged changes (Req 12.4)
795
+ // Unstaged changes
714
796
  args = ["diff"];
715
797
  }
716
798
  else {
717
- // Staged changes — default (Req 12.3)
799
+ // Staged changes — default
718
800
  args = ["diff", "--cached"];
719
801
  }
720
802
  const output = execFileSync("git", args, {
@@ -734,7 +816,6 @@ function readGitDiff(options, projectRoot) {
734
816
  * Extracts summary, issues (with severity), and suggestions from the
735
817
  * "# Code Review" markdown format defined in review.md.
736
818
  *
737
- * Requirements: 12.7
738
819
  */
739
820
  function parseReviewResponse(content) {
740
821
  const result = {
@@ -785,7 +866,6 @@ function parseReviewResponse(content) {
785
866
  /**
786
867
  * Render the structured review to the terminal in readable format.
787
868
  *
788
- * Requirements: 12.8
789
869
  */
790
870
  function renderReview(review) {
791
871
  info("");
@@ -824,30 +904,31 @@ function renderReview(review) {
824
904
  * Execute the review command.
825
905
  *
826
906
  * Flow:
827
- * 1. Parse flags and load configuration (Req 12.1)
828
- * 2. Detect project type (Req 12.1)
829
- * 3. Create session with mode: "plan" (Req 12.2)
830
- * 4. Read git diff (staged / unstaged / branch) (Req 12.3, 12.4, 12.5)
831
- * 5. Build system prompt from review.md template (Req 12.6)
832
- * 6. Send diff + project context to provider (Req 12.6)
833
- * 7. Parse structured review (summary, issues, suggestions) (Req 12.7)
834
- * 8. Render review to terminal (Req 12.8)
835
- * 9. Output JSON if --format json (Req 12.9)
836
- * 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
837
917
  *
838
- * Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9, 12.10
839
918
  */
840
919
  export async function runReview(options) {
841
920
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
842
- // 1. Load configuration (Req 12.1)
921
+ // 1. Load configuration
843
922
  const config = getConfig(projectRoot, {
844
923
  quiet: options.quiet,
924
+ provider: options.provider,
925
+ model: options.model,
845
926
  });
846
927
  // Initialize UI with config settings
847
- initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
848
- // 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
849
930
  detectProjectType(projectRoot);
850
- // 3. Initialize database and create session (Req 12.10)
931
+ // 3. Initialize database and create session
851
932
  const db = initializeDatabase();
852
933
  const sessionId = randomUUID();
853
934
  createSession(db, {
@@ -855,7 +936,7 @@ export async function runReview(options) {
855
936
  command: "review",
856
937
  projectRoot,
857
938
  provider: config.provider.default,
858
- model: config.provider.model,
939
+ model: resolveModel(config),
859
940
  });
860
941
  // Resolve provider (interactive setup if needed)
861
942
  let provider;
@@ -872,19 +953,19 @@ export async function runReview(options) {
872
953
  updateSessionStatus(db, sessionId, "failed", String(err));
873
954
  process.exit(4);
874
955
  }
875
- // Build execution context with mode: "plan" (Req 12.2)
956
+ // Build execution context with mode: "plan"
876
957
  const ctx = {
877
958
  projectRoot,
878
959
  sessionId,
879
960
  provider: config.provider.default,
880
- model: config.provider.model,
961
+ model: resolveModel(config),
881
962
  mode: "plan", // review is always read-only
882
963
  dryRun: false,
883
964
  assumeYes: false,
884
965
  maxIterations: config.agent.maxIterations,
885
966
  timeoutSeconds: config.agent.timeoutSeconds,
886
967
  };
887
- // 4. Read git diff (Req 12.3, 12.4, 12.5)
968
+ // 4. Read git diff
888
969
  let diff;
889
970
  try {
890
971
  diff = readGitDiff(options, projectRoot);
@@ -905,10 +986,10 @@ export async function runReview(options) {
905
986
  updateSessionStatus(db, sessionId, "completed");
906
987
  return;
907
988
  }
908
- // 5. Build system prompt from review.md template (Req 12.6)
989
+ // 5. Build system prompt from review.md template
909
990
  const systemPrompt = buildSystemPrompt("review", ctx);
910
991
  logMessage(db, sessionId, "system", systemPrompt);
911
- // 6. Build user request: diff + project context (Req 12.6)
992
+ // 6. Build user request: diff + project context
912
993
  const project = detectProjectType(projectRoot);
913
994
  const diffSource = options.branch
914
995
  ? `branch diff (current vs ${options.branch})`
@@ -928,23 +1009,28 @@ export async function runReview(options) {
928
1009
  ]
929
1010
  .filter((l) => l !== null)
930
1011
  .join("\n");
931
- // Execute agent loop — streams response to stdout (Req 12.6, 12.7, 12.8)
1012
+ // Execute agent loop — streams response to stdout
932
1013
  try {
933
1014
  const reviewContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "review", db, systemPrompt);
934
- // 7. Parse structured review (Req 12.7)
1015
+ // 7. Parse structured review
935
1016
  const review = parseReviewResponse(reviewContent);
936
- // 8 & 9. Render or output JSON (Req 12.8, 12.9)
937
- if (options.format === "json") {
938
- // JSON output to stdout (Req 12.9)
939
- 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');
940
1026
  }
941
1027
  else {
942
- // Render to terminal in readable format (Req 12.8)
1028
+ // Render to terminal in readable format
943
1029
  // Note: agentLoop already streamed the raw response; renderReview
944
1030
  // provides a structured re-render for clarity.
945
1031
  renderReview(review);
946
1032
  }
947
- // 10. Mark session as completed (Req 12.10)
1033
+ // 10. Mark session as completed
948
1034
  updateSessionStatus(db, sessionId, "completed");
949
1035
  }
950
1036
  catch (err) {
@@ -956,6 +1042,17 @@ export async function runReview(options) {
956
1042
  updateSessionStatus(db, sessionId, "cancelled");
957
1043
  process.exit(130);
958
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
+ }
959
1056
  uiError(message);
960
1057
  updateSessionStatus(db, sessionId, "failed", message);
961
1058
  process.exit(1);
@@ -965,31 +1062,32 @@ export async function runReview(options) {
965
1062
  * Execute the explore command.
966
1063
  *
967
1064
  * Flow:
968
- * 1. Parse flags and load configuration (Req 13.1)
969
- * 2. Detect project type (Req 13.1)
970
- * 3. Create session with mode: "plan" (Req 13.2)
971
- * 4. Scan repository structure respecting .gitignore (Req 13.3)
972
- * 5. Detect frameworks and key configuration files (Req 13.4)
973
- * 6. Identify entry points based on project type (Req 13.5)
974
- * 7. Build system prompt from explore.md template (Req 13.3–13.6)
975
- * 8. Execute agent loop to summarize structure/patterns (Req 13.6)
976
- * 9. Render exploration summary to terminal (Req 13.7)
977
- * 10. Save to ./.aria/explore.md if --save flag (Req 13.8)
978
- * 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
979
1076
  *
980
- * Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9, 13.10
981
1077
  */
982
1078
  export async function runExplore(options) {
983
1079
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
984
- // 1. Load configuration (Req 13.1)
1080
+ // 1. Load configuration
985
1081
  const config = getConfig(projectRoot, {
986
1082
  quiet: options.quiet,
1083
+ provider: options.provider,
1084
+ model: options.model,
987
1085
  });
988
1086
  // Initialize UI with config settings
989
- initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
990
- // 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
991
1089
  const project = detectProjectType(projectRoot);
992
- // 2. Initialize database and create session (Req 13.10)
1090
+ // 2. Initialize database and create session
993
1091
  const db = initializeDatabase();
994
1092
  const sessionId = randomUUID();
995
1093
  createSession(db, {
@@ -997,7 +1095,7 @@ export async function runExplore(options) {
997
1095
  command: "explore",
998
1096
  projectRoot,
999
1097
  provider: config.provider.default,
1000
- model: config.provider.model,
1098
+ model: resolveModel(config),
1001
1099
  });
1002
1100
  // 3. Resolve provider (interactive setup if needed)
1003
1101
  let provider;
@@ -1014,22 +1112,22 @@ export async function runExplore(options) {
1014
1112
  updateSessionStatus(db, sessionId, "failed", String(err));
1015
1113
  process.exit(4);
1016
1114
  }
1017
- // 4. Build execution context with mode: "plan" (Req 13.2)
1115
+ // 4. Build execution context with mode: "plan"
1018
1116
  const ctx = {
1019
1117
  projectRoot,
1020
1118
  sessionId,
1021
1119
  provider: config.provider.default,
1022
- model: config.provider.model,
1120
+ model: resolveModel(config),
1023
1121
  mode: "plan", // explore is always read-only
1024
1122
  dryRun: false,
1025
1123
  assumeYes: false,
1026
1124
  maxIterations: config.agent.maxIterations,
1027
1125
  timeoutSeconds: config.agent.timeoutSeconds,
1028
1126
  };
1029
- // 5. Build system prompt from explore.md template (Req 13.3–13.6)
1127
+ // 5. Build system prompt from explore.md template
1030
1128
  const systemPrompt = buildSystemPrompt("explore", ctx);
1031
1129
  logMessage(db, sessionId, "system", systemPrompt);
1032
- // 6. Build user request with project context and depth hint (Req 13.9)
1130
+ // 6. Build user request with project context and depth hint
1033
1131
  const frameworkInfo = project.framework
1034
1132
  ? `${project.framework.name}${project.framework.version ? ` ${project.framework.version}` : ""}${project.framework.router ? ` (${project.framework.router} router)` : ""}`
1035
1133
  : "none";
@@ -1053,20 +1151,24 @@ export async function runExplore(options) {
1053
1151
  ]
1054
1152
  .filter((l) => l !== null)
1055
1153
  .join("\n");
1056
- // 7. Execute agent loop — streams response to stdout (Req 13.6, 13.7)
1154
+ // 7. Execute agent loop — streams response to stdout
1057
1155
  try {
1058
1156
  const exploreContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "explore", db, systemPrompt);
1059
- // 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
1060
1162
  if (options.save) {
1061
1163
  const ariaDir = path.join(projectRoot, ".aria");
1062
1164
  const savePath = path.join(ariaDir, "explore.md");
1063
1165
  if (!existsSync(ariaDir)) {
1064
1166
  mkdirSync(ariaDir, { recursive: true });
1065
1167
  }
1066
- writeFileSync(savePath, exploreContent, "utf-8");
1168
+ writeFileAtomic(savePath, exploreContent);
1067
1169
  info(`Exploration summary saved to .aria/explore.md`);
1068
1170
  }
1069
- // 9. Mark session as completed (Req 13.10)
1171
+ // 9. Mark session as completed
1070
1172
  updateSessionStatus(db, sessionId, "completed");
1071
1173
  }
1072
1174
  catch (err) {
@@ -1077,6 +1179,17 @@ export async function runExplore(options) {
1077
1179
  updateSessionStatus(db, sessionId, "cancelled");
1078
1180
  process.exit(130);
1079
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
+ }
1080
1193
  uiError(message);
1081
1194
  updateSessionStatus(db, sessionId, "failed", message);
1082
1195
  process.exit(1);
@@ -1086,7 +1199,6 @@ export async function runExplore(options) {
1086
1199
  * Format a SQLite timestamp string into a human-readable relative time.
1087
1200
  * e.g. "2 hours ago", "3 days ago", "just now"
1088
1201
  *
1089
- * Requirements: 14.7
1090
1202
  */
1091
1203
  function formatTimestamp(timestamp) {
1092
1204
  // SQLite CURRENT_TIMESTAMP returns "YYYY-MM-DD HH:MM:SS" (space, no T, no Z).
@@ -1138,8 +1250,20 @@ function colorizeStatus(status) {
1138
1250
  * Render a tool execution tree for a session.
1139
1251
  * Shows tool calls in chronological order with input/output summaries.
1140
1252
  *
1141
- * Requirements: 14.6
1142
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
+ }
1143
1267
  function renderToolTree(db, sessionId) {
1144
1268
  const executions = db
1145
1269
  .prepare(`SELECT tool_name, input, output, error, created_at
@@ -1157,7 +1281,8 @@ function renderToolTree(db, sessionId) {
1157
1281
  const prefix = isLast ? "└─" : "├─";
1158
1282
  const childPrefix = isLast ? " " : "│ ";
1159
1283
  const statusIcon = exec.error ? red("✗") : green("✓");
1160
- 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))}`);
1161
1286
  // Show a brief summary of the input
1162
1287
  try {
1163
1288
  const inputObj = JSON.parse(exec.input);
@@ -1190,23 +1315,88 @@ function renderToolTree(db, sessionId) {
1190
1315
  * Execute the history command.
1191
1316
  *
1192
1317
  * Flow:
1193
- * 1. If no --session flag: list recent sessions in a table (Req 14.2, 14.3, 14.4)
1194
- * 2. If --session flag: display full session log (Req 14.5)
1195
- * 3. If --tree flag: render tool execution tree (Req 14.6)
1196
- * 4. Format timestamps in human-readable format (Req 14.7)
1197
- * 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
1198
1323
  *
1199
- * Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.8
1200
1324
  */
1201
1325
  export async function runHistory(options) {
1202
1326
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
1203
1327
  // Load configuration and initialize UI
1204
1328
  const config = getConfig(projectRoot, { quiet: options.quiet });
1205
- 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'));
1206
1330
  // Initialize database
1207
1331
  const db = initializeDatabase();
1208
1332
  // ---------------------------------------------------------------------------
1209
- // 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
1210
1400
  // ---------------------------------------------------------------------------
1211
1401
  if (options.session) {
1212
1402
  const session = getSession(db, options.session);
@@ -1264,7 +1454,7 @@ export async function runHistory(options) {
1264
1454
  if (toolCount > 0) {
1265
1455
  info(bold(`Tool Executions (${toolCount}):`));
1266
1456
  if (options.tree) {
1267
- // Render as tree (Req 14.6)
1457
+ // Render as tree
1268
1458
  renderToolTree(db, options.session);
1269
1459
  }
1270
1460
  else {
@@ -1285,30 +1475,55 @@ export async function runHistory(options) {
1285
1475
  return;
1286
1476
  }
1287
1477
  // ---------------------------------------------------------------------------
1288
- // Case 2: No --session flag — list recent sessions (Req 14.2, 14.3, 14.4)
1478
+ // Case 2: No --session flag — list recent sessions
1289
1479
  // ---------------------------------------------------------------------------
1290
1480
  const PAGE_SIZE = 20;
1291
1481
  const limit = options.limit ?? PAGE_SIZE;
1292
- // Fetch sessions with pagination support (Req 14.8)
1482
+ // Fetch sessions with pagination support
1293
1483
  const sessions = listSessions(db, { limit });
1294
1484
  if (sessions.length === 0) {
1295
- 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));
1296
1503
  return;
1297
1504
  }
1298
- // Build table rows (Req 14.3)
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
+ }
1511
+ return;
1512
+ }
1513
+ // Build table rows
1299
1514
  const rows = sessions.map((s) => [
1300
1515
  dim(s.id.slice(0, 8)), // abbreviated ID
1301
1516
  cyan(s.command),
1302
- formatTimestamp(s.createdAt), // human-readable timestamp (Req 14.7)
1517
+ formatTimestamp(s.createdAt), // human-readable timestamp
1303
1518
  colorizeStatus(s.status),
1304
1519
  ]);
1305
- // Render table with cli-table3 (Req 14.3, 20.4)
1520
+ // Render table with cli-table3
1306
1521
  const table = renderTable({
1307
1522
  head: ["ID", "Command", "When", "Status"],
1308
1523
  colWidths: [12, 12, 20, 12],
1309
1524
  }, rows);
1310
1525
  info(table);
1311
- // Show pagination hint if there may be more results (Req 14.8)
1526
+ // Show pagination hint if there may be more results
1312
1527
  if (sessions.length === limit && !options.limit) {
1313
1528
  info(dim(`\nShowing ${limit} most recent sessions. Use --limit <n> to see more.`));
1314
1529
  }
@@ -1399,7 +1614,6 @@ function parseConfigValue(value) {
1399
1614
  }
1400
1615
  /**
1401
1616
  * Display the effective configuration with precedence sources.
1402
- * Requirements: 15.2
1403
1617
  */
1404
1618
  function displayEffectiveConfig(projectRoot, config) {
1405
1619
  const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
@@ -1440,13 +1654,12 @@ function displayEffectiveConfig(projectRoot, config) {
1440
1654
  * Execute the config command.
1441
1655
  *
1442
1656
  * Subcommands:
1443
- * - (none): Display effective configuration with precedence sources (Req 15.2)
1444
- * - get <key>: Display value for specified key (Req 15.3)
1445
- * - set <key> <value>: Write key-value to ~/.aria/config.toml (Req 15.4–15.6, 15.10)
1446
- * - path: Display configuration file resolution paths (Req 15.7)
1447
- * - 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
1448
1662
  *
1449
- * Requirements: 15.1–15.10
1450
1663
  */
1451
1664
  export async function runConfig(options) {
1452
1665
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
@@ -1456,14 +1669,14 @@ export async function runConfig(options) {
1456
1669
  const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
1457
1670
  const projectConfigPath = path.join(projectRoot, ".aria.toml");
1458
1671
  // ---------------------------------------------------------------------------
1459
- // No subcommand: display effective configuration (Req 15.2)
1672
+ // No subcommand: display effective configuration
1460
1673
  // ---------------------------------------------------------------------------
1461
1674
  if (!options.subcommand) {
1462
1675
  displayEffectiveConfig(projectRoot, config);
1463
1676
  return;
1464
1677
  }
1465
1678
  // ---------------------------------------------------------------------------
1466
- // config path: display config file resolution paths (Req 15.7)
1679
+ // config path: display config file resolution paths
1467
1680
  // ---------------------------------------------------------------------------
1468
1681
  if (options.subcommand === "path") {
1469
1682
  info(bold("Configuration file paths:"));
@@ -1476,7 +1689,7 @@ export async function runConfig(options) {
1476
1689
  return;
1477
1690
  }
1478
1691
  // ---------------------------------------------------------------------------
1479
- // config get <key>: display value for key (Req 15.3)
1692
+ // config get <key>: display value for key
1480
1693
  // ---------------------------------------------------------------------------
1481
1694
  if (options.subcommand === "get") {
1482
1695
  const key = options.key;
@@ -1489,14 +1702,14 @@ export async function runConfig(options) {
1489
1702
  return;
1490
1703
  }
1491
1704
  // ---------------------------------------------------------------------------
1492
- // config set <key> <value>: write to user config (Req 15.4–15.6, 15.10)
1705
+ // config set <key> <value>: write to user config
1493
1706
  // ---------------------------------------------------------------------------
1494
1707
  if (options.subcommand === "set") {
1495
1708
  const key = options.key;
1496
1709
  const rawValue = options.value;
1497
1710
  // Parse the value to the appropriate type
1498
1711
  const parsedValue = parseConfigValue(rawValue);
1499
- // Validate by applying to current config and re-validating (Req 15.10)
1712
+ // Validate by applying to current config and re-validating
1500
1713
  const currentMerged = loadConfig(projectRoot);
1501
1714
  const updatedMerged = setNestedValue(currentMerged, key, parsedValue);
1502
1715
  let validatedConfig;
@@ -1512,16 +1725,16 @@ export async function runConfig(options) {
1512
1725
  ? readFileSync(userConfigPath, "utf-8")
1513
1726
  : "";
1514
1727
  const newContent = serializeConfigToToml(validatedConfig);
1515
- // Preview the diff (Req 15.5, 15.6)
1728
+ // Preview the diff
1516
1729
  const diffOutput = generateAndRenderDiff(userConfigPath, oldContent, newContent);
1517
1730
  info(bold("Preview:"));
1518
1731
  info(diffOutput);
1519
- // If --dry-run, exit without writing (Req 15.6)
1732
+ // If --dry-run, exit without writing
1520
1733
  if (options.dryRun) {
1521
1734
  info(yellow("Dry-run mode — no changes written."));
1522
1735
  return;
1523
1736
  }
1524
- // If not --yes, prompt for confirmation (Req 15.5)
1737
+ // If not --yes, prompt for confirmation
1525
1738
  if (!options.yes) {
1526
1739
  let confirmed;
1527
1740
  try {
@@ -1539,7 +1752,7 @@ export async function runConfig(options) {
1539
1752
  process.exit(130);
1540
1753
  }
1541
1754
  }
1542
- // Write to ~/.aria/config.toml (Req 15.4)
1755
+ // Write to ~/.aria/config.toml
1543
1756
  const ariaDir = path.join(os.homedir(), ".aria");
1544
1757
  if (!existsSync(ariaDir)) {
1545
1758
  mkdirSync(ariaDir, { recursive: true });
@@ -1549,7 +1762,7 @@ export async function runConfig(options) {
1549
1762
  return;
1550
1763
  }
1551
1764
  // ---------------------------------------------------------------------------
1552
- // config init: create ./.aria.toml with defaults (Req 15.8, 15.9)
1765
+ // config init: create ./.aria.toml with defaults
1553
1766
  // ---------------------------------------------------------------------------
1554
1767
  if (options.subcommand === "init") {
1555
1768
  // Generate default config content
@@ -1562,12 +1775,12 @@ export async function runConfig(options) {
1562
1775
  const diffOutput = generateAndRenderDiff(projectConfigPath, oldContent, defaultContent);
1563
1776
  info(bold("Preview (.aria.toml):"));
1564
1777
  info(diffOutput);
1565
- // If --dry-run, exit without writing (Req 17.4)
1778
+ // If --dry-run, exit without writing
1566
1779
  if (options.dryRun) {
1567
1780
  info(yellow("Dry-run mode — no file created."));
1568
1781
  return;
1569
1782
  }
1570
- // If not --yes, prompt for confirmation (Req 15.9)
1783
+ // If not --yes, prompt for confirmation
1571
1784
  if (!options.yes) {
1572
1785
  let confirmed;
1573
1786
  try {
@@ -1585,7 +1798,7 @@ export async function runConfig(options) {
1585
1798
  process.exit(130);
1586
1799
  }
1587
1800
  }
1588
- // Write ./.aria.toml (Req 15.8)
1801
+ // Write ./.aria.toml
1589
1802
  writeFileSync(projectConfigPath, defaultContent, "utf-8");
1590
1803
  info(green(`✓ Created ${projectConfigPath}`));
1591
1804
  return;
@@ -1597,7 +1810,6 @@ export async function runConfig(options) {
1597
1810
  * Runs a series of environment diagnostic checks and reports results.
1598
1811
  * Exits with code 1 if any critical check fails.
1599
1812
  *
1600
- * Requirements: 16.1–16.13
1601
1813
  */
1602
1814
  export async function runDoctor(options = {}) {
1603
1815
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
@@ -1612,7 +1824,7 @@ export async function runDoctor(options = {}) {
1612
1824
  }
1613
1825
  const checks = [];
1614
1826
  // -------------------------------------------------------------------------
1615
- // 1. Node.js version >= 20 (Req 16.2) — CRITICAL
1827
+ // 1. Node.js version >= 20
1616
1828
  // -------------------------------------------------------------------------
1617
1829
  {
1618
1830
  const nodeVersion = process.version; // e.g. "v20.11.0"
@@ -1629,7 +1841,7 @@ export async function runDoctor(options = {}) {
1629
1841
  }
1630
1842
  }
1631
1843
  // -------------------------------------------------------------------------
1632
- // 2. git availability (Req 16.3) — WARN only
1844
+ // 2. git availability — WARN only
1633
1845
  // -------------------------------------------------------------------------
1634
1846
  {
1635
1847
  try {
@@ -1641,7 +1853,7 @@ export async function runDoctor(options = {}) {
1641
1853
  }
1642
1854
  }
1643
1855
  // -------------------------------------------------------------------------
1644
- // 3. ripgrep (rg) availability (Req 16.4) — WARN only
1856
+ // 3. ripgrep (rg) availability — WARN only
1645
1857
  // -------------------------------------------------------------------------
1646
1858
  {
1647
1859
  try {
@@ -1653,7 +1865,7 @@ export async function runDoctor(options = {}) {
1653
1865
  }
1654
1866
  }
1655
1867
  // -------------------------------------------------------------------------
1656
- // 4. Config file syntax and schema validation (Req 16.5) — CRITICAL
1868
+ // 4. Config file syntax and schema validation — CRITICAL
1657
1869
  // -------------------------------------------------------------------------
1658
1870
  {
1659
1871
  const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
@@ -1680,7 +1892,7 @@ export async function runDoctor(options = {}) {
1680
1892
  }
1681
1893
  }
1682
1894
  // -------------------------------------------------------------------------
1683
- // 5. History DB accessibility and schema version (Req 16.6) — CRITICAL
1895
+ // 5. History DB accessibility and schema version — CRITICAL
1684
1896
  // -------------------------------------------------------------------------
1685
1897
  {
1686
1898
  try {
@@ -1698,34 +1910,44 @@ export async function runDoctor(options = {}) {
1698
1910
  }
1699
1911
  }
1700
1912
  // -------------------------------------------------------------------------
1701
- // 6. Provider readiness — API key presence (Req 16.7) CRITICAL
1913
+ // 6. Provider readiness — API key presence CRITICAL for default
1914
+ // v0.2.2: report all configured providers, fail only if default key is missing
1702
1915
  // -------------------------------------------------------------------------
1703
1916
  {
1704
- const provider = config?.provider.default ?? "anthropic";
1917
+ const defaultProvider = config?.provider.default ?? "anthropic";
1705
1918
  const keyMap = {
1706
1919
  anthropic: "ANTHROPIC_API_KEY",
1707
1920
  openai: "OPENAI_API_KEY",
1708
1921
  openrouter: "OPENROUTER_API_KEY",
1709
- ollama: "", // no key needed
1922
+ ollama: null, // no key needed
1710
1923
  };
1711
- const envKey = keyMap[provider];
1712
- if (!envKey) {
1713
- // Ollama no API key required
1714
- checks.push({ name: "provider", status: "pass", message: `${provider} (no API key required)` });
1924
+ // Critical check: default provider must be ready
1925
+ const defaultEnvKey = keyMap[defaultProvider];
1926
+ if (defaultEnvKey === null) {
1927
+ checks.push({ name: "provider", status: "pass", message: `${defaultProvider} (no API key required)` });
1715
1928
  }
1716
- else if (process.env[envKey]) {
1717
- checks.push({ name: "provider", status: "pass", message: `${provider} (${envKey} present)` });
1929
+ else if (process.env[defaultEnvKey]) {
1930
+ const model = config?.provider.model ?? "default";
1931
+ checks.push({ name: "provider", status: "pass", message: `${defaultProvider} (${defaultEnvKey} present, model: ${model})` });
1718
1932
  }
1719
1933
  else {
1720
1934
  checks.push({
1721
1935
  name: "provider",
1722
1936
  status: "fail",
1723
- message: `${provider} (${envKey} not set)`,
1937
+ message: `${defaultProvider} (${defaultEnvKey} not set)`,
1724
1938
  });
1725
1939
  }
1940
+ // Non-critical: only report secondary providers that are actually configured (key present)
1941
+ const secondaryProviders = Object.entries(keyMap).filter(([name]) => name !== defaultProvider && name !== "ollama");
1942
+ for (const [name, envKey] of secondaryProviders) {
1943
+ if (envKey && process.env[envKey]) {
1944
+ checks.push({ name: `provider:${name}`, status: "pass", message: `${name} (${envKey} present)` });
1945
+ }
1946
+ // Don't warn about unconfigured secondary providers — too noisy on vanilla installs
1947
+ }
1726
1948
  }
1727
1949
  // -------------------------------------------------------------------------
1728
- // 7. Project type detection (Req 16.8)
1950
+ // 7. Project type detection
1729
1951
  // -------------------------------------------------------------------------
1730
1952
  {
1731
1953
  try {
@@ -1744,7 +1966,7 @@ export async function runDoctor(options = {}) {
1744
1966
  }
1745
1967
  }
1746
1968
  // -------------------------------------------------------------------------
1747
- // 8. Prisma schema existence and model count (Req 16.9 + v0.2.0)
1969
+ // 8. Prisma schema existence and model count
1748
1970
  // -------------------------------------------------------------------------
1749
1971
  {
1750
1972
  try {
@@ -1784,7 +2006,7 @@ export async function runDoctor(options = {}) {
1784
2006
  }
1785
2007
  }
1786
2008
  // -------------------------------------------------------------------------
1787
- // 9. Ollama reachability if Ollama provider selected (Req 16.10) — WARN
2009
+ // 9. Ollama reachability if Ollama provider selected — WARN
1788
2010
  // -------------------------------------------------------------------------
1789
2011
  {
1790
2012
  const provider = config?.provider.default ?? "anthropic";
@@ -1807,10 +2029,22 @@ export async function runDoctor(options = {}) {
1807
2029
  // -------------------------------------------------------------------------
1808
2030
  const criticalNames = new Set(["nodejs", "config", "history_db", "provider"]);
1809
2031
  const hasCriticalFailure = checks.some((c) => c.status === "fail" && criticalNames.has(c.name));
1810
- if (options.format === "json") {
1811
- // JSON output (Req 16.12)
1812
- const allPassed = !checks.some((c) => c.status === "fail");
1813
- 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
+ }
1814
2048
  }
1815
2049
  else {
1816
2050
  // Text output (Req 16.11)
@@ -1836,7 +2070,7 @@ export async function runDoctor(options = {}) {
1836
2070
  info(green("All critical checks passed."));
1837
2071
  }
1838
2072
  }
1839
- // Exit with code 1 if any critical check fails (Req 16.13)
2073
+ // Exit with code 1 if any critical check fails
1840
2074
  if (hasCriticalFailure) {
1841
2075
  process.exit(1);
1842
2076
  }