markform 0.1.5 → 0.1.7

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 (31) hide show
  1. package/README.md +126 -68
  2. package/dist/ai-sdk.d.mts +1 -2
  3. package/dist/ai-sdk.mjs +3 -2
  4. package/dist/{apply-BCCiJzQr.mjs → apply-g23rRn7p.mjs} +29 -24
  5. package/dist/bin.d.mts +0 -1
  6. package/dist/bin.mjs +3 -6
  7. package/dist/{cli-D469amuk.mjs → cli-Bqlm-WWw.mjs} +1528 -730
  8. package/dist/cli.d.mts +0 -1
  9. package/dist/cli.mjs +2 -6
  10. package/dist/coreTypes-DCvD7feM.d.mts +3203 -0
  11. package/dist/{coreTypes-pyctKRgc.mjs → coreTypes-__Cwxz5q.mjs} +10 -3
  12. package/dist/index.d.mts +164 -8
  13. package/dist/index.mjs +6 -5
  14. package/dist/{session-uF0e6m6k.mjs → session-CgCNni0e.mjs} +3 -2
  15. package/dist/session-DruaYPZ1.mjs +4 -0
  16. package/dist/{shared-CZsyShck.mjs → shared-C9yW5FLZ.mjs} +2 -1
  17. package/dist/{shared-BqPnYXrn.mjs → shared-DQ6y3Ggc.mjs} +4 -2
  18. package/dist/{src-Df0XX7UB.mjs → src-BiuxbzF3.mjs} +495 -91
  19. package/docs/markform-apis.md +52 -1
  20. package/docs/markform-spec.md +11 -2
  21. package/examples/earnings-analysis/earnings-analysis.form.md +1 -0
  22. package/examples/movie-research/movie-research-basic.form.md +1 -0
  23. package/examples/movie-research/movie-research-deep.form.md +1 -0
  24. package/examples/movie-research/{movie-research-minimal.form.md → movie-research-demo.form.md} +4 -3
  25. package/examples/simple/simple.form.md +1 -0
  26. package/examples/simple/simple.schema.json +374 -0
  27. package/examples/startup-deep-research/startup-deep-research.form.md +1 -0
  28. package/examples/startup-research/startup-research.form.md +1 -0
  29. package/package.json +11 -9
  30. package/dist/coreTypes-9XZSNOv6.d.mts +0 -8951
  31. package/dist/session-B_stoXQn.mjs +0 -4
@@ -1,19 +1,87 @@
1
- import { L as PatchSchema } from "./coreTypes-pyctKRgc.mjs";
2
- import { A as parseRolesFlag, D as deriveReportPath, E as deriveExportPath, F as hasWebSearchSupport, I as parseModelIdForDisplay, M as WEB_SEARCH_CONFIG, N as formatSuggestedLlms, O as detectFileType, T as USER_ROLE, _ as DEFAULT_MAX_TURNS, b as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, d as serializeRawMarkdown, f as serializeReportMarkdown, g as DEFAULT_MAX_PATCHES_PER_TURN, h as DEFAULT_MAX_ISSUES_PER_TURN, j as SUGGESTED_LLMS, k as getFormsDir, m as DEFAULT_FORMS_DIR, p as AGENT_ROLE, r as inspect, t as applyPatches, u as serialize, v as DEFAULT_PORT, w as REPORT_EXTENSION, x as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN } from "./apply-BCCiJzQr.mjs";
3
- import { a as resolveHarnessConfig, c as getProviderNames, f as createMockAgent, i as runResearch, l as resolveModel, m as createHarness, s as getProviderInfo, t as VERSION, u as createLiveAgent, w as parseForm } from "./src-Df0XX7UB.mjs";
4
- import { n as serializeSession } from "./session-uF0e6m6k.mjs";
5
- import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-BqPnYXrn.mjs";
1
+
2
+ import { L as PatchSchema } from "./coreTypes-__Cwxz5q.mjs";
3
+ import { A as deriveSchemaPath, D as USER_ROLE, E as REPORT_EXTENSION, F as formatSuggestedLlms, L as hasWebSearchSupport, M as parseRolesFlag, N as SUGGESTED_LLMS, O as deriveExportPath, P as WEB_SEARCH_CONFIG, R as parseModelIdForDisplay, S as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, T as MAX_FORMS_IN_MENU, _ as DEFAULT_MAX_PATCHES_PER_TURN, d as serialize, f as serializeRawMarkdown, g as DEFAULT_MAX_ISSUES_PER_TURN, h as DEFAULT_FORMS_DIR, i as inspect, j as detectFileType, k as deriveReportPath, m as AGENT_ROLE, n as getAllFields, p as serializeReportMarkdown, t as applyPatches, v as DEFAULT_MAX_TURNS, x as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, y as DEFAULT_PORT } from "./apply-g23rRn7p.mjs";
4
+ import { E as parseForm, T as formToJsonSchema, a as resolveHarnessConfig, c as getProviderNames, f as createMockAgent, i as runResearch, l as resolveModel, m as createHarness, n as isResearchForm, o as fillForm, s as getProviderInfo, t as VERSION, u as createLiveAgent } from "./src-BiuxbzF3.mjs";
5
+ import { n as serializeSession } from "./session-CgCNni0e.mjs";
6
+ import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-DQ6y3Ggc.mjs";
6
7
  import YAML from "yaml";
7
- import { basename, dirname, join, resolve } from "node:path";
8
8
  import { Command } from "commander";
9
9
  import pc from "picocolors";
10
- import { existsSync, readFileSync } from "node:fs";
10
+ import { exec, execSync, spawn } from "node:child_process";
11
+ import { basename, dirname, join, resolve } from "node:path";
12
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
11
13
  import { fileURLToPath } from "node:url";
12
14
  import { readFile } from "node:fs/promises";
13
15
  import * as p from "@clack/prompts";
14
- import { exec, spawn } from "node:child_process";
15
16
  import { createServer } from "node:http";
16
17
 
18
+ //#region src/cli/lib/cliVersion.ts
19
+ /**
20
+ * CLI version helper that computes git version in development mode.
21
+ *
22
+ * When running via tsx (development), __MARKFORM_VERSION__ is not injected,
23
+ * so VERSION from index.ts returns 'development'. This module computes the
24
+ * actual git version dynamically for consistent version display.
25
+ */
26
+ /**
27
+ * Get version from git tags with format: X.Y.Z-dev.N.hash
28
+ * Only called when running in development mode (via tsx).
29
+ */
30
+ function getGitVersion() {
31
+ try {
32
+ const git = (args) => execSync(`git ${args}`, {
33
+ encoding: "utf-8",
34
+ stdio: [
35
+ "ignore",
36
+ "pipe",
37
+ "ignore"
38
+ ]
39
+ }).trim();
40
+ const tag = git("describe --tags --abbrev=0");
41
+ const tagVersion = tag.replace(/^v/, "");
42
+ const [major, minor, patch] = tagVersion.split(".").map(Number);
43
+ const commitsSinceTag = parseInt(git(`rev-list ${tag}..HEAD --count`), 10);
44
+ const hash = git("rev-parse --short=7 HEAD");
45
+ let dirty = false;
46
+ try {
47
+ git("diff --quiet");
48
+ git("diff --cached --quiet");
49
+ } catch {
50
+ dirty = true;
51
+ }
52
+ if (commitsSinceTag === 0 && !dirty) return tagVersion;
53
+ return `${major}.${minor}.${(patch ?? 0) + 1}-dev.${commitsSinceTag}.${dirty ? `${hash}-dirty` : hash}`;
54
+ } catch {
55
+ return "development";
56
+ }
57
+ }
58
+ /**
59
+ * CLI version - uses build-time VERSION if available, otherwise computes from git.
60
+ */
61
+ const CLI_VERSION = VERSION === "development" ? getGitVersion() : VERSION;
62
+
63
+ //#endregion
64
+ //#region src/cli/lib/paths.ts
65
+ /**
66
+ * Path utilities for CLI commands.
67
+ *
68
+ * This module contains Node.js-dependent path utilities that are only used
69
+ * by CLI code. Keeping these separate from settings.ts allows the core
70
+ * library to remain Node.js-free.
71
+ */
72
+ /**
73
+ * Resolve the forms directory path to an absolute path.
74
+ * Uses the provided override or falls back to DEFAULT_FORMS_DIR.
75
+ *
76
+ * @param override Optional override path from CLI --forms-dir option
77
+ * @param cwd Base directory for resolving relative paths (defaults to process.cwd())
78
+ * @returns Absolute path to the forms directory
79
+ */
80
+ function getFormsDir(override, cwd = process.cwd()) {
81
+ return resolve(cwd, override ?? DEFAULT_FORMS_DIR);
82
+ }
83
+
84
+ //#endregion
17
85
  //#region src/cli/commands/apis.ts
18
86
  /**
19
87
  * Get the path to the markform-apis.md file.
@@ -124,7 +192,7 @@ function formatState$2(state, useColors) {
124
192
  /**
125
193
  * Format apply report for console output.
126
194
  */
127
- function formatConsoleReport$2(report, useColors) {
195
+ function formatConsoleReport$3(report, useColors) {
128
196
  const lines = [];
129
197
  const bold = useColors ? pc.bold : (s) => s;
130
198
  const dim = useColors ? pc.dim : (s) => s;
@@ -206,7 +274,7 @@ function registerApplyCommand(program) {
206
274
  structure: result.structureSummary,
207
275
  progress: result.progressSummary,
208
276
  issues: result.issues
209
- }, (data, useColors) => formatConsoleReport$2(data, useColors));
277
+ }, (data, useColors) => formatConsoleReport$3(data, useColors));
210
278
  console.error(output);
211
279
  process.exit(1);
212
280
  }
@@ -218,7 +286,7 @@ function registerApplyCommand(program) {
218
286
  structure: result.structureSummary,
219
287
  progress: result.progressSummary,
220
288
  issues: result.issues
221
- }, (data, useColors) => formatConsoleReport$2(data, useColors));
289
+ }, (data, useColors) => formatConsoleReport$3(data, useColors));
222
290
  if (options.output) {
223
291
  await writeFile(options.output, output);
224
292
  logSuccess(ctx, `Report written to ${options.output}`);
@@ -238,34 +306,26 @@ function registerApplyCommand(program) {
238
306
  }
239
307
 
240
308
  //#endregion
241
- //#region src/cli/commands/docs.ts
309
+ //#region src/cli/lib/fileViewer.ts
242
310
  /**
243
- * Get the path to the markform-reference.md file.
244
- * Works both during development and when installed as a package.
311
+ * File viewer utility for displaying files with colorization and pagination.
312
+ *
313
+ * Provides a modern console experience:
314
+ * - Syntax highlighting for markdown and YAML
315
+ * - Pagination using system pager (less) when available
316
+ * - Fallback to console output when not interactive
245
317
  */
246
- function getDocsPath() {
247
- const thisDir = dirname(fileURLToPath(import.meta.url));
248
- if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-reference.md");
249
- return join(dirname(dirname(dirname(thisDir))), "docs", "markform-reference.md");
250
- }
251
318
  /**
252
- * Load the docs content.
319
+ * Check if stdout is an interactive terminal.
253
320
  */
254
- function loadDocs() {
255
- const docsPath = getDocsPath();
256
- try {
257
- return readFileSync(docsPath, "utf-8");
258
- } catch (error) {
259
- const message = error instanceof Error ? error.message : String(error);
260
- throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);
261
- }
321
+ function isInteractive$3() {
322
+ return process.stdout.isTTY === true;
262
323
  }
263
324
  /**
264
- * Apply basic terminal formatting to markdown content.
265
- * Colorizes headers, code blocks, and other elements for better readability.
325
+ * Apply terminal formatting to markdown content.
326
+ * Colorizes headers, code blocks, and other elements.
266
327
  */
267
- function formatMarkdown$3(content, useColors) {
268
- if (!useColors) return content;
328
+ function formatMarkdown$3(content) {
269
329
  const lines = content.split("\n");
270
330
  const formatted = [];
271
331
  let inCodeBlock = false;
@@ -276,11 +336,11 @@ function formatMarkdown$3(content, useColors) {
276
336
  continue;
277
337
  }
278
338
  if (inCodeBlock) {
279
- formatted.push(pc.dim(line));
339
+ formatted.push(pc.cyan(line));
280
340
  continue;
281
341
  }
282
342
  if (line.startsWith("# ")) {
283
- formatted.push(pc.bold(pc.cyan(line)));
343
+ formatted.push(pc.bold(pc.magenta(line)));
284
344
  continue;
285
345
  }
286
346
  if (line.startsWith("## ")) {
@@ -288,6 +348,10 @@ function formatMarkdown$3(content, useColors) {
288
348
  continue;
289
349
  }
290
350
  if (line.startsWith("### ")) {
351
+ formatted.push(pc.bold(pc.cyan(line)));
352
+ continue;
353
+ }
354
+ if (line.startsWith("#### ")) {
291
355
  formatted.push(pc.bold(line));
292
356
  continue;
293
357
  }
@@ -300,30 +364,290 @@ function formatMarkdown$3(content, useColors) {
300
364
  formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
301
365
  return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
302
366
  });
367
+ formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
368
+ return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
369
+ });
303
370
  formatted.push(formattedLine);
304
371
  }
305
372
  return formatted.join("\n");
306
373
  }
307
374
  /**
308
- * Check if stdout is an interactive terminal.
375
+ * Apply terminal formatting to YAML content.
309
376
  */
310
- function isInteractive$3() {
311
- return process.stdout.isTTY === true;
377
+ function formatYaml(content) {
378
+ const lines = content.split("\n");
379
+ const formatted = [];
380
+ for (const line of lines) {
381
+ if (line.trim().startsWith("#")) {
382
+ formatted.push(pc.dim(line));
383
+ continue;
384
+ }
385
+ const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
386
+ if (match) {
387
+ const [, indent, key, colon, value] = match;
388
+ formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
389
+ continue;
390
+ }
391
+ if (line.trim().startsWith("-")) {
392
+ formatted.push(pc.green(line));
393
+ continue;
394
+ }
395
+ formatted.push(line);
396
+ }
397
+ return formatted.join("\n");
312
398
  }
313
399
  /**
314
- * Display content.
400
+ * Format file content based on extension.
315
401
  */
316
- function displayContent$2(content) {
317
- console.log(content);
402
+ function formatContent(content, filename) {
403
+ if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
404
+ if (filename.endsWith(".md")) return formatMarkdown$3(content);
405
+ return content;
318
406
  }
319
407
  /**
320
- * Register the docs command.
408
+ * Display content using system pager (less) if available.
409
+ * Falls back to console.log if not interactive or pager unavailable.
410
+ *
411
+ * @returns Promise that resolves when viewing is complete
321
412
  */
322
- function registerDocsCommand(program) {
323
- program.command("docs").description("Display concise Markform syntax reference (agent-friendly)").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
413
+ async function displayWithPager(content, title) {
414
+ if (!isInteractive$3()) {
415
+ console.log(content);
416
+ return;
417
+ }
418
+ const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
419
+ return new Promise((resolve$1) => {
420
+ const pager = spawn("less", [
421
+ "-R",
422
+ "-S",
423
+ "-X",
424
+ "-F",
425
+ "-K"
426
+ ], { stdio: [
427
+ "pipe",
428
+ "inherit",
429
+ "inherit"
430
+ ] });
431
+ pager.on("error", () => {
432
+ console.log(header);
433
+ console.log("");
434
+ console.log(content);
435
+ console.log("");
436
+ resolve$1();
437
+ });
438
+ pager.on("close", () => {
439
+ resolve$1();
440
+ });
441
+ pager.stdin.write(header + "\n\n");
442
+ pager.stdin.write(content);
443
+ pager.stdin.end();
444
+ });
445
+ }
446
+ /**
447
+ * Load and display a file with formatting and pagination.
448
+ */
449
+ async function viewFile(filePath) {
450
+ const content = readFileSync(filePath, "utf-8");
451
+ const filename = basename(filePath);
452
+ await displayWithPager(formatContent(content, filename), filename);
453
+ }
454
+ /**
455
+ * Show an interactive file viewer chooser.
456
+ *
457
+ * Presents a list of files to view:
458
+ * - "Show report:" for the report output (.report.md) at the top
459
+ * - "Show source:" for other files (.form.md, .raw.md, .yml)
460
+ * - "Quit" at the bottom
461
+ *
462
+ * Loops until the user selects Quit.
463
+ *
464
+ * @param files Array of file options to display
465
+ */
466
+ async function showFileViewerChooser(files) {
467
+ if (!isInteractive$3()) return;
468
+ console.log("");
469
+ const reportFile = files.find((f) => f.path.endsWith(".report.md"));
470
+ const sourceFiles = files.filter((f) => !f.path.endsWith(".report.md"));
471
+ while (true) {
472
+ const options = [];
473
+ if (reportFile) options.push({
474
+ value: reportFile.path,
475
+ label: `Show report: ${pc.green(basename(reportFile.path))}`,
476
+ hint: reportFile.hint ?? ""
477
+ });
478
+ for (const file of sourceFiles) options.push({
479
+ value: file.path,
480
+ label: `Show source: ${pc.green(basename(file.path))}`,
481
+ hint: file.hint ?? ""
482
+ });
483
+ options.push({
484
+ value: "quit",
485
+ label: "Quit",
486
+ hint: ""
487
+ });
488
+ const selection = await p.select({
489
+ message: "View files:",
490
+ options
491
+ });
492
+ if (p.isCancel(selection) || selection === "quit") break;
493
+ await viewFile(selection);
494
+ console.log("");
495
+ }
496
+ }
497
+
498
+ //#endregion
499
+ //#region src/cli/commands/browse.ts
500
+ /**
501
+ * Browse command - Interactive file browser for the forms directory.
502
+ *
503
+ * Provides a menu-based interface for viewing files with syntax highlighting
504
+ * and pagination. Shows .form.md, .report.md, .yml, and .schema.json files.
505
+ *
506
+ * Usage:
507
+ * markform browse # Browse all files in forms/
508
+ * markform browse --filter=foo # Only show files matching pattern
509
+ */
510
+ /** File extensions we support viewing */
511
+ const VIEWABLE_EXTENSIONS = [
512
+ ".form.md",
513
+ ".report.md",
514
+ ".yml",
515
+ ".yaml",
516
+ ".schema.json",
517
+ ".raw.md"
518
+ ];
519
+ /**
520
+ * Check if a file has a viewable extension.
521
+ */
522
+ function isViewableFile(filename) {
523
+ return VIEWABLE_EXTENSIONS.some((ext) => filename.endsWith(ext));
524
+ }
525
+ /**
526
+ * Get the display extension for sorting/grouping.
527
+ */
528
+ function getExtension(filename) {
529
+ for (const ext of VIEWABLE_EXTENSIONS) if (filename.endsWith(ext)) return ext;
530
+ return "";
531
+ }
532
+ /**
533
+ * Scan forms directory for viewable files.
534
+ */
535
+ function scanFormsDirectory$1(formsDir, filter) {
536
+ const entries = [];
537
+ try {
538
+ const files = readdirSync(formsDir);
539
+ for (const file of files) {
540
+ if (!isViewableFile(file)) continue;
541
+ if (filter && !file.toLowerCase().includes(filter.toLowerCase())) continue;
542
+ const fullPath = join(formsDir, file);
543
+ try {
544
+ const stat = statSync(fullPath);
545
+ if (stat.isFile()) entries.push({
546
+ path: fullPath,
547
+ filename: file,
548
+ mtime: stat.mtime,
549
+ extension: getExtension(file)
550
+ });
551
+ } catch {}
552
+ }
553
+ } catch {}
554
+ entries.sort((a, b) => {
555
+ const timeDiff = b.mtime.getTime() - a.mtime.getTime();
556
+ if (timeDiff !== 0) return timeDiff;
557
+ return a.filename.localeCompare(b.filename);
558
+ });
559
+ return entries;
560
+ }
561
+ /**
562
+ * Get extension hint for display.
563
+ */
564
+ function getExtensionHint(ext) {
565
+ switch (ext) {
566
+ case ".form.md": return "markform source";
567
+ case ".report.md": return "output report";
568
+ case ".yml":
569
+ case ".yaml": return "YAML values";
570
+ case ".schema.json": return "JSON Schema";
571
+ case ".raw.md": return "raw markdown";
572
+ default: return "";
573
+ }
574
+ }
575
+ /**
576
+ * Format file entry for menu display.
577
+ */
578
+ function formatFileLabel(entry) {
579
+ return `${entry.extension === ".report.md" ? pc.green("*") : " "} ${entry.filename}`;
580
+ }
581
+ /**
582
+ * Browse specific output files after a form run.
583
+ *
584
+ * This is a convenience function for the examples workflow that shows
585
+ * the standard output files (report, yml, form, schema) for a completed form.
586
+ *
587
+ * @param basePath - Base path of the output (e.g., "forms/movie-research-demo-filled1")
588
+ */
589
+ async function browseOutputFiles(basePath) {
590
+ const outputExtensions = [
591
+ ".report.md",
592
+ ".yml",
593
+ ".form.md",
594
+ ".schema.json"
595
+ ];
596
+ const files = [];
597
+ for (const ext of outputExtensions) {
598
+ const fullPath = basePath + ext;
599
+ try {
600
+ statSync(fullPath);
601
+ files.push({
602
+ path: fullPath,
603
+ label: basename(fullPath),
604
+ hint: getExtensionHint(ext)
605
+ });
606
+ } catch {}
607
+ }
608
+ if (files.length === 0) return;
609
+ await showFileViewerChooser(files);
610
+ }
611
+ /**
612
+ * Register the browse command.
613
+ */
614
+ function registerBrowseCommand(program) {
615
+ program.command("browse").description("Browse and view files in the forms directory").option("--filter <pattern>", "Only show files matching pattern").action(async (options, cmd) => {
324
616
  const ctx = getCommandContext(cmd);
325
617
  try {
326
- displayContent$2(formatMarkdown$3(loadDocs(), !options.raw && isInteractive$3() && !ctx.quiet));
618
+ const formsDir = getFormsDir(ctx.formsDir);
619
+ p.intro(pc.bgCyan(pc.black(" markform browse ")));
620
+ const entries = scanFormsDirectory$1(formsDir, options.filter);
621
+ if (entries.length === 0) {
622
+ if (options.filter) p.log.warn(`No files matching "${options.filter}" found in ${formatPath(formsDir)}`);
623
+ else p.log.warn(`No viewable files found in ${formatPath(formsDir)}`);
624
+ console.log("");
625
+ console.log(`Run ${pc.cyan("'markform examples'")} to get started.`);
626
+ p.outro("");
627
+ return;
628
+ }
629
+ console.log(pc.dim(`Found ${entries.length} file(s) in ${formatPath(formsDir)}`));
630
+ const menuOptions = entries.map((entry) => ({
631
+ value: entry.path,
632
+ label: formatFileLabel(entry),
633
+ hint: getExtensionHint(entry.extension)
634
+ }));
635
+ menuOptions.push({
636
+ value: "quit",
637
+ label: "Quit",
638
+ hint: ""
639
+ });
640
+ while (true) {
641
+ const selection = await p.select({
642
+ message: "Select a file to view:",
643
+ options: menuOptions
644
+ });
645
+ if (p.isCancel(selection)) break;
646
+ if (selection === "quit") break;
647
+ await viewFile(selection);
648
+ console.log("");
649
+ }
650
+ p.outro("");
327
651
  } catch (error) {
328
652
  logError(error instanceof Error ? error.message : String(error));
329
653
  process.exit(1);
@@ -332,19 +656,114 @@ function registerDocsCommand(program) {
332
656
  }
333
657
 
334
658
  //#endregion
335
- //#region src/cli/lib/exportHelpers.ts
659
+ //#region src/cli/commands/docs.ts
336
660
  /**
337
- * Export helpers for multi-format form output.
338
- *
339
- * Provides reusable functions for exporting forms to multiple formats:
340
- * - Markform format (.form.md) - canonical form with directives
341
- * - Raw markdown (.raw.md) - plain readable markdown
342
- * - YAML values (.yml) - extracted field values
661
+ * Get the path to the markform-reference.md file.
662
+ * Works both during development and when installed as a package.
343
663
  */
664
+ function getDocsPath() {
665
+ const thisDir = dirname(fileURLToPath(import.meta.url));
666
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-reference.md");
667
+ return join(dirname(dirname(dirname(thisDir))), "docs", "markform-reference.md");
668
+ }
344
669
  /**
345
- * Convert field responses to structured format for export (markform-218).
346
- *
347
- * Includes state for all fields:
670
+ * Load the docs content.
671
+ */
672
+ function loadDocs() {
673
+ const docsPath = getDocsPath();
674
+ try {
675
+ return readFileSync(docsPath, "utf-8");
676
+ } catch (error) {
677
+ const message = error instanceof Error ? error.message : String(error);
678
+ throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);
679
+ }
680
+ }
681
+ /**
682
+ * Apply basic terminal formatting to markdown content.
683
+ * Colorizes headers, code blocks, and other elements for better readability.
684
+ */
685
+ function formatMarkdown$2(content, useColors) {
686
+ if (!useColors) return content;
687
+ const lines = content.split("\n");
688
+ const formatted = [];
689
+ let inCodeBlock = false;
690
+ for (const line of lines) {
691
+ if (line.startsWith("```")) {
692
+ inCodeBlock = !inCodeBlock;
693
+ formatted.push(pc.dim(line));
694
+ continue;
695
+ }
696
+ if (inCodeBlock) {
697
+ formatted.push(pc.dim(line));
698
+ continue;
699
+ }
700
+ if (line.startsWith("# ")) {
701
+ formatted.push(pc.bold(pc.cyan(line)));
702
+ continue;
703
+ }
704
+ if (line.startsWith("## ")) {
705
+ formatted.push(pc.bold(pc.blue(line)));
706
+ continue;
707
+ }
708
+ if (line.startsWith("### ")) {
709
+ formatted.push(pc.bold(line));
710
+ continue;
711
+ }
712
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
713
+ return pc.yellow(code);
714
+ });
715
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
716
+ return pc.bold(text);
717
+ });
718
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
719
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
720
+ });
721
+ formatted.push(formattedLine);
722
+ }
723
+ return formatted.join("\n");
724
+ }
725
+ /**
726
+ * Check if stdout is an interactive terminal.
727
+ */
728
+ function isInteractive$2() {
729
+ return process.stdout.isTTY === true;
730
+ }
731
+ /**
732
+ * Display content.
733
+ */
734
+ function displayContent$2(content) {
735
+ console.log(content);
736
+ }
737
+ /**
738
+ * Register the docs command.
739
+ */
740
+ function registerDocsCommand(program) {
741
+ program.command("docs").description("Display concise Markform syntax reference (agent-friendly)").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
742
+ const ctx = getCommandContext(cmd);
743
+ try {
744
+ displayContent$2(formatMarkdown$2(loadDocs(), !options.raw && isInteractive$2() && !ctx.quiet));
745
+ } catch (error) {
746
+ logError(error instanceof Error ? error.message : String(error));
747
+ process.exit(1);
748
+ }
749
+ });
750
+ }
751
+
752
+ //#endregion
753
+ //#region src/cli/lib/exportHelpers.ts
754
+ /**
755
+ * Export helpers for multi-format form output.
756
+ *
757
+ * Provides reusable functions for exporting forms to multiple formats:
758
+ * - Markform format (.form.md) - canonical form with directives
759
+ * - Raw markdown (.raw.md) - plain readable markdown
760
+ * - YAML values (.yml) - extracted field values
761
+ * - JSON Schema (.schema.json) - form structure for validation/tooling
762
+ */
763
+ /**
764
+ * Convert field responses to structured format for export (markform-218).
765
+ *
766
+ * Includes state for all fields:
348
767
  * - { state: 'unanswered' } for unfilled fields
349
768
  * - { state: 'skipped' } for skipped fields
350
769
  * - { state: 'aborted' } for aborted fields
@@ -428,7 +847,7 @@ function toNotesArray(form) {
428
847
  * Derive export paths from a base form path.
429
848
  * Uses centralized extension constants from settings.ts.
430
849
  *
431
- * Standard exports: report, values (yaml), form.
850
+ * Standard exports: report, values (yaml), form, schema.
432
851
  * Raw markdown is available via CLI but not in standard exports.
433
852
  *
434
853
  * @param basePath - Path to the .form.md file
@@ -438,7 +857,8 @@ function deriveExportPaths(basePath) {
438
857
  return {
439
858
  reportPath: deriveReportPath(basePath),
440
859
  yamlPath: deriveExportPath(basePath, "yaml"),
441
- formPath: deriveExportPath(basePath, "form")
860
+ formPath: deriveExportPath(basePath, "form"),
861
+ schemaPath: deriveSchemaPath(basePath)
442
862
  };
443
863
  }
444
864
  /**
@@ -448,6 +868,7 @@ function deriveExportPaths(basePath) {
448
868
  * - Report format (.report.md) - filtered markdown (excludes instructions, report=false)
449
869
  * - YAML values (.yml) - structured format with state and notes
450
870
  * - Markform format (.form.md) - canonical form with directives
871
+ * - JSON Schema (.schema.json) - form structure for validation/tooling
451
872
  *
452
873
  * Note: Raw markdown (.raw.md) is available via CLI `markform export --raw`
453
874
  * but is not included in standard multi-format export.
@@ -470,6 +891,9 @@ async function exportMultiFormat(form, basePath) {
470
891
  await writeFile(paths.yamlPath, yamlContent);
471
892
  const formContent = serialize(form);
472
893
  await writeFile(paths.formPath, formContent);
894
+ const schemaResult = formToJsonSchema(form);
895
+ const schemaContent = JSON.stringify(schemaResult.schema, null, 2) + "\n";
896
+ await writeFile(paths.schemaPath, schemaContent);
473
897
  return paths;
474
898
  }
475
899
 
@@ -545,96 +969,6 @@ function registerDumpCommand(program) {
545
969
  });
546
970
  }
547
971
 
548
- //#endregion
549
- //#region src/cli/lib/patchFormat.ts
550
- /** Maximum characters for a patch value display before truncation */
551
- const PATCH_VALUE_MAX_LENGTH = 1e3;
552
- /**
553
- * Truncate a string to max length with ellipsis if needed.
554
- */
555
- function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
556
- if (value.length <= maxLength) return value;
557
- return value.slice(0, maxLength) + "…";
558
- }
559
- /**
560
- * Format a patch value for display with truncation.
561
- */
562
- function formatPatchValue(patch) {
563
- switch (patch.op) {
564
- case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
565
- case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
566
- case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
567
- case "set_single_select": return patch.selected ?? "(none)";
568
- case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
569
- case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
570
- case "clear_field": return "(cleared)";
571
- case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
572
- case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
573
- case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
574
- case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
575
- case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
576
- case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
577
- case "set_table": return patch.rows.length > 0 ? truncate(`[${patch.rows.length} rows]`) : "(empty)";
578
- case "add_note": return truncate(`note: ${patch.text}`);
579
- case "remove_note": return `(remove note ${patch.noteId})`;
580
- }
581
- }
582
- /**
583
- * Get a short display name for the patch operation type.
584
- */
585
- function formatPatchType(patch) {
586
- switch (patch.op) {
587
- case "set_string": return "string";
588
- case "set_number": return "number";
589
- case "set_string_list": return "string_list";
590
- case "set_single_select": return "select";
591
- case "set_multi_select": return "multi_select";
592
- case "set_checkboxes": return "checkboxes";
593
- case "clear_field": return "clear";
594
- case "skip_field": return "skip";
595
- case "abort_field": return "abort";
596
- case "set_url": return "url";
597
- case "set_url_list": return "url_list";
598
- case "set_date": return "date";
599
- case "set_year": return "year";
600
- case "set_table": return "table";
601
- case "add_note": return "note";
602
- case "remove_note": return "remove_note";
603
- }
604
- }
605
-
606
- //#endregion
607
- //#region src/cli/lib/formatting.ts
608
- /**
609
- * Get a short status word from an issue reason.
610
- */
611
- function issueReasonToStatus(reason) {
612
- switch (reason) {
613
- case "required_missing": return "missing";
614
- case "validation_error": return "invalid";
615
- case "checkbox_incomplete": return "incomplete";
616
- case "min_items_not_met": return "too-few";
617
- case "optional_empty": return "empty";
618
- default: return "issue";
619
- }
620
- }
621
- /**
622
- * Format a single issue as "fieldId (status)".
623
- */
624
- function formatIssueBrief(issue) {
625
- const status = issueReasonToStatus(issue.reason);
626
- return `${issue.ref} (${status})`;
627
- }
628
- /**
629
- * Format issues for turn logging - shows count and brief field list.
630
- * Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
631
- */
632
- function formatTurnIssues(issues, maxShow = 5) {
633
- const count = issues.length;
634
- if (count === 0) return "0 issues";
635
- return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
636
- }
637
-
638
972
  //#endregion
639
973
  //#region src/cli/examples/exampleRegistry.ts
640
974
  /**
@@ -649,18 +983,18 @@ function formatTurnIssues(issues, maxShow = 5) {
649
983
  * Title and description are loaded dynamically from frontmatter.
650
984
  */
651
985
  const EXAMPLE_DEFINITIONS = [
986
+ {
987
+ id: "movie-research-demo",
988
+ filename: "movie-research-demo.form.md",
989
+ path: "movie-research/movie-research-demo.form.md",
990
+ type: "research"
991
+ },
652
992
  {
653
993
  id: "simple",
654
994
  filename: "simple.form.md",
655
995
  path: "simple/simple.form.md",
656
996
  type: "fill"
657
997
  },
658
- {
659
- id: "movie-research-minimal",
660
- filename: "movie-research-minimal.form.md",
661
- path: "movie-research/movie-research-minimal.form.md",
662
- type: "research"
663
- },
664
998
  {
665
999
  id: "movie-research-basic",
666
1000
  filename: "movie-research-basic.form.md",
@@ -673,19 +1007,29 @@ const EXAMPLE_DEFINITIONS = [
673
1007
  path: "movie-research/movie-research-deep.form.md",
674
1008
  type: "research"
675
1009
  },
676
- {
677
- id: "earnings-analysis",
678
- filename: "earnings-analysis.form.md",
679
- path: "earnings-analysis/earnings-analysis.form.md",
680
- type: "research"
681
- },
682
1010
  {
683
1011
  id: "startup-deep-research",
684
1012
  filename: "startup-deep-research.form.md",
685
1013
  path: "startup-deep-research/startup-deep-research.form.md",
686
1014
  type: "research"
1015
+ },
1016
+ {
1017
+ id: "earnings-analysis",
1018
+ filename: "earnings-analysis.form.md",
1019
+ path: "earnings-analysis/earnings-analysis.form.md",
1020
+ type: "research"
687
1021
  }
688
1022
  ];
1023
+ /** Default example ID for menus (movie-research-demo, index 0) */
1024
+ const DEFAULT_EXAMPLE_ID = "movie-research-demo";
1025
+ /**
1026
+ * Get the canonical order index for an example by filename.
1027
+ * Returns -1 if not found (unknown files sort to the end).
1028
+ */
1029
+ function getExampleOrder(filename) {
1030
+ const index = EXAMPLE_DEFINITIONS.findIndex((e) => e.filename === filename);
1031
+ return index >= 0 ? index : EXAMPLE_DEFINITIONS.length;
1032
+ }
689
1033
  /**
690
1034
  * Get the path to the examples directory.
691
1035
  * Works both during development and when installed as a package.
@@ -772,46 +1116,200 @@ function getAllExamplesWithMetadata() {
772
1116
  }
773
1117
 
774
1118
  //#endregion
775
- //#region src/cli/lib/versioning.ts
1119
+ //#region src/cli/lib/formatting.ts
776
1120
  /**
777
- * Versioned filename utilities for form output.
778
- *
779
- * Generates versioned filenames to avoid overwriting existing files.
780
- * Pattern: name.form.md → name-filled1.form.md → name-filled2.form.md
1121
+ * Color and output formatting utilities for CLI.
781
1122
  */
782
1123
  /**
783
- * Version pattern that matches -filledN or _filledN before the extension.
784
- * Also matches legacy -vN pattern for backwards compatibility.
1124
+ * Get a short status word from an issue reason.
785
1125
  */
786
- const VERSION_PATTERN = /^(.+?)(?:[-_]?(?:filled|v)(\d+))?(\.form\.md)$/i;
1126
+ function issueReasonToStatus(reason) {
1127
+ switch (reason) {
1128
+ case "required_missing": return "missing";
1129
+ case "validation_error": return "invalid";
1130
+ case "checkbox_incomplete": return "incomplete";
1131
+ case "min_items_not_met": return "too-few";
1132
+ case "optional_empty": return "empty";
1133
+ default: return "issue";
1134
+ }
1135
+ }
787
1136
  /**
788
- * Extension pattern for fallback matching.
1137
+ * Format a single issue as "fieldId (status)".
789
1138
  */
790
- const EXTENSION_PATTERN = /^(.+)(\.form\.md)$/i;
1139
+ function formatIssueBrief(issue) {
1140
+ const status = issueReasonToStatus(issue.reason);
1141
+ return `${issue.ref} (${status})`;
1142
+ }
791
1143
  /**
792
- * Parse a versioned filename into its components.
793
- *
794
- * @param filePath - Path to parse
795
- * @returns Parsed components or null if not a valid form file
1144
+ * Format issues for turn logging - shows count and brief field list.
1145
+ * Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
796
1146
  */
797
- function parseVersionedPath(filePath) {
798
- const match = VERSION_PATTERN.exec(filePath);
799
- if (match) {
800
- const base = match[1];
801
- const versionStr = match[2];
802
- const ext = match[3];
803
- if (base !== void 0 && ext !== void 0) return {
804
- base,
805
- version: versionStr ? parseInt(versionStr, 10) : null,
806
- extension: ext
807
- };
808
- }
809
- const extMatch = EXTENSION_PATTERN.exec(filePath);
810
- if (extMatch) {
811
- const base = extMatch[1];
812
- const ext = extMatch[2];
813
- if (base !== void 0 && ext !== void 0) return {
814
- base,
1147
+ function formatTurnIssues(issues, maxShow = 5) {
1148
+ const count = issues.length;
1149
+ if (count === 0) return "0 issues";
1150
+ return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
1151
+ }
1152
+ /**
1153
+ * Format form info for menu label display.
1154
+ * Format: "filename - Title [runMode]"
1155
+ * Example: "movie-research-deep.form.md - Movie Research (Deep) [research]"
1156
+ */
1157
+ function formatFormLabel(info) {
1158
+ const titlePart = info.title ? ` - ${info.title}` : "";
1159
+ const runModePart = info.runMode ? ` [${info.runMode}]` : "";
1160
+ return `${info.filename}${titlePart}${runModePart}`;
1161
+ }
1162
+ /**
1163
+ * Format form info for menu hint display.
1164
+ * Returns description without parentheses (prompts library adds them).
1165
+ */
1166
+ function formatFormHint(info) {
1167
+ return info.description ?? "";
1168
+ }
1169
+ /**
1170
+ * Format form info for log line (e.g., after copying).
1171
+ * Format: "filename - Title" (dimmed title)
1172
+ * Example: "✓ movie-research-deep.form.md - Movie Research (Deep)"
1173
+ */
1174
+ function formatFormLogLine(info, prefix) {
1175
+ const titlePart = info.title ? ` - ${info.title}` : "";
1176
+ return `${prefix} ${info.filename}${pc.dim(titlePart)}`;
1177
+ }
1178
+
1179
+ //#endregion
1180
+ //#region src/cli/lib/runMode.ts
1181
+ /**
1182
+ * Get the set of unique roles present in a form's fields.
1183
+ */
1184
+ function getFieldRoles(form) {
1185
+ const allFields = getAllFields(form);
1186
+ return new Set(allFields.map((field) => field.role));
1187
+ }
1188
+ /**
1189
+ * Validate that run_mode is consistent with form structure.
1190
+ *
1191
+ * Rules:
1192
+ * - interactive: Form MUST have at least one role="user" field
1193
+ * - fill: Form MUST have at least one role="agent" field
1194
+ * - research: Form MUST have at least one role="agent" field
1195
+ */
1196
+ function validateRunMode(form, runMode) {
1197
+ const roles = getFieldRoles(form);
1198
+ switch (runMode) {
1199
+ case "interactive":
1200
+ if (!roles.has(USER_ROLE)) return {
1201
+ valid: false,
1202
+ error: `run_mode="interactive" but form has no user-role fields. Available roles: ${[...roles].join(", ") || "(none)"}`
1203
+ };
1204
+ break;
1205
+ case "fill":
1206
+ case "research":
1207
+ if (!roles.has(AGENT_ROLE)) return {
1208
+ valid: false,
1209
+ error: `run_mode="${runMode}" but form has no agent-role fields. Available roles: ${[...roles].join(", ") || "(none)"}`
1210
+ };
1211
+ break;
1212
+ }
1213
+ return { valid: true };
1214
+ }
1215
+ /**
1216
+ * Determine the run mode for a form.
1217
+ *
1218
+ * 1. If explicit run_mode in frontmatter, validate and use it
1219
+ * 2. Otherwise, infer from field roles:
1220
+ * - All user fields → interactive
1221
+ * - All agent fields → fill (or research if isResearchForm)
1222
+ * - Mixed roles → error (require explicit run_mode)
1223
+ */
1224
+ function determineRunMode(form) {
1225
+ const explicitMode = form.metadata?.runMode;
1226
+ if (explicitMode) {
1227
+ const validation = validateRunMode(form, explicitMode);
1228
+ if (!validation.valid) return {
1229
+ success: false,
1230
+ error: validation.error
1231
+ };
1232
+ return {
1233
+ success: true,
1234
+ runMode: explicitMode,
1235
+ source: "explicit"
1236
+ };
1237
+ }
1238
+ const roles = getFieldRoles(form);
1239
+ if (roles.size === 0) return {
1240
+ success: false,
1241
+ error: "Form has no fields"
1242
+ };
1243
+ if (roles.size === 1 && roles.has(USER_ROLE)) return {
1244
+ success: true,
1245
+ runMode: "interactive",
1246
+ source: "inferred"
1247
+ };
1248
+ if (roles.size === 1 && roles.has(AGENT_ROLE)) {
1249
+ if (isResearchForm(form)) return {
1250
+ success: true,
1251
+ runMode: "research",
1252
+ source: "inferred"
1253
+ };
1254
+ return {
1255
+ success: true,
1256
+ runMode: "fill",
1257
+ source: "inferred"
1258
+ };
1259
+ }
1260
+ return {
1261
+ success: false,
1262
+ error: `Cannot determine run mode. Form has roles: ${[...roles].join(", ")}. Add 'run_mode' to frontmatter: interactive, fill, or research.`
1263
+ };
1264
+ }
1265
+ /**
1266
+ * Get a human-readable description of the run mode source.
1267
+ */
1268
+ function formatRunModeSource(source) {
1269
+ return source === "explicit" ? "from frontmatter" : "inferred from field roles";
1270
+ }
1271
+
1272
+ //#endregion
1273
+ //#region src/cli/lib/versioning.ts
1274
+ /**
1275
+ * Versioned filename utilities for form output.
1276
+ *
1277
+ * Generates versioned filenames to avoid overwriting existing files.
1278
+ * Pattern: name.form.md → name-filled1.form.md → name-filled2.form.md
1279
+ */
1280
+ /**
1281
+ * Version pattern that matches -filledN or _filledN before the extension.
1282
+ * Also matches legacy -vN pattern for backwards compatibility.
1283
+ */
1284
+ const VERSION_PATTERN = /^(.+?)(?:[-_]?(?:filled|v)(\d+))?(\.form\.md)$/i;
1285
+ /**
1286
+ * Extension pattern for fallback matching.
1287
+ */
1288
+ const EXTENSION_PATTERN = /^(.+)(\.form\.md)$/i;
1289
+ /**
1290
+ * Parse a versioned filename into its components.
1291
+ *
1292
+ * @param filePath - Path to parse
1293
+ * @returns Parsed components or null if not a valid form file
1294
+ */
1295
+ function parseVersionedPath(filePath) {
1296
+ const match = VERSION_PATTERN.exec(filePath);
1297
+ if (match) {
1298
+ const base = match[1];
1299
+ const versionStr = match[2];
1300
+ const ext = match[3];
1301
+ if (base !== void 0 && ext !== void 0) return {
1302
+ base,
1303
+ version: versionStr ? parseInt(versionStr, 10) : null,
1304
+ extension: ext
1305
+ };
1306
+ }
1307
+ const extMatch = EXTENSION_PATTERN.exec(filePath);
1308
+ if (extMatch) {
1309
+ const base = extMatch[1];
1310
+ const ext = extMatch[2];
1311
+ if (base !== void 0 && ext !== void 0) return {
1312
+ base,
815
1313
  version: null,
816
1314
  extension: ext
817
1315
  };
@@ -1411,198 +1909,489 @@ function showInteractiveOutro(patchCount, cancelled) {
1411
1909
  }
1412
1910
 
1413
1911
  //#endregion
1414
- //#region src/cli/lib/fileViewer.ts
1912
+ //#region src/cli/lib/patchFormat.ts
1913
+ /** Maximum characters for a patch value display before truncation */
1914
+ const PATCH_VALUE_MAX_LENGTH = 1e3;
1415
1915
  /**
1416
- * File viewer utility for displaying files with colorization and pagination.
1417
- *
1418
- * Provides a modern console experience:
1419
- * - Syntax highlighting for markdown and YAML
1420
- * - Pagination using system pager (less) when available
1421
- * - Fallback to console output when not interactive
1916
+ * Truncate a string to max length with ellipsis if needed.
1422
1917
  */
1918
+ function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
1919
+ if (value.length <= maxLength) return value;
1920
+ return value.slice(0, maxLength) + "…";
1921
+ }
1423
1922
  /**
1424
- * Check if stdout is an interactive terminal.
1923
+ * Format a patch value for display with truncation.
1425
1924
  */
1426
- function isInteractive$2() {
1427
- return process.stdout.isTTY === true;
1925
+ function formatPatchValue(patch) {
1926
+ switch (patch.op) {
1927
+ case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
1928
+ case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
1929
+ case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
1930
+ case "set_single_select": return patch.selected ?? "(none)";
1931
+ case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
1932
+ case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
1933
+ case "clear_field": return "(cleared)";
1934
+ case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
1935
+ case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
1936
+ case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
1937
+ case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
1938
+ case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
1939
+ case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
1940
+ case "set_table": return patch.rows.length > 0 ? truncate(`[${patch.rows.length} rows]`) : "(empty)";
1941
+ case "add_note": return truncate(`note: ${patch.text}`);
1942
+ case "remove_note": return `(remove note ${patch.noteId})`;
1943
+ }
1428
1944
  }
1429
1945
  /**
1430
- * Apply terminal formatting to markdown content.
1431
- * Colorizes headers, code blocks, and other elements.
1946
+ * Get a short display name for the patch operation type.
1432
1947
  */
1433
- function formatMarkdown$2(content) {
1434
- const lines = content.split("\n");
1435
- const formatted = [];
1436
- let inCodeBlock = false;
1437
- for (const line of lines) {
1438
- if (line.startsWith("```")) {
1439
- inCodeBlock = !inCodeBlock;
1440
- formatted.push(pc.dim(line));
1441
- continue;
1442
- }
1443
- if (inCodeBlock) {
1444
- formatted.push(pc.cyan(line));
1445
- continue;
1446
- }
1447
- if (line.startsWith("# ")) {
1448
- formatted.push(pc.bold(pc.magenta(line)));
1449
- continue;
1450
- }
1451
- if (line.startsWith("## ")) {
1452
- formatted.push(pc.bold(pc.blue(line)));
1453
- continue;
1454
- }
1455
- if (line.startsWith("### ")) {
1456
- formatted.push(pc.bold(pc.cyan(line)));
1457
- continue;
1458
- }
1459
- if (line.startsWith("#### ")) {
1460
- formatted.push(pc.bold(line));
1461
- continue;
1462
- }
1463
- let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
1464
- return pc.yellow(code);
1465
- });
1466
- formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
1467
- return pc.bold(text);
1468
- });
1469
- formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
1470
- return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
1471
- });
1472
- formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
1473
- return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
1474
- });
1475
- formatted.push(formattedLine);
1948
+ function formatPatchType(patch) {
1949
+ switch (patch.op) {
1950
+ case "set_string": return "string";
1951
+ case "set_number": return "number";
1952
+ case "set_string_list": return "string_list";
1953
+ case "set_single_select": return "select";
1954
+ case "set_multi_select": return "multi_select";
1955
+ case "set_checkboxes": return "checkboxes";
1956
+ case "clear_field": return "clear";
1957
+ case "skip_field": return "skip";
1958
+ case "abort_field": return "abort";
1959
+ case "set_url": return "url";
1960
+ case "set_url_list": return "url_list";
1961
+ case "set_date": return "date";
1962
+ case "set_year": return "year";
1963
+ case "set_table": return "table";
1964
+ case "add_note": return "note";
1965
+ case "remove_note": return "remove_note";
1476
1966
  }
1477
- return formatted.join("\n");
1478
1967
  }
1968
+
1969
+ //#endregion
1970
+ //#region src/cli/lib/fillLogging.ts
1479
1971
  /**
1480
- * Apply terminal formatting to YAML content.
1972
+ * Fill Logging Callbacks - Create FillCallbacks for unified CLI logging.
1973
+ *
1974
+ * Provides consistent turn-by-turn logging across all CLI commands that
1975
+ * run form-filling (fill, run, examples). API consumers can also use
1976
+ * these callbacks or implement their own.
1977
+ *
1978
+ * Default output (always shown unless --quiet):
1979
+ * - Turn numbers with issues list (field IDs + issue types)
1980
+ * - Patches per turn (field ID + value)
1981
+ * - Completion status
1982
+ *
1983
+ * Verbose output (--verbose flag):
1984
+ * - Token counts per turn
1985
+ * - Tool call start/end with timing
1986
+ * - Detailed stats and LLM metadata
1481
1987
  */
1482
- function formatYaml(content) {
1483
- const lines = content.split("\n");
1484
- const formatted = [];
1485
- for (const line of lines) {
1486
- if (line.trim().startsWith("#")) {
1487
- formatted.push(pc.dim(line));
1488
- continue;
1489
- }
1490
- const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
1491
- if (match) {
1492
- const [, indent, key, colon, value] = match;
1493
- formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
1494
- continue;
1495
- }
1496
- if (line.trim().startsWith("-")) {
1497
- formatted.push(pc.green(line));
1498
- continue;
1988
+ /**
1989
+ * Create FillCallbacks that produce standard CLI logging output.
1990
+ *
1991
+ * Default output (always shown unless --quiet):
1992
+ * - Turn numbers with issues list (field IDs + issue types)
1993
+ * - Patches per turn (field ID + value)
1994
+ * - Completion status
1995
+ *
1996
+ * Verbose output (--verbose flag):
1997
+ * - Token counts per turn
1998
+ * - Tool call start/end with timing
1999
+ * - Detailed stats and LLM metadata
2000
+ *
2001
+ * This is used by fill, run, and examples commands for consistent output.
2002
+ *
2003
+ * @param ctx - Command context for verbose/quiet flags
2004
+ * @param options - Optional spinner for tool progress
2005
+ * @returns FillCallbacks with all logging implemented
2006
+ *
2007
+ * @example
2008
+ * ```typescript
2009
+ * const callbacks = createFillLoggingCallbacks(ctx, { spinner });
2010
+ * const result = await fillForm({
2011
+ * form: formMarkdown,
2012
+ * model: 'anthropic/claude-sonnet-4-5',
2013
+ * enableWebSearch: true,
2014
+ * callbacks,
2015
+ * });
2016
+ * ```
2017
+ */
2018
+ function createFillLoggingCallbacks(ctx, options = {}) {
2019
+ return {
2020
+ onIssuesIdentified: ({ turnNumber, issues }) => {
2021
+ logInfo(ctx, `${pc.bold(`Turn ${turnNumber}:`)} ${formatTurnIssues(issues)}`);
2022
+ },
2023
+ onPatchesGenerated: ({ patches, stats }) => {
2024
+ logInfo(ctx, ` -> ${pc.yellow(String(patches.length))} patch(es):`);
2025
+ for (const patch of patches) {
2026
+ const typeName = formatPatchType(patch);
2027
+ const value = formatPatchValue(patch);
2028
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
2029
+ if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2030
+ else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2031
+ }
2032
+ if (stats && ctx.verbose) {
2033
+ logVerbose(ctx, ` Tokens: in=${stats.inputTokens ?? 0} out=${stats.outputTokens ?? 0}`);
2034
+ if (stats.toolCalls && stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
2035
+ }
2036
+ },
2037
+ onTurnComplete: ({ isComplete }) => {
2038
+ if (isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
2039
+ },
2040
+ onToolStart: ({ name }) => {
2041
+ if (name.includes("search")) options.spinner?.message(`Web search...`);
2042
+ logVerbose(ctx, ` Tool started: ${name}`);
2043
+ },
2044
+ onToolEnd: ({ name, durationMs, error }) => {
2045
+ if (error) logVerbose(ctx, ` Tool ${name} failed: ${error} (${durationMs}ms)`);
2046
+ else logVerbose(ctx, ` Tool ${name} completed (${durationMs}ms)`);
2047
+ },
2048
+ onLlmCallStart: ({ model }) => {
2049
+ logVerbose(ctx, ` LLM call: ${model}`);
2050
+ },
2051
+ onLlmCallEnd: ({ model, inputTokens, outputTokens }) => {
2052
+ logVerbose(ctx, ` LLM response: ${model} (in=${inputTokens} out=${outputTokens})`);
1499
2053
  }
1500
- formatted.push(line);
1501
- }
1502
- return formatted.join("\n");
2054
+ };
1503
2055
  }
2056
+
2057
+ //#endregion
2058
+ //#region src/cli/commands/run.ts
1504
2059
  /**
1505
- * Format file content based on extension.
2060
+ * Run command - Interactive launcher for running forms.
2061
+ *
2062
+ * Provides a menu-based interface for selecting and running forms
2063
+ * from the forms directory. Automatically detects run mode based
2064
+ * on frontmatter or field roles.
2065
+ *
2066
+ * Usage:
2067
+ * markform run # Browse forms, select, run
2068
+ * markform run movie.form.md # Run specific form directly
2069
+ * markform run --limit=50 # Override menu limit
1506
2070
  */
1507
- function formatContent(content, filename) {
1508
- if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
1509
- if (filename.endsWith(".md")) return formatMarkdown$2(content);
1510
- return content;
2071
+ /**
2072
+ * Scan forms directory for .form.md files.
2073
+ */
2074
+ function scanFormsDirectory(formsDir) {
2075
+ const entries = [];
2076
+ try {
2077
+ const files = readdirSync(formsDir);
2078
+ for (const file of files) {
2079
+ if (!file.endsWith(".form.md")) continue;
2080
+ const fullPath = join(formsDir, file);
2081
+ try {
2082
+ const stat = statSync(fullPath);
2083
+ if (stat.isFile()) entries.push({
2084
+ path: fullPath,
2085
+ filename: file,
2086
+ mtime: stat.mtime
2087
+ });
2088
+ } catch {}
2089
+ }
2090
+ } catch {}
2091
+ entries.sort((a, b) => {
2092
+ const orderDiff = getExampleOrder(a.filename) - getExampleOrder(b.filename);
2093
+ if (orderDiff !== 0) return orderDiff;
2094
+ return a.filename.localeCompare(b.filename);
2095
+ });
2096
+ return entries;
1511
2097
  }
1512
2098
  /**
1513
- * Display content using system pager (less) if available.
1514
- * Falls back to console.log if not interactive or pager unavailable.
1515
- *
1516
- * @returns Promise that resolves when viewing is complete
2099
+ * Load form metadata for menu display.
1517
2100
  */
1518
- async function displayWithPager(content, title) {
1519
- if (!isInteractive$2()) {
1520
- console.log(content);
1521
- return;
2101
+ async function enrichFormEntry(entry) {
2102
+ try {
2103
+ const form = parseForm(await readFile$1(entry.path));
2104
+ const runModeResult = determineRunMode(form);
2105
+ return {
2106
+ ...entry,
2107
+ title: form.schema.title,
2108
+ description: form.schema.description,
2109
+ runMode: runModeResult.success ? runModeResult.runMode : void 0
2110
+ };
2111
+ } catch {
2112
+ return entry;
1522
2113
  }
1523
- const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
1524
- return new Promise((resolve$1) => {
1525
- const pager = spawn("less", [
1526
- "-R",
1527
- "-S",
1528
- "-X",
1529
- "-F",
1530
- "-K"
1531
- ], { stdio: [
1532
- "pipe",
1533
- "inherit",
1534
- "inherit"
1535
- ] });
1536
- pager.on("error", () => {
1537
- console.log(header);
1538
- console.log("");
1539
- console.log(content);
1540
- console.log("");
1541
- resolve$1();
1542
- });
1543
- pager.on("close", () => {
1544
- resolve$1();
2114
+ }
2115
+ /**
2116
+ * Build model options for the select prompt.
2117
+ */
2118
+ function buildModelOptions(webSearchOnly) {
2119
+ const options = [];
2120
+ for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
2121
+ if (webSearchOnly && !hasWebSearchSupport(provider)) continue;
2122
+ const info = getProviderInfo(provider);
2123
+ const keyStatus = !!process.env[info.envVar] ? pc.green("") : "○";
2124
+ for (const model of models) options.push({
2125
+ value: `${provider}/${model}`,
2126
+ label: `${provider}/${model}`,
2127
+ hint: `${keyStatus} ${info.envVar}`
1545
2128
  });
1546
- pager.stdin.write(header + "\n\n");
1547
- pager.stdin.write(content);
1548
- pager.stdin.end();
2129
+ }
2130
+ options.push({
2131
+ value: "custom",
2132
+ label: "Enter custom model ID...",
2133
+ hint: "provider/model-id format"
1549
2134
  });
2135
+ return options;
1550
2136
  }
1551
2137
  /**
1552
- * Load and display a file with formatting and pagination.
2138
+ * Prompt user to select a model.
1553
2139
  */
1554
- async function viewFile(filePath) {
1555
- const content = readFileSync(filePath, "utf-8");
1556
- const filename = basename(filePath);
1557
- await displayWithPager(formatContent(content, filename), filename);
2140
+ async function promptForModel(webSearchRequired) {
2141
+ const modelOptions = buildModelOptions(webSearchRequired);
2142
+ if (webSearchRequired && modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
2143
+ const message = webSearchRequired ? "Select LLM model (web search required):" : "Select LLM model:";
2144
+ const selection = await p.select({
2145
+ message,
2146
+ options: modelOptions
2147
+ });
2148
+ if (p.isCancel(selection)) return null;
2149
+ if (selection === "custom") {
2150
+ const customModel = await p.text({
2151
+ message: "Model ID (provider/model-id):",
2152
+ placeholder: "anthropic/claude-sonnet-4-20250514",
2153
+ validate: (value) => {
2154
+ if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
2155
+ }
2156
+ });
2157
+ if (p.isCancel(customModel)) return null;
2158
+ return customModel;
2159
+ }
2160
+ return selection;
1558
2161
  }
1559
2162
  /**
1560
- * Show an interactive file viewer chooser.
1561
- *
1562
- * Presents a list of files to view:
1563
- * - "Show report:" for the report output (.report.md) at the top
1564
- * - "Show source:" for other files (.form.md, .raw.md, .yml)
1565
- * - "Quit" at the bottom
1566
- *
1567
- * Loops until the user selects Quit.
1568
- *
1569
- * @param files Array of file options to display
2163
+ * Collect user input interactively (without exporting).
2164
+ * Returns true if successful, false if cancelled.
1570
2165
  */
1571
- async function showFileViewerChooser(files) {
1572
- if (!isInteractive$2()) return;
2166
+ async function collectUserInput(form) {
2167
+ const targetRoles = [USER_ROLE];
2168
+ const inspectResult = inspect(form, { targetRoles });
2169
+ const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
2170
+ const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
2171
+ if (uniqueFieldIds.size === 0) return true;
2172
+ showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
2173
+ const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
2174
+ if (cancelled) {
2175
+ showInteractiveOutro(0, true);
2176
+ return false;
2177
+ }
2178
+ if (patches.length > 0) applyPatches(form, patches);
2179
+ showInteractiveOutro(patches.length, false);
2180
+ return true;
2181
+ }
2182
+ /**
2183
+ * Run interactive fill workflow.
2184
+ * @returns ExportResult with paths to output files, or undefined if cancelled/no fields
2185
+ */
2186
+ async function runInteractiveWorkflow(form, filePath, formsDir) {
2187
+ const startTime = Date.now();
2188
+ const targetRoles = [USER_ROLE];
2189
+ const inspectResult = inspect(form, { targetRoles });
2190
+ const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
2191
+ const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
2192
+ if (uniqueFieldIds.size === 0) {
2193
+ p.log.info("No user-role fields to fill.");
2194
+ return;
2195
+ }
2196
+ showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
2197
+ const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
2198
+ if (cancelled) {
2199
+ showInteractiveOutro(0, true);
2200
+ return;
2201
+ }
2202
+ if (patches.length > 0) applyPatches(form, patches);
2203
+ await ensureFormsDir(formsDir);
2204
+ const exportResult = await exportMultiFormat(form, generateVersionedPathInFormsDir(filePath, formsDir));
2205
+ showInteractiveOutro(patches.length, false);
1573
2206
  console.log("");
1574
- const reportFile = files.find((f) => f.path.endsWith(".report.md"));
1575
- const sourceFiles = files.filter((f) => !f.path.endsWith(".report.md"));
1576
- while (true) {
1577
- const options = [];
1578
- if (reportFile) options.push({
1579
- value: reportFile.path,
1580
- label: `Show report: ${pc.green(basename(reportFile.path))}`,
1581
- hint: reportFile.hint ?? ""
1582
- });
1583
- for (const file of sourceFiles) options.push({
1584
- value: file.path,
1585
- label: `Show source: ${pc.green(basename(file.path))}`,
1586
- hint: file.hint ?? ""
1587
- });
1588
- options.push({
1589
- value: "quit",
1590
- label: "Quit",
1591
- hint: ""
1592
- });
1593
- const selection = await p.select({
1594
- message: "View files:",
1595
- options
1596
- });
1597
- if (p.isCancel(selection) || selection === "quit") break;
1598
- await viewFile(selection);
1599
- console.log("");
2207
+ p.log.success("Outputs:");
2208
+ console.log(` ${formatPath(exportResult.reportPath)} ${pc.dim("(output report)")}`);
2209
+ console.log(` ${formatPath(exportResult.yamlPath)} ${pc.dim("(output values)")}`);
2210
+ console.log(` ${formatPath(exportResult.formPath)} ${pc.dim("(filled markform source)")}`);
2211
+ console.log(` ${formatPath(exportResult.schemaPath)} ${pc.dim("(JSON Schema)")}`);
2212
+ logTiming({
2213
+ verbose: false,
2214
+ format: "console",
2215
+ dryRun: false,
2216
+ quiet: false,
2217
+ overwrite: false
2218
+ }, "Fill time", Date.now() - startTime);
2219
+ return exportResult;
2220
+ }
2221
+ /**
2222
+ * Run agent fill workflow using fillForm with logging callbacks.
2223
+ * @returns ExportResult with paths to output files
2224
+ */
2225
+ async function runAgentFillWorkflow(form, modelId, formsDir, filePath, isResearch, overwrite, ctx) {
2226
+ const startTime = Date.now();
2227
+ const maxTurns = DEFAULT_MAX_TURNS;
2228
+ const maxPatchesPerTurn = isResearch ? DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN : DEFAULT_MAX_PATCHES_PER_TURN;
2229
+ const maxIssuesPerTurn = isResearch ? DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN : DEFAULT_MAX_ISSUES_PER_TURN;
2230
+ logVerbose(ctx, `Config: max_turns=${maxTurns}, max_issues_per_turn=${maxIssuesPerTurn}, max_patches_per_turn=${maxPatchesPerTurn}`);
2231
+ const callbacks = createFillLoggingCallbacks(ctx);
2232
+ const workflowLabel = isResearch ? "Research" : "Agent fill";
2233
+ p.log.step(pc.bold(`${workflowLabel} in progress...`));
2234
+ const result = await fillForm({
2235
+ form,
2236
+ model: modelId,
2237
+ maxTurns,
2238
+ maxPatchesPerTurn,
2239
+ maxIssuesPerTurn,
2240
+ targetRoles: [AGENT_ROLE],
2241
+ fillMode: overwrite ? "overwrite" : "continue",
2242
+ enableWebSearch: isResearch,
2243
+ callbacks
2244
+ });
2245
+ if (result.status.ok) p.log.success(pc.green(`Form completed in ${result.turns} turn(s)`));
2246
+ else if (result.status.reason === "max_turns") p.log.warn(pc.yellow(`Max turns reached (${maxTurns})`));
2247
+ else throw new Error(result.status.message ?? `Fill failed: ${result.status.reason}`);
2248
+ await ensureFormsDir(formsDir);
2249
+ const outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
2250
+ const exportResult = await exportMultiFormat(result.form, outputPath);
2251
+ console.log("");
2252
+ p.log.success(`${workflowLabel} complete. Outputs:`);
2253
+ console.log(` ${formatPath(exportResult.reportPath)} ${pc.dim("(output report)")}`);
2254
+ console.log(` ${formatPath(exportResult.yamlPath)} ${pc.dim("(output values)")}`);
2255
+ console.log(` ${formatPath(exportResult.formPath)} ${pc.dim("(filled markform source)")}`);
2256
+ console.log(` ${formatPath(exportResult.schemaPath)} ${pc.dim("(JSON Schema)")}`);
2257
+ logTiming(ctx, isResearch ? "Research time" : "Fill time", Date.now() - startTime);
2258
+ return exportResult;
2259
+ }
2260
+ /**
2261
+ * Run a form directly (callable from other commands).
2262
+ * This executes the same workflow as `markform run <file>`.
2263
+ *
2264
+ * @param selectedPath - Path to the form file
2265
+ * @param formsDir - Directory for output files
2266
+ * @param overwrite - Whether to overwrite existing field values
2267
+ * @param ctx - Optional command context for logging (defaults to non-verbose/quiet)
2268
+ * @returns ExportResult with paths to output files, or undefined if cancelled/no output
2269
+ */
2270
+ async function runForm(selectedPath, formsDir, overwrite, ctx) {
2271
+ const effectiveCtx = ctx ?? {
2272
+ verbose: false,
2273
+ quiet: false,
2274
+ dryRun: false,
2275
+ format: "console",
2276
+ overwrite
2277
+ };
2278
+ const form = parseForm(await readFile$1(selectedPath));
2279
+ const runModeResult = determineRunMode(form);
2280
+ if (!runModeResult.success) throw new Error(runModeResult.error);
2281
+ const { runMode } = runModeResult;
2282
+ switch (runMode) {
2283
+ case "interactive": return runInteractiveWorkflow(form, selectedPath, formsDir);
2284
+ case "fill":
2285
+ case "research": {
2286
+ const isResearch = runMode === "research";
2287
+ if (!await collectUserInput(form)) {
2288
+ p.cancel("Cancelled.");
2289
+ return;
2290
+ }
2291
+ const modelId = await promptForModel(isResearch);
2292
+ if (!modelId) {
2293
+ p.cancel("Cancelled.");
2294
+ return;
2295
+ }
2296
+ return runAgentFillWorkflow(form, modelId, formsDir, selectedPath, isResearch, overwrite, effectiveCtx);
2297
+ }
1600
2298
  }
1601
2299
  }
2300
+ /**
2301
+ * Register the run command.
2302
+ */
2303
+ function registerRunCommand(program) {
2304
+ program.command("run [file]").description("Browse and run forms from the forms directory").option("--limit <n>", `Maximum forms to show in menu (default: ${MAX_FORMS_IN_MENU})`, String(MAX_FORMS_IN_MENU)).action(async (file, options, cmd) => {
2305
+ const ctx = getCommandContext(cmd);
2306
+ try {
2307
+ const formsDir = getFormsDir(ctx.formsDir);
2308
+ const limit = options.limit ? parseInt(options.limit, 10) : MAX_FORMS_IN_MENU;
2309
+ let selectedPath;
2310
+ if (file) {
2311
+ selectedPath = file.startsWith("/") ? file : join(formsDir, file);
2312
+ if (!selectedPath.endsWith(".form.md") && !selectedPath.endsWith(".md")) selectedPath = `${selectedPath}.form.md`;
2313
+ } else {
2314
+ p.intro(pc.bgCyan(pc.black(" markform run ")));
2315
+ const entries = scanFormsDirectory(formsDir);
2316
+ if (entries.length === 0) {
2317
+ p.log.warn(`No forms found in ${formatPath(formsDir)}`);
2318
+ console.log("");
2319
+ console.log(`Run ${pc.cyan("'markform examples'")} to get started.`);
2320
+ p.outro("");
2321
+ return;
2322
+ }
2323
+ const entriesToShow = entries.slice(0, limit);
2324
+ const enrichedEntries = await Promise.all(entriesToShow.map(enrichFormEntry));
2325
+ const menuOptions = enrichedEntries.map((entry) => ({
2326
+ value: entry.path,
2327
+ label: formatFormLabel(entry),
2328
+ hint: formatFormHint(entry)
2329
+ }));
2330
+ const defaultExample = getExampleById(DEFAULT_EXAMPLE_ID);
2331
+ const initialValue = enrichedEntries.find((e) => e.filename === defaultExample?.filename)?.path;
2332
+ if (entries.length > limit) console.log(pc.dim(`Showing ${limit} of ${entries.length} forms`));
2333
+ const selection = await p.select({
2334
+ message: "Select a form to run:",
2335
+ options: menuOptions,
2336
+ initialValue
2337
+ });
2338
+ if (p.isCancel(selection)) {
2339
+ p.cancel("Cancelled.");
2340
+ process.exit(0);
2341
+ }
2342
+ selectedPath = selection;
2343
+ }
2344
+ logVerbose(ctx, `Reading form: ${selectedPath}`);
2345
+ const form = parseForm(await readFile$1(selectedPath));
2346
+ const runModeResult = determineRunMode(form);
2347
+ if (!runModeResult.success) {
2348
+ logError(runModeResult.error);
2349
+ process.exit(1);
2350
+ }
2351
+ const { runMode, source } = runModeResult;
2352
+ logInfo(ctx, `Run mode: ${runMode} (${formatRunModeSource(source)})`);
2353
+ switch (runMode) {
2354
+ case "interactive":
2355
+ await runInteractiveWorkflow(form, selectedPath, formsDir);
2356
+ break;
2357
+ case "fill":
2358
+ case "research": {
2359
+ const isResearch = runMode === "research";
2360
+ if (!await collectUserInput(form)) {
2361
+ p.cancel("Cancelled.");
2362
+ process.exit(0);
2363
+ }
2364
+ const modelId = await promptForModel(isResearch);
2365
+ if (!modelId) {
2366
+ p.cancel("Cancelled.");
2367
+ process.exit(0);
2368
+ }
2369
+ await runAgentFillWorkflow(form, modelId, formsDir, selectedPath, isResearch, ctx.overwrite, ctx);
2370
+ break;
2371
+ }
2372
+ }
2373
+ if (!file) p.outro("Happy form filling!");
2374
+ } catch (error) {
2375
+ logError(error instanceof Error ? error.message : String(error));
2376
+ process.exit(1);
2377
+ }
2378
+ });
2379
+ }
1602
2380
 
1603
2381
  //#endregion
1604
2382
  //#region src/cli/commands/examples.ts
1605
2383
  /**
2384
+ * Examples command - Copy bundled example forms to the forms directory.
2385
+ *
2386
+ * This command provides a simple way to get started with markform by
2387
+ * copying bundled example forms to your local forms directory.
2388
+ *
2389
+ * Usage:
2390
+ * markform examples # Copy all bundled examples to ./forms/
2391
+ * markform examples --list # List bundled examples (no copy)
2392
+ * markform examples --name=foo # Copy specific example only
2393
+ */
2394
+ /**
1606
2395
  * Print non-interactive list of examples.
1607
2396
  */
1608
2397
  function printExamplesList() {
@@ -1618,409 +2407,149 @@ function printExamplesList() {
1618
2407
  }
1619
2408
  }
1620
2409
  /**
1621
- * Display API availability status at startup.
2410
+ * Copy an example form to the forms directory.
2411
+ *
2412
+ * @returns true if copied, false if skipped
1622
2413
  */
1623
- function showApiStatus() {
1624
- console.log("API Status:");
1625
- for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
1626
- const info = getProviderInfo(provider);
1627
- const hasKey = !!process.env[info.envVar];
1628
- const status = hasKey ? pc.green("✓") : "○";
1629
- const envVar = hasKey ? info.envVar : pc.yellow(info.envVar);
1630
- console.log(` ${status} ${provider} (${envVar})`);
2414
+ async function copyExample(exampleId, formsDir, overwrite, _quiet) {
2415
+ const example = getExampleById(exampleId);
2416
+ if (!example) throw new Error(`Unknown example: ${exampleId}`);
2417
+ const outputPath = join(formsDir, example.filename);
2418
+ if (existsSync(outputPath)) {
2419
+ if (!overwrite) return {
2420
+ copied: false,
2421
+ skipped: true,
2422
+ path: outputPath
2423
+ };
1631
2424
  }
1632
- console.log("");
2425
+ await writeFile(outputPath, loadExampleContent(exampleId));
2426
+ return {
2427
+ copied: true,
2428
+ skipped: false,
2429
+ path: outputPath
2430
+ };
1633
2431
  }
1634
2432
  /**
1635
- * Build model options for the select prompt.
2433
+ * Scan forms directory and enrich entries with metadata.
1636
2434
  */
1637
- function buildModelOptions() {
1638
- const options = [];
1639
- for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1640
- const info = getProviderInfo(provider);
1641
- const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1642
- for (const model of models) options.push({
1643
- value: `${provider}/${model}`,
1644
- label: `${provider}/${model}`,
1645
- hint: `${keyStatus} ${info.envVar}`
1646
- });
1647
- }
1648
- options.push({
1649
- value: "custom",
1650
- label: "Enter custom model ID...",
1651
- hint: "provider/model-id format"
1652
- });
1653
- return options;
2435
+ async function getFormEntries(formsDir) {
2436
+ const entries = [];
2437
+ try {
2438
+ const files = readdirSync(formsDir);
2439
+ for (const file of files) {
2440
+ if (!file.endsWith(".form.md")) continue;
2441
+ const fullPath = join(formsDir, file);
2442
+ try {
2443
+ if (statSync(fullPath).isFile()) {
2444
+ const form = parseForm(await readFile$1(fullPath));
2445
+ const runModeResult = determineRunMode(form);
2446
+ entries.push({
2447
+ path: fullPath,
2448
+ filename: file,
2449
+ title: form.schema.title,
2450
+ description: form.schema.description,
2451
+ runMode: runModeResult.success ? runModeResult.runMode : void 0
2452
+ });
2453
+ }
2454
+ } catch {}
2455
+ }
2456
+ } catch {}
2457
+ return entries;
1654
2458
  }
1655
2459
  /**
1656
- * Prompt user to select a model for agent fill.
2460
+ * Copy all examples to the forms directory.
2461
+ * Returns { copied, skipped } counts for the caller to handle prompts.
1657
2462
  */
1658
- async function promptForModel() {
1659
- const modelOptions = buildModelOptions();
1660
- const selection = await p.select({
1661
- message: "Select LLM model:",
1662
- options: modelOptions
1663
- });
1664
- if (p.isCancel(selection)) return null;
1665
- if (selection === "custom") {
1666
- const customModel = await p.text({
1667
- message: "Model ID (provider/model-id):",
1668
- placeholder: "anthropic/claude-sonnet-4-20250514",
1669
- validate: (value) => {
1670
- if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
1671
- }
1672
- });
1673
- if (p.isCancel(customModel)) return null;
1674
- return customModel;
2463
+ async function copyAllExamples(formsDir, overwrite, quiet) {
2464
+ const examples = getAllExamplesWithMetadata();
2465
+ const total = examples.length;
2466
+ if (!quiet) {
2467
+ console.log(`Copying ${total} example forms to ${formatPath(formsDir)}...`);
2468
+ console.log("");
1675
2469
  }
1676
- return selection;
1677
- }
1678
- /**
1679
- * Build model options filtered to providers with web search support.
1680
- */
1681
- function buildWebSearchModelOptions() {
1682
- const options = [];
1683
- for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1684
- if (!hasWebSearchSupport(provider)) continue;
1685
- const info = getProviderInfo(provider);
1686
- const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1687
- for (const model of models) options.push({
1688
- value: `${provider}/${model}`,
1689
- label: `${provider}/${model}`,
1690
- hint: `${keyStatus} ${info.envVar}`
1691
- });
2470
+ let copied = 0;
2471
+ let skipped = 0;
2472
+ for (const example of examples) {
2473
+ const result = await copyExample(example.id, formsDir, overwrite, quiet);
2474
+ if (result.copied) {
2475
+ copied++;
2476
+ if (!quiet) console.log(formatFormLogLine(example, ` ${pc.green("✓")}`));
2477
+ } else if (result.skipped) {
2478
+ skipped++;
2479
+ if (!quiet) console.log(`${formatFormLogLine(example, ` ${pc.yellow("○")}`)} ${pc.dim("(exists, skipped)")}`);
2480
+ }
1692
2481
  }
1693
- options.push({
1694
- value: "custom",
1695
- label: "Enter custom model ID...",
1696
- hint: "provider/model-id format"
1697
- });
1698
- return options;
2482
+ if (!quiet) {
2483
+ console.log("");
2484
+ if (skipped > 0) console.log(pc.yellow(`Skipped ${skipped} existing file(s). Use --overwrite to replace them.`));
2485
+ console.log(pc.green(`Done! Copied ${copied} example form(s) to ${formatPath(formsDir)}`));
2486
+ }
2487
+ return {
2488
+ copied,
2489
+ skipped
2490
+ };
1699
2491
  }
1700
2492
  /**
1701
- * Prompt user to select a model with web search capability for research workflow.
2493
+ * Show form selection menu and return the selected path.
1702
2494
  */
1703
- async function promptForWebSearchModel() {
1704
- const modelOptions = buildWebSearchModelOptions();
1705
- if (modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
2495
+ async function showFormMenu(formsDir) {
2496
+ const entries = await getFormEntries(formsDir);
2497
+ if (entries.length === 0) return null;
2498
+ const sortedEntries = [...entries].sort((a, b) => {
2499
+ return getExampleOrder(a.filename) - getExampleOrder(b.filename);
2500
+ });
2501
+ const defaultExample = getExampleById(DEFAULT_EXAMPLE_ID);
2502
+ const defaultIndex = sortedEntries.findIndex((e) => e.filename === defaultExample?.filename);
2503
+ const menuOptions = sortedEntries.map((entry) => ({
2504
+ value: entry.path,
2505
+ label: formatFormLabel(entry),
2506
+ hint: formatFormHint(entry)
2507
+ }));
2508
+ const initialValue = (defaultIndex >= 0 ? sortedEntries[defaultIndex] : void 0)?.path;
1706
2509
  const selection = await p.select({
1707
- message: "Select LLM model (web search required):",
1708
- options: modelOptions
2510
+ message: "Select a form to run:",
2511
+ options: menuOptions,
2512
+ initialValue
1709
2513
  });
1710
2514
  if (p.isCancel(selection)) return null;
1711
- if (selection === "custom") {
1712
- const customModel = await p.text({
1713
- message: "Model ID (provider/model-id):",
1714
- placeholder: "openai/gpt-5-mini",
1715
- validate: (value) => {
1716
- if (!value.includes("/")) return "Format: provider/model-id (e.g., openai/gpt-5-mini)";
1717
- }
1718
- });
1719
- if (p.isCancel(customModel)) return null;
1720
- return customModel;
1721
- }
1722
2515
  return selection;
1723
2516
  }
1724
2517
  /**
1725
- * Run the agent fill workflow.
1726
- * Accepts optional harness config overrides - research uses different defaults.
1727
- */
1728
- async function runAgentFill(form, modelId, _outputPath, configOverrides) {
1729
- const { provider: providerName, model: modelName } = parseModelIdForDisplay(modelId);
1730
- const resolveSpinner = createSpinner({
1731
- type: "compute",
1732
- operation: `Resolving model: ${modelId}`
1733
- });
1734
- let model, provider;
1735
- try {
1736
- const result = await resolveModel(modelId);
1737
- model = result.model;
1738
- provider = result.provider;
1739
- resolveSpinner.stop(`✓ Model resolved: ${modelId}`);
1740
- } catch (error) {
1741
- resolveSpinner.error("Model resolution failed");
1742
- throw error;
1743
- }
1744
- const harnessConfig = {
1745
- maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
1746
- maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
1747
- maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
1748
- targetRoles: [AGENT_ROLE],
1749
- fillMode: "continue"
1750
- };
1751
- console.log("");
1752
- console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
1753
- const harness = createHarness(form, harnessConfig);
1754
- const agent = createLiveAgent({
1755
- model,
1756
- provider,
1757
- targetRole: AGENT_ROLE,
1758
- enableWebSearch: true
1759
- });
1760
- p.log.step(pc.bold("Agent fill in progress..."));
1761
- let stepResult = harness.step();
1762
- while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1763
- console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1764
- const llmSpinner = createSpinner({
1765
- type: "api",
1766
- provider: providerName,
1767
- model: modelName,
1768
- turnNumber: stepResult.turnNumber
1769
- });
1770
- let response;
1771
- try {
1772
- response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1773
- llmSpinner.stop();
1774
- } catch (error) {
1775
- llmSpinner.error("LLM call failed");
1776
- throw error;
1777
- }
1778
- const { patches, stats } = response;
1779
- for (const patch of patches) {
1780
- const typeName = formatPatchType(patch);
1781
- const value = formatPatchValue(patch);
1782
- const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
1783
- if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
1784
- else console.log(` (${typeName}) = ${pc.green(value)}`);
1785
- }
1786
- stepResult = harness.apply(patches, stepResult.issues);
1787
- const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
1788
- console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
1789
- if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1790
- }
1791
- if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
1792
- else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
1793
- Object.assign(form, harness.getForm());
1794
- return {
1795
- success: stepResult.isComplete,
1796
- turnCount: harness.getTurnNumber()
1797
- };
1798
- }
1799
- /**
1800
- * Run the interactive example scaffolding and filling flow.
1801
- *
1802
- * @param preselectedId Optional example ID to pre-select
1803
- * @param formsDirOverride Optional forms directory override from CLI option
2518
+ * Copy a specific example to the forms directory.
1804
2519
  */
1805
- async function runInteractiveFlow(preselectedId, formsDirOverride) {
1806
- const startTime = Date.now();
1807
- p.intro(pc.bgCyan(pc.black(" markform examples ")));
1808
- const formsDir = getFormsDir(formsDirOverride);
1809
- await ensureFormsDir(formsDir);
1810
- showApiStatus();
1811
- let selectedId = preselectedId;
1812
- if (!selectedId) {
1813
- const examples = getAllExamplesWithMetadata();
1814
- const selection = await p.select({
1815
- message: "Select an example form to scaffold:",
1816
- options: examples.map((example$1) => ({
1817
- value: example$1.id,
1818
- label: example$1.title ?? example$1.id,
1819
- hint: example$1.description
1820
- }))
1821
- });
1822
- if (p.isCancel(selection)) {
1823
- p.cancel("Cancelled.");
1824
- process.exit(0);
1825
- }
1826
- selectedId = selection;
1827
- }
1828
- const example = getExampleById(selectedId);
1829
- if (!example) {
1830
- p.cancel(`Unknown example: ${selectedId}`);
1831
- process.exit(1);
1832
- }
1833
- const defaultFilename = basename(generateVersionedPathInFormsDir(example.filename, formsDir));
1834
- const filenameResult = await p.text({
1835
- message: `Output filename (in ${formatPath(formsDir)}):`,
1836
- initialValue: defaultFilename,
1837
- validate: (value) => {
1838
- if (!value.trim()) return "Filename is required";
1839
- if (!value.endsWith(".form.md") && !value.endsWith(".md")) return "Filename should end with .form.md or .md";
1840
- }
1841
- });
1842
- if (p.isCancel(filenameResult)) {
1843
- p.cancel("Cancelled.");
1844
- process.exit(0);
1845
- }
1846
- const filename = filenameResult;
1847
- const outputPath = join(formsDir, filename);
1848
- if (existsSync(outputPath)) {
1849
- const overwrite = await p.confirm({
1850
- message: `${filename} already exists. Overwrite?`,
1851
- initialValue: false
1852
- });
1853
- if (p.isCancel(overwrite) || !overwrite) {
1854
- p.cancel("Cancelled.");
1855
- process.exit(0);
1856
- }
1857
- }
1858
- let content;
1859
- try {
1860
- content = loadExampleContent(selectedId);
1861
- await writeFile(outputPath, content);
1862
- } catch (error) {
1863
- const message = error instanceof Error ? error.message : String(error);
1864
- p.cancel(`Failed to write file: ${message}`);
1865
- process.exit(1);
1866
- }
1867
- p.log.success(`Created ${formatPath(outputPath)}`);
1868
- const form = parseForm(content);
1869
- const targetRoles = [USER_ROLE];
1870
- let userFillOutputs = null;
1871
- const inspectResult = inspect(form, { targetRoles });
1872
- const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
1873
- const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
1874
- if (uniqueFieldIds.size === 0) {
1875
- p.log.info("No user-role fields to fill in this example.");
1876
- if (inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field").length === 0) {
1877
- logTiming({
1878
- verbose: false,
1879
- format: "console",
1880
- dryRun: false,
1881
- quiet: false
1882
- }, "Total time", Date.now() - startTime);
1883
- p.outro("Form scaffolded with no fields to fill.");
1884
- return;
1885
- }
1886
- } else {
1887
- showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
1888
- const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
1889
- if (cancelled) {
1890
- showInteractiveOutro(0, true);
1891
- process.exit(1);
1892
- }
1893
- if (patches.length > 0) applyPatches(form, patches);
1894
- userFillOutputs = await exportMultiFormat(form, outputPath);
1895
- showInteractiveOutro(patches.length, false);
1896
- console.log("");
1897
- p.log.success("Outputs:");
1898
- console.log(` ${formatPath(userFillOutputs.reportPath)} ${pc.dim("(output report)")}`);
1899
- console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(output values)")}`);
1900
- console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(filled markform source)")}`);
1901
- logTiming({
1902
- verbose: false,
1903
- format: "console",
1904
- dryRun: false,
1905
- quiet: false
1906
- }, "Fill time", Date.now() - startTime);
1907
- }
1908
- const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
1909
- const isResearchExample = example.type === "research";
1910
- if (agentFieldIssues.length > 0) {
1911
- console.log("");
1912
- const workflowLabel = isResearchExample ? "research" : "agent fill";
1913
- p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
1914
- const confirmMessage = isResearchExample ? "Run research now? (requires web search)" : "Run agent fill now?";
1915
- const runAgent = await p.confirm({
1916
- message: confirmMessage,
1917
- initialValue: true
1918
- });
1919
- if (p.isCancel(runAgent) || !runAgent) {
2520
+ async function copySingleExample(exampleId, formsDir, overwrite, quiet) {
2521
+ const example = getExampleById(exampleId);
2522
+ if (!example) throw new Error(`Unknown example: ${exampleId}`);
2523
+ if (!quiet) console.log(`Copying ${example.filename} to ${formatPath(formsDir)}...`);
2524
+ const result = await copyExample(exampleId, formsDir, overwrite, quiet);
2525
+ if (result.copied) {
2526
+ if (!quiet) {
2527
+ console.log(formatFormLogLine(example, ` ${pc.green("✓")}`));
1920
2528
  console.log("");
1921
- const cliCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=<provider/model>` : ` markform fill ${formatPath(outputPath)} --model=<provider/model>`;
1922
- console.log(`You can run ${workflowLabel} later with:`);
1923
- console.log(cliCommand);
1924
- if (userFillOutputs) await showFileViewerChooser([
1925
- {
1926
- path: userFillOutputs.reportPath,
1927
- label: "Report",
1928
- hint: "output report"
1929
- },
1930
- {
1931
- path: userFillOutputs.yamlPath,
1932
- label: "Values",
1933
- hint: "output values"
1934
- },
1935
- {
1936
- path: userFillOutputs.formPath,
1937
- label: "Form",
1938
- hint: "filled markform source"
1939
- }
1940
- ]);
1941
- p.outro("Happy form filling!");
1942
- return;
1943
- }
1944
- const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
1945
- if (!modelId) {
1946
- p.cancel("Cancelled.");
1947
- process.exit(0);
1948
- }
1949
- const agentDefaultFilename = basename(generateVersionedPathInFormsDir(outputPath, formsDir));
1950
- const agentFilenameResult = await p.text({
1951
- message: `Agent output filename (in ${formatPath(formsDir)}):`,
1952
- initialValue: agentDefaultFilename,
1953
- validate: (value) => {
1954
- if (!value.trim()) return "Filename is required";
1955
- }
1956
- });
1957
- if (p.isCancel(agentFilenameResult)) {
1958
- p.cancel("Cancelled.");
1959
- process.exit(0);
2529
+ console.log(pc.green("Done!"));
2530
+ console.log(`Run ${pc.cyan(`'markform run ${example.filename}'`)} to try it.`);
1960
2531
  }
1961
- const agentOutputPath = join(formsDir, agentFilenameResult);
1962
- const agentStartTime = Date.now();
1963
- const timingLabel = isResearchExample ? "Research time" : "Agent fill time";
1964
- const configOverrides = isResearchExample ? {
1965
- maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
1966
- maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
1967
- } : void 0;
1968
- try {
1969
- const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
1970
- logTiming({
1971
- verbose: false,
1972
- format: "console",
1973
- dryRun: false,
1974
- quiet: false
1975
- }, timingLabel, Date.now() - agentStartTime);
1976
- const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, agentOutputPath);
1977
- console.log("");
1978
- const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
1979
- p.log.success(successMessage);
1980
- console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
1981
- console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
1982
- console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
1983
- if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
1984
- await showFileViewerChooser([
1985
- {
1986
- path: reportPath,
1987
- label: "Report",
1988
- hint: "output report"
1989
- },
1990
- {
1991
- path: yamlPath,
1992
- label: "Values",
1993
- hint: "output values"
1994
- },
1995
- {
1996
- path: formPath,
1997
- label: "Form",
1998
- hint: "filled markform source"
1999
- }
2000
- ]);
2001
- } catch (error) {
2002
- const message = error instanceof Error ? error.message : String(error);
2003
- const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
2004
- p.log.error(`${failMessage}: ${message}`);
2532
+ } else if (result.skipped) {
2533
+ if (!quiet) {
2534
+ console.log(`${formatFormLogLine(example, ` ${pc.yellow("")}`)} ${pc.dim("(exists, skipped)")}`);
2005
2535
  console.log("");
2006
- console.log("You can try again with:");
2007
- const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
2008
- console.log(retryCommand);
2536
+ console.log(pc.yellow(`File already exists. Use --overwrite to replace it.`));
2009
2537
  }
2010
2538
  }
2011
- p.outro("Happy form filling!");
2012
2539
  }
2013
2540
  /**
2014
2541
  * Register the examples command.
2015
2542
  */
2016
2543
  function registerExamplesCommand(program) {
2017
- program.command("examples").description("Try out some example forms interactively using the console").option("--list", "List available examples without interactive selection").option("--name <example>", "Select example by ID (still prompts for filename)").action(async (options, cmd) => {
2544
+ program.command("examples").description("Copy bundled example forms to the forms directory").option("--list", "List available examples without copying").option("--name <example>", "Copy specific example by ID").action(async (options, cmd) => {
2018
2545
  const ctx = getCommandContext(cmd);
2019
2546
  try {
2020
2547
  if (options.list) {
2021
2548
  printExamplesList();
2022
2549
  return;
2023
2550
  }
2551
+ const formsDir = getFormsDir(ctx.formsDir);
2552
+ await ensureFormsDir(formsDir);
2024
2553
  if (options.name) {
2025
2554
  if (!getExampleById(options.name)) {
2026
2555
  logError(`Unknown example: ${options.name}`);
@@ -2028,8 +2557,35 @@ function registerExamplesCommand(program) {
2028
2557
  for (const ex of EXAMPLE_DEFINITIONS) console.log(` ${ex.id}`);
2029
2558
  process.exit(1);
2030
2559
  }
2560
+ await copySingleExample(options.name, formsDir, ctx.overwrite, ctx.quiet);
2561
+ } else {
2562
+ const { copied } = await copyAllExamples(formsDir, ctx.overwrite, ctx.quiet);
2563
+ if (!ctx.quiet && copied > 0) {
2564
+ console.log("");
2565
+ const wantToRun = await p.confirm({
2566
+ message: "Do you want to try running a form?",
2567
+ initialValue: true
2568
+ });
2569
+ if (p.isCancel(wantToRun) || !wantToRun) {
2570
+ console.log("");
2571
+ console.log(`Run ${pc.cyan("'markform run'")} to select and run a form later.`);
2572
+ } else {
2573
+ const selectedPath = await showFormMenu(formsDir);
2574
+ if (selectedPath) {
2575
+ console.log("");
2576
+ const exportResult = await runForm(selectedPath, formsDir, ctx.overwrite);
2577
+ if (exportResult) {
2578
+ console.log("");
2579
+ const wantToBrowse = await p.confirm({
2580
+ message: "Would you like to view the output files?",
2581
+ initialValue: true
2582
+ });
2583
+ if (!p.isCancel(wantToBrowse) && wantToBrowse) await browseOutputFiles(exportResult.formPath.replace(/\.form\.md$/, ""));
2584
+ }
2585
+ }
2586
+ }
2587
+ }
2031
2588
  }
2032
- await runInteractiveFlow(options.name, ctx.formsDir);
2033
2589
  } catch (error) {
2034
2590
  logError(error instanceof Error ? error.message : String(error));
2035
2591
  process.exit(1);
@@ -2101,6 +2657,39 @@ function registerExportCommand(program) {
2101
2657
  });
2102
2658
  }
2103
2659
 
2660
+ //#endregion
2661
+ //#region src/cli/lib/fillCallbacks.ts
2662
+ /**
2663
+ * Create FillCallbacks for CLI commands.
2664
+ *
2665
+ * Provides spinner feedback during tool execution (especially web search).
2666
+ * Only implements tool callbacks - turn/LLM callbacks are handled by CLI's
2667
+ * own logging which has richer context.
2668
+ *
2669
+ * @param spinner - Active spinner handle to update
2670
+ * @param ctx - Command context for verbose logging
2671
+ * @returns FillCallbacks with onToolStart and onToolEnd
2672
+ *
2673
+ * @example
2674
+ * ```typescript
2675
+ * const spinner = createSpinner({ type: 'api', provider, model });
2676
+ * const callbacks = createCliToolCallbacks(spinner, ctx);
2677
+ * const agent = createLiveAgent({ model, callbacks, ... });
2678
+ * ```
2679
+ */
2680
+ function createCliToolCallbacks(spinner, ctx) {
2681
+ return {
2682
+ onToolStart: ({ name }) => {
2683
+ if (name.includes("search")) spinner.message(`🔍 Web search...`);
2684
+ logVerbose(ctx, ` Tool started: ${name}`);
2685
+ },
2686
+ onToolEnd: ({ name, durationMs, error }) => {
2687
+ if (error) logVerbose(ctx, ` Tool ${name} failed: ${error} (${durationMs}ms)`);
2688
+ else logVerbose(ctx, ` Tool ${name} completed (${durationMs}ms)`);
2689
+ }
2690
+ };
2691
+ }
2692
+
2104
2693
  //#endregion
2105
2694
  //#region src/cli/commands/fill.ts
2106
2695
  /**
@@ -2202,13 +2791,14 @@ function registerFillCommand(program) {
2202
2791
  logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
2203
2792
  showInteractiveOutro(patches.length, false);
2204
2793
  } else {
2205
- const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, outputPath$1);
2794
+ const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(form, outputPath$1);
2206
2795
  showInteractiveOutro(patches.length, false);
2207
2796
  console.log("");
2208
2797
  p.log.success("Outputs:");
2209
2798
  console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
2210
2799
  console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
2211
2800
  console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
2801
+ console.log(` ${formatPath(schemaPath)} ${pc.dim("(JSON Schema)")}`);
2212
2802
  }
2213
2803
  logTiming(ctx, "Fill time", durationMs$1);
2214
2804
  if (patches.length > 0) {
@@ -2243,6 +2833,7 @@ function registerFillCommand(program) {
2243
2833
  let mockPath;
2244
2834
  let agentProvider;
2245
2835
  let agentModelName;
2836
+ let currentSpinner = null;
2246
2837
  if (options.mock) {
2247
2838
  mockPath = resolve(options.mockSource);
2248
2839
  logVerbose(ctx, `Reading mock source: ${mockPath}`);
@@ -2262,13 +2853,21 @@ function registerFillCommand(program) {
2262
2853
  logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
2263
2854
  systemPrompt = await readFile$1(promptPath);
2264
2855
  }
2856
+ const callbacks = createCliToolCallbacks({
2857
+ message: (msg) => currentSpinner?.message(msg),
2858
+ update: (context) => currentSpinner?.update(context),
2859
+ stop: (msg) => currentSpinner?.stop(msg),
2860
+ error: (msg) => currentSpinner?.error(msg),
2861
+ getElapsedMs: () => currentSpinner?.getElapsedMs() ?? 0
2862
+ }, ctx);
2265
2863
  const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
2266
2864
  const liveAgent = createLiveAgent({
2267
2865
  model,
2268
2866
  provider,
2269
2867
  systemPromptAddition: systemPrompt,
2270
2868
  targetRole: primaryRole,
2271
- enableWebSearch: true
2869
+ enableWebSearch: true,
2870
+ callbacks
2272
2871
  });
2273
2872
  agent = liveAgent;
2274
2873
  logInfo(ctx, `Available tools: ${liveAgent.getAvailableToolNames().join(", ")}`);
@@ -2285,18 +2884,23 @@ function registerFillCommand(program) {
2285
2884
  logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
2286
2885
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
2287
2886
  let spinner = null;
2288
- if (!options.mock && agentProvider && agentModelName && process.stdout.isTTY && !ctx.quiet) spinner = createSpinner({
2289
- type: "api",
2290
- provider: agentProvider,
2291
- model: agentModelName,
2292
- turnNumber: stepResult.turnNumber
2293
- });
2887
+ if (!options.mock && agentProvider && agentModelName && process.stdout.isTTY && !ctx.quiet) {
2888
+ spinner = createSpinner({
2889
+ type: "api",
2890
+ provider: agentProvider,
2891
+ model: agentModelName,
2892
+ turnNumber: stepResult.turnNumber
2893
+ });
2894
+ currentSpinner = spinner;
2895
+ }
2294
2896
  let response;
2295
2897
  try {
2296
2898
  response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
2297
2899
  spinner?.stop();
2900
+ currentSpinner = null;
2298
2901
  } catch (error) {
2299
2902
  spinner?.error("LLM call failed");
2903
+ currentSpinner = null;
2300
2904
  throw error;
2301
2905
  }
2302
2906
  const { patches, stats } = response;
@@ -2475,7 +3079,7 @@ function formatFieldValue(value, useColors) {
2475
3079
  /**
2476
3080
  * Format inspect report for console output.
2477
3081
  */
2478
- function formatConsoleReport$1(report, useColors) {
3082
+ function formatConsoleReport$2(report, useColors) {
2479
3083
  const lines = [];
2480
3084
  const bold = useColors ? pc.bold : (s) => s;
2481
3085
  const dim = useColors ? pc.dim : (s) => s;
@@ -2588,7 +3192,7 @@ function registerInspectCommand(program) {
2588
3192
  severity: issue.severity,
2589
3193
  blockedBy: issue.blockedBy
2590
3194
  }))
2591
- }, (data, useColors) => formatConsoleReport$1(data, useColors));
3195
+ }, (data, useColors) => formatConsoleReport$2(data, useColors));
2592
3196
  console.log(output);
2593
3197
  } catch (error) {
2594
3198
  logError(error instanceof Error ? error.message : String(error));
@@ -4029,15 +4633,16 @@ function registerResearchCommand(program) {
4029
4633
  logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
4030
4634
  logInfo(ctx, `Turns: ${result.totalTurns}`);
4031
4635
  if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
4032
- const { reportPath, yamlPath, formPath } = await exportMultiFormat(result.form, outputPath);
4636
+ const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(result.form, outputPath);
4033
4637
  logSuccess(ctx, "Outputs:");
4034
4638
  console.log(` ${reportPath} ${pc.dim("(output report)")}`);
4035
4639
  console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
4036
4640
  console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
4641
+ console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
4037
4642
  if (options.transcript && result.transcript) {
4038
- const { serializeSession: serializeSession$1 } = await import("./session-B_stoXQn.mjs");
4643
+ const { serializeSession: serializeSession$1 } = await import("./session-DruaYPZ1.mjs");
4039
4644
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
4040
- const { writeFile: writeFile$1 } = await import("./shared-CZsyShck.mjs");
4645
+ const { writeFile: writeFile$1 } = await import("./shared-C9yW5FLZ.mjs");
4041
4646
  await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
4042
4647
  logInfo(ctx, `Transcript: ${transcriptPath}`);
4043
4648
  }
@@ -4049,6 +4654,195 @@ function registerResearchCommand(program) {
4049
4654
  });
4050
4655
  }
4051
4656
 
4657
+ //#endregion
4658
+ //#region src/cli/commands/schema.ts
4659
+ const VALID_DRAFTS = [
4660
+ "2020-12",
4661
+ "2019-09",
4662
+ "draft-07"
4663
+ ];
4664
+ /**
4665
+ * Register the schema command.
4666
+ */
4667
+ function registerSchemaCommand(program) {
4668
+ program.command("schema <file>").description("Export form structure as JSON Schema").option("--pure", "Exclude x-markform extension properties").option("--draft <version>", `JSON Schema draft version: ${VALID_DRAFTS.join(", ")}`, "2020-12").option("--compact", "Output compact JSON (no formatting)").action(async (file, options, cmd) => {
4669
+ const ctx = getCommandContext(cmd);
4670
+ try {
4671
+ const draft = options.draft ?? "2020-12";
4672
+ if (!VALID_DRAFTS.includes(draft)) throw new Error(`Invalid draft version: ${options.draft}. Valid options: ${VALID_DRAFTS.join(", ")}`);
4673
+ logVerbose(ctx, `Reading file: ${file}`);
4674
+ const content = await readFile$1(file);
4675
+ logVerbose(ctx, "Parsing form...");
4676
+ const form = parseForm(content);
4677
+ logVerbose(ctx, "Generating JSON Schema...");
4678
+ const result = formToJsonSchema(form, {
4679
+ includeExtensions: !options.pure,
4680
+ draft
4681
+ });
4682
+ if (ctx.format === "yaml") console.log(YAML.stringify(result.schema));
4683
+ else if (options.compact) console.log(JSON.stringify(result.schema));
4684
+ else console.log(JSON.stringify(result.schema, null, 2));
4685
+ } catch (error) {
4686
+ logError(error instanceof Error ? error.message : String(error));
4687
+ process.exit(1);
4688
+ }
4689
+ });
4690
+ }
4691
+
4692
+ //#endregion
4693
+ //#region src/cli/commands/status.ts
4694
+ /**
4695
+ * Status command - Display form fill status with per-role breakdown.
4696
+ *
4697
+ * Provides:
4698
+ * - Overall progress counts
4699
+ * - Per-role fill statistics
4700
+ * - Run mode (explicit or inferred)
4701
+ * - Suggested next command
4702
+ */
4703
+ /**
4704
+ * Compute field statistics from a list of fields.
4705
+ */
4706
+ function computeFieldStats(form, fields) {
4707
+ let answered = 0;
4708
+ let skipped = 0;
4709
+ let aborted = 0;
4710
+ let unanswered = 0;
4711
+ for (const field of fields) switch (form.responsesByFieldId[field.id]?.state ?? "unanswered") {
4712
+ case "answered":
4713
+ answered++;
4714
+ break;
4715
+ case "skipped":
4716
+ skipped++;
4717
+ break;
4718
+ case "aborted":
4719
+ aborted++;
4720
+ break;
4721
+ case "unanswered":
4722
+ default:
4723
+ unanswered++;
4724
+ break;
4725
+ }
4726
+ return {
4727
+ total: fields.length,
4728
+ answered,
4729
+ skipped,
4730
+ aborted,
4731
+ unanswered
4732
+ };
4733
+ }
4734
+ /**
4735
+ * Compute statistics grouped by role.
4736
+ */
4737
+ function computeStatsByRole(form) {
4738
+ const allFields = getAllFields(form);
4739
+ const roles = getFieldRoles(form);
4740
+ const result = {};
4741
+ for (const role of roles) result[role] = computeFieldStats(form, allFields.filter((f) => f.role === role));
4742
+ return result;
4743
+ }
4744
+ /**
4745
+ * Format percentage with one decimal place.
4746
+ */
4747
+ function formatPercent(numerator, denominator) {
4748
+ if (denominator === 0) return "0%";
4749
+ return `${Math.round(numerator / denominator * 100)}%`;
4750
+ }
4751
+ /**
4752
+ * Format status report for console output.
4753
+ */
4754
+ function formatConsoleReport$1(report, useColors) {
4755
+ const lines = [];
4756
+ const bold = useColors ? pc.bold : (s) => s;
4757
+ const dim = useColors ? pc.dim : (s) => s;
4758
+ const cyan = useColors ? pc.cyan : (s) => s;
4759
+ const green = useColors ? pc.green : (s) => s;
4760
+ const yellow = useColors ? pc.yellow : (s) => s;
4761
+ const red = useColors ? pc.red : (s) => s;
4762
+ lines.push(bold(cyan(`Form Status: ${basename(report.path)}`)));
4763
+ lines.push("");
4764
+ const overall = report.overall;
4765
+ const overallPercent = formatPercent(overall.answered, overall.total);
4766
+ lines.push(`${bold("Overall:")} ${overall.answered}/${overall.total} fields filled (${overallPercent})`);
4767
+ lines.push(` ${green("✓")} Complete: ${overall.answered}`);
4768
+ lines.push(` ${dim("○")} Empty: ${overall.unanswered}`);
4769
+ if (overall.skipped > 0) lines.push(` ${yellow("⊘")} Skipped: ${overall.skipped}`);
4770
+ if (overall.aborted > 0) lines.push(` ${red("✗")} Aborted: ${overall.aborted}`);
4771
+ lines.push("");
4772
+ lines.push(bold("By Role:"));
4773
+ const roles = Object.keys(report.byRole).sort((a, b) => {
4774
+ if (a === USER_ROLE) return -1;
4775
+ if (b === USER_ROLE) return 1;
4776
+ if (a === AGENT_ROLE) return -1;
4777
+ if (b === AGENT_ROLE) return 1;
4778
+ return a.localeCompare(b);
4779
+ });
4780
+ for (const role of roles) {
4781
+ const stats = report.byRole[role];
4782
+ if (!stats) continue;
4783
+ const percent = formatPercent(stats.answered, stats.total);
4784
+ const needsAttention = role === USER_ROLE && stats.unanswered > 0 ? yellow(" ← needs attention") : "";
4785
+ lines.push(` ${role}: ${stats.answered}/${stats.total} filled (${percent})${needsAttention}`);
4786
+ }
4787
+ lines.push("");
4788
+ if (report.runMode) {
4789
+ const source = report.runModeSource === "explicit" ? "explicit" : "inferred";
4790
+ lines.push(`${bold("Run Mode:")} ${report.runMode} (${source})`);
4791
+ } else lines.push(`${bold("Run Mode:")} ${dim("unknown")}`);
4792
+ if (report.suggestedCommand) lines.push(`${bold("Suggested:")} ${cyan(report.suggestedCommand)}`);
4793
+ return lines.join("\n");
4794
+ }
4795
+ /**
4796
+ * Generate a suggested command based on status.
4797
+ */
4798
+ function getSuggestedCommand(report) {
4799
+ const { overall, byRole, runMode, path } = report;
4800
+ const filename = basename(path);
4801
+ if (overall.total > 0 && overall.answered === overall.total) return null;
4802
+ const userStats = byRole[USER_ROLE];
4803
+ if (userStats && userStats.unanswered > 0) return `markform fill ${filename} --interactive`;
4804
+ if (runMode === "research") return `markform research ${filename}`;
4805
+ return `markform run ${filename}`;
4806
+ }
4807
+ /**
4808
+ * Register the status command.
4809
+ */
4810
+ function registerStatusCommand(program) {
4811
+ program.command("status <file>").description("Display form fill status with per-role breakdown").action(async (file, _options, cmd) => {
4812
+ const ctx = getCommandContext(cmd);
4813
+ try {
4814
+ logVerbose(ctx, `Reading file: ${file}`);
4815
+ const content = await readFile$1(file);
4816
+ logVerbose(ctx, "Parsing form...");
4817
+ const form = parseForm(content);
4818
+ logVerbose(ctx, "Computing status...");
4819
+ const overall = computeFieldStats(form, getAllFields(form));
4820
+ const byRole = computeStatsByRole(form);
4821
+ const runModeResult = determineRunMode(form);
4822
+ let runMode = null;
4823
+ let runModeSource = "unknown";
4824
+ if (runModeResult.success) {
4825
+ runMode = runModeResult.runMode;
4826
+ runModeSource = runModeResult.source;
4827
+ }
4828
+ const report = {
4829
+ path: file,
4830
+ runMode,
4831
+ runModeSource,
4832
+ overall,
4833
+ byRole,
4834
+ suggestedCommand: null
4835
+ };
4836
+ report.suggestedCommand = getSuggestedCommand(report);
4837
+ const output = formatOutput(ctx, report, (data, useColors) => formatConsoleReport$1(data, useColors));
4838
+ console.log(output);
4839
+ } catch (error) {
4840
+ logError(error instanceof Error ? error.message : String(error));
4841
+ process.exit(1);
4842
+ }
4843
+ });
4844
+ }
4845
+
4052
4846
  //#endregion
4053
4847
  //#region src/cli/commands/validate.ts
4054
4848
  /**
@@ -4183,12 +4977,13 @@ function withColoredHelp(cmd) {
4183
4977
  */
4184
4978
  function createProgram() {
4185
4979
  const program = withColoredHelp(new Command());
4186
- program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION, "--version", "output the version number").showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`);
4980
+ program.name("markform").description("Agent-friendly, human-readable, editable forms").version(CLI_VERSION, "--version", "output the version number").showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`).option("--overwrite", "Overwrite existing field values (default: continue/skip filled)");
4187
4981
  registerReadmeCommand(program);
4188
4982
  registerDocsCommand(program);
4189
4983
  registerSpecCommand(program);
4190
4984
  registerApisCommand(program);
4191
4985
  registerApplyCommand(program);
4986
+ registerBrowseCommand(program);
4192
4987
  registerDumpCommand(program);
4193
4988
  registerExamplesCommand(program);
4194
4989
  registerExportCommand(program);
@@ -4198,7 +4993,10 @@ function createProgram() {
4198
4993
  registerRenderCommand(program);
4199
4994
  registerReportCommand(program);
4200
4995
  registerResearchCommand(program);
4996
+ registerRunCommand(program);
4997
+ registerSchemaCommand(program);
4201
4998
  registerServeCommand(program);
4999
+ registerStatusCommand(program);
4202
5000
  registerValidateCommand(program);
4203
5001
  return program;
4204
5002
  }