markform 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/DOCS.md +546 -0
  2. package/README.md +484 -45
  3. package/SPEC.md +2779 -0
  4. package/dist/ai-sdk.d.mts +2 -2
  5. package/dist/ai-sdk.mjs +5 -3
  6. package/dist/{apply-C0vjijlP.mjs → apply-BfAGTHMh.mjs} +1044 -593
  7. package/dist/bin.mjs +6 -3
  8. package/dist/cli-B3NVm6zL.mjs +3904 -0
  9. package/dist/cli.mjs +6 -3
  10. package/dist/{coreTypes-T7dAuewt.d.mts → coreTypes-BXhhz9Iq.d.mts} +2795 -685
  11. package/dist/coreTypes-Dful87E0.mjs +537 -0
  12. package/dist/index.d.mts +196 -18
  13. package/dist/index.mjs +5 -3
  14. package/dist/session-Bqnwi9wp.mjs +110 -0
  15. package/dist/session-DdAtY2Ni.mjs +4 -0
  16. package/dist/shared-D7gf27Tr.mjs +3 -0
  17. package/dist/shared-N_s1M-_K.mjs +176 -0
  18. package/dist/src-BXRkGFpG.mjs +7587 -0
  19. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
  20. package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
  21. package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
  22. package/examples/movie-research/movie-research-basic.form.md +164 -0
  23. package/examples/movie-research/movie-research-deep.form.md +486 -0
  24. package/examples/movie-research/movie-research-minimal.form.md +73 -0
  25. package/examples/simple/simple-mock-filled.form.md +52 -12
  26. package/examples/simple/simple-skipped-filled.form.md +170 -0
  27. package/examples/simple/simple-with-skips.session.yaml +189 -0
  28. package/examples/simple/simple.form.md +34 -12
  29. package/examples/simple/simple.session.yaml +80 -37
  30. package/examples/startup-deep-research/startup-deep-research.form.md +456 -0
  31. package/examples/startup-research/startup-research-mock-filled.form.md +307 -0
  32. package/examples/startup-research/startup-research.form.md +211 -0
  33. package/package.json +11 -5
  34. package/dist/cli-9fvFySww.mjs +0 -2564
  35. package/dist/src-DBD3Dt4f.mjs +0 -1785
  36. package/examples/political-research/political-research.form.md +0 -233
  37. package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
@@ -0,0 +1,3904 @@
1
+ import { N as PatchSchema } from "./coreTypes-Dful87E0.mjs";
2
+ import { A as SUGGESTED_LLMS, D as detectFileType, E as deriveExportPath, M as formatSuggestedLlms, O as getFormsDir, P as hasWebSearchSupport, 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 WEB_SEARCH_CONFIG, k as parseRolesFlag, 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-BfAGTHMh.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, v as parseForm } from "./src-BXRkGFpG.mjs";
4
+ import { n as serializeSession } from "./session-Bqnwi9wp.mjs";
5
+ import { a as getCommandContext, c as logInfo, d as logVerbose, f as logWarn, h as writeFile, i as formatPath, l as logSuccess, n as ensureFormsDir, o as logDryRun, p as readFile$1, r as formatOutput, s as logError, t as OUTPUT_FORMATS, u as logTiming } from "./shared-N_s1M-_K.mjs";
6
+ import YAML from "yaml";
7
+ import { basename, dirname, join, resolve } from "node:path";
8
+ import { Command } from "commander";
9
+ import pc from "picocolors";
10
+ import { readFile } from "node:fs/promises";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
13
+ import * as p from "@clack/prompts";
14
+ import { exec, spawn } from "node:child_process";
15
+ import { createServer } from "node:http";
16
+
17
+ //#region src/cli/commands/apply.ts
18
+ /**
19
+ * Format state badge for console output.
20
+ */
21
+ function formatState$2(state, useColors) {
22
+ const [text, colorFn] = {
23
+ complete: ["✓ complete", pc.green],
24
+ incomplete: ["○ incomplete", pc.yellow],
25
+ empty: ["◌ empty", pc.dim],
26
+ invalid: ["✗ invalid", pc.red]
27
+ }[state] ?? [state, (s) => s];
28
+ return useColors ? colorFn(text) : text;
29
+ }
30
+ /**
31
+ * Format apply report for console output.
32
+ */
33
+ function formatConsoleReport$2(report, useColors) {
34
+ const lines = [];
35
+ const bold = useColors ? pc.bold : (s) => s;
36
+ const dim = useColors ? pc.dim : (s) => s;
37
+ const cyan = useColors ? pc.cyan : (s) => s;
38
+ const green = useColors ? pc.green : (s) => s;
39
+ const red = useColors ? pc.red : (s) => s;
40
+ lines.push(bold(cyan("Apply Result")));
41
+ lines.push("");
42
+ const statusColor = report.apply_status === "applied" ? green : red;
43
+ lines.push(`${bold("Status:")} ${statusColor(report.apply_status)}`);
44
+ lines.push(`${bold("Form State:")} ${formatState$2(report.form_state, useColors)}`);
45
+ lines.push(`${bold("Complete:")} ${report.is_complete ? green("yes") : dim("no")}`);
46
+ lines.push("");
47
+ const counts = report.progress.counts;
48
+ lines.push(bold("Progress:"));
49
+ lines.push(` Total fields: ${counts.totalFields}`);
50
+ lines.push(` Valid: ${counts.validFields}, Invalid: ${counts.invalidFields}`);
51
+ lines.push(` Filled: ${counts.filledFields}, Empty: ${counts.emptyFields}`);
52
+ lines.push(` Empty required: ${counts.emptyRequiredFields}`);
53
+ lines.push("");
54
+ if (report.issues.length > 0) {
55
+ lines.push(bold(`Issues (${report.issues.length}):`));
56
+ for (const issue of report.issues) {
57
+ const priority = `P${issue.priority}`;
58
+ lines.push(` ${dim(priority)} ${dim(issue.ref)}: ${issue.message}`);
59
+ }
60
+ } else lines.push(dim("No issues."));
61
+ return lines.join("\n");
62
+ }
63
+ /**
64
+ * Register the apply command.
65
+ */
66
+ function registerApplyCommand(program) {
67
+ program.command("apply <file>").description("Apply patches to a form").option("--patch <json>", "JSON array of patches to apply").option("-o, --output <file>", "Output file (defaults to stdout)").option("--report", "Output apply result report instead of modified form").action(async (file, options, cmd) => {
68
+ const ctx = getCommandContext(cmd);
69
+ try {
70
+ if (!options.patch) {
71
+ logError("--patch option is required");
72
+ process.exit(1);
73
+ }
74
+ logVerbose(ctx, `Reading file: ${file}`);
75
+ const content = await readFile$1(file);
76
+ logVerbose(ctx, "Parsing form...");
77
+ const form = parseForm(content);
78
+ logVerbose(ctx, "Parsing patches...");
79
+ let parsedJson;
80
+ try {
81
+ parsedJson = JSON.parse(options.patch);
82
+ } catch {
83
+ logError("Invalid JSON in --patch option");
84
+ process.exit(1);
85
+ }
86
+ if (!Array.isArray(parsedJson)) {
87
+ logError("--patch must be a JSON array");
88
+ process.exit(1);
89
+ }
90
+ const patches = parsedJson;
91
+ const validatedPatches = [];
92
+ for (let i = 0; i < patches.length; i++) {
93
+ const result$1 = PatchSchema.safeParse(patches[i]);
94
+ if (!result$1.success) {
95
+ logError(`Invalid patch at index ${i}: ${result$1.error.issues[0]?.message ?? "Unknown error"}`);
96
+ process.exit(1);
97
+ }
98
+ validatedPatches.push(result$1.data);
99
+ }
100
+ if (ctx.dryRun) {
101
+ logDryRun(`Would apply ${validatedPatches.length} patches to ${file}`, { patches: validatedPatches });
102
+ return;
103
+ }
104
+ logVerbose(ctx, `Applying ${validatedPatches.length} patches...`);
105
+ const result = applyPatches(form, validatedPatches);
106
+ if (result.applyStatus === "rejected") {
107
+ logError("Patches rejected - structural validation failed");
108
+ const output = formatOutput(ctx, {
109
+ apply_status: result.applyStatus,
110
+ form_state: result.formState,
111
+ is_complete: result.isComplete,
112
+ structure: result.structureSummary,
113
+ progress: result.progressSummary,
114
+ issues: result.issues
115
+ }, (data, useColors) => formatConsoleReport$2(data, useColors));
116
+ console.error(output);
117
+ process.exit(1);
118
+ }
119
+ if (options.report) {
120
+ const output = formatOutput(ctx, {
121
+ apply_status: result.applyStatus,
122
+ form_state: result.formState,
123
+ is_complete: result.isComplete,
124
+ structure: result.structureSummary,
125
+ progress: result.progressSummary,
126
+ issues: result.issues
127
+ }, (data, useColors) => formatConsoleReport$2(data, useColors));
128
+ if (options.output) {
129
+ await writeFile(options.output, output);
130
+ logSuccess(ctx, `Report written to ${options.output}`);
131
+ } else console.log(output);
132
+ } else {
133
+ const output = serialize(form);
134
+ if (options.output) {
135
+ await writeFile(options.output, output);
136
+ logSuccess(ctx, `Modified form written to ${options.output}`);
137
+ } else console.log(output);
138
+ }
139
+ } catch (error) {
140
+ logError(error instanceof Error ? error.message : String(error));
141
+ process.exit(1);
142
+ }
143
+ });
144
+ }
145
+
146
+ //#endregion
147
+ //#region src/cli/commands/docs.ts
148
+ /**
149
+ * Get the path to the DOCS.md file.
150
+ * Works both during development and when installed as a package.
151
+ */
152
+ function getDocsPath() {
153
+ const thisDir = dirname(fileURLToPath(import.meta.url));
154
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "DOCS.md");
155
+ return join(dirname(dirname(dirname(thisDir))), "DOCS.md");
156
+ }
157
+ /**
158
+ * Load the docs content.
159
+ */
160
+ function loadDocs() {
161
+ const docsPath = getDocsPath();
162
+ try {
163
+ return readFileSync(docsPath, "utf-8");
164
+ } catch (error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ throw new Error(`Failed to load DOCS from ${docsPath}: ${message}`);
167
+ }
168
+ }
169
+ /**
170
+ * Apply basic terminal formatting to markdown content.
171
+ * Colorizes headers, code blocks, and other elements for better readability.
172
+ */
173
+ function formatMarkdown$3(content, useColors) {
174
+ if (!useColors) return content;
175
+ const lines = content.split("\n");
176
+ const formatted = [];
177
+ let inCodeBlock = false;
178
+ for (const line of lines) {
179
+ if (line.startsWith("```")) {
180
+ inCodeBlock = !inCodeBlock;
181
+ formatted.push(pc.dim(line));
182
+ continue;
183
+ }
184
+ if (inCodeBlock) {
185
+ formatted.push(pc.dim(line));
186
+ continue;
187
+ }
188
+ if (line.startsWith("# ")) {
189
+ formatted.push(pc.bold(pc.cyan(line)));
190
+ continue;
191
+ }
192
+ if (line.startsWith("## ")) {
193
+ formatted.push(pc.bold(pc.blue(line)));
194
+ continue;
195
+ }
196
+ if (line.startsWith("### ")) {
197
+ formatted.push(pc.bold(line));
198
+ continue;
199
+ }
200
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
201
+ return pc.yellow(code);
202
+ });
203
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
204
+ return pc.bold(text);
205
+ });
206
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
207
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
208
+ });
209
+ formatted.push(formattedLine);
210
+ }
211
+ return formatted.join("\n");
212
+ }
213
+ /**
214
+ * Check if stdout is an interactive terminal.
215
+ */
216
+ function isInteractive$3() {
217
+ return process.stdout.isTTY === true;
218
+ }
219
+ /**
220
+ * Display content.
221
+ */
222
+ function displayContent$2(content) {
223
+ console.log(content);
224
+ }
225
+ /**
226
+ * Register the docs command.
227
+ */
228
+ function registerDocsCommand(program) {
229
+ program.command("docs").description("Display concise Markform syntax reference (agent-friendly)").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
230
+ const ctx = getCommandContext(cmd);
231
+ try {
232
+ displayContent$2(formatMarkdown$3(loadDocs(), !options.raw && isInteractive$3() && !ctx.quiet));
233
+ } catch (error) {
234
+ logError(error instanceof Error ? error.message : String(error));
235
+ process.exit(1);
236
+ }
237
+ });
238
+ }
239
+
240
+ //#endregion
241
+ //#region src/cli/lib/exportHelpers.ts
242
+ /**
243
+ * Export helpers for multi-format form output.
244
+ *
245
+ * Provides reusable functions for exporting forms to multiple formats:
246
+ * - Markform format (.form.md) - canonical form with directives
247
+ * - Raw markdown (.raw.md) - plain readable markdown
248
+ * - YAML values (.yml) - extracted field values
249
+ */
250
+ /**
251
+ * Convert field responses to structured format for export (markform-218).
252
+ *
253
+ * Includes state for all fields:
254
+ * - { state: 'unanswered' } for unfilled fields
255
+ * - { state: 'skipped' } for skipped fields
256
+ * - { state: 'aborted' } for aborted fields
257
+ * - { state: 'answered', value: ... } for answered fields
258
+ */
259
+ function toStructuredValues(form) {
260
+ const result = {};
261
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
262
+ if (!response || response.state === "unanswered") {
263
+ result[fieldId] = { state: "unanswered" };
264
+ continue;
265
+ }
266
+ if (response.state === "skipped") {
267
+ result[fieldId] = {
268
+ state: "skipped",
269
+ ...response.reason && { reason: response.reason }
270
+ };
271
+ continue;
272
+ }
273
+ if (response.state === "aborted") {
274
+ result[fieldId] = {
275
+ state: "aborted",
276
+ ...response.reason && { reason: response.reason }
277
+ };
278
+ continue;
279
+ }
280
+ if (!response.value) {
281
+ result[fieldId] = {
282
+ state: "answered",
283
+ value: null
284
+ };
285
+ continue;
286
+ }
287
+ const value = response.value;
288
+ let exportValue;
289
+ switch (value.kind) {
290
+ case "string":
291
+ exportValue = value.value ?? null;
292
+ break;
293
+ case "number":
294
+ exportValue = value.value ?? null;
295
+ break;
296
+ case "string_list":
297
+ exportValue = value.items;
298
+ break;
299
+ case "single_select":
300
+ exportValue = value.selected ?? null;
301
+ break;
302
+ case "multi_select":
303
+ exportValue = value.selected;
304
+ break;
305
+ case "checkboxes":
306
+ exportValue = value.values;
307
+ break;
308
+ case "url":
309
+ exportValue = value.value ?? null;
310
+ break;
311
+ case "url_list":
312
+ exportValue = value.items;
313
+ break;
314
+ }
315
+ result[fieldId] = {
316
+ state: "answered",
317
+ value: exportValue
318
+ };
319
+ }
320
+ return result;
321
+ }
322
+ /**
323
+ * Convert notes to export format (markform-219).
324
+ */
325
+ function toNotesArray(form) {
326
+ return form.notes.map((note) => ({
327
+ id: note.id,
328
+ ref: note.ref,
329
+ role: note.role,
330
+ text: note.text
331
+ }));
332
+ }
333
+ /**
334
+ * Derive export paths from a base form path.
335
+ * Uses centralized extension constants from settings.ts.
336
+ *
337
+ * @param basePath - Path to the .form.md file
338
+ * @returns Object with paths for all export formats
339
+ */
340
+ function deriveExportPaths(basePath) {
341
+ return {
342
+ formPath: deriveExportPath(basePath, "form"),
343
+ rawPath: deriveExportPath(basePath, "raw"),
344
+ yamlPath: deriveExportPath(basePath, "yaml")
345
+ };
346
+ }
347
+ /**
348
+ * Export form to multiple formats.
349
+ *
350
+ * Writes:
351
+ * - Markform format (.form.md) - canonical form with directives
352
+ * - Raw markdown (.raw.md) - plain readable markdown (no directives)
353
+ * - YAML values (.yml) - structured format with state and notes (markform-218, markform-219)
354
+ *
355
+ * @param form - The parsed form to export
356
+ * @param basePath - Base path for the .form.md file (other paths are derived)
357
+ * @returns Paths to all exported files
358
+ */
359
+ async function exportMultiFormat(form, basePath) {
360
+ const paths = deriveExportPaths(basePath);
361
+ const formContent = serialize(form);
362
+ await writeFile(paths.formPath, formContent);
363
+ const rawContent = serializeRawMarkdown(form);
364
+ await writeFile(paths.rawPath, rawContent);
365
+ const values = toStructuredValues(form);
366
+ const notes = toNotesArray(form);
367
+ const exportData = {
368
+ values,
369
+ ...notes.length > 0 && { notes }
370
+ };
371
+ const yamlContent = YAML.stringify(exportData);
372
+ await writeFile(paths.yamlPath, yamlContent);
373
+ return paths;
374
+ }
375
+
376
+ //#endregion
377
+ //#region src/cli/commands/dump.ts
378
+ /**
379
+ * Format a field response for console display, including state information.
380
+ */
381
+ function formatFieldResponse(response, useColors) {
382
+ const dim = useColors ? pc.dim : (s) => s;
383
+ const green = useColors ? pc.green : (s) => s;
384
+ const yellow = useColors ? pc.yellow : (s) => s;
385
+ if (response.state === "unanswered") return dim("(unanswered)");
386
+ if (response.state === "skipped") return yellow(`[skipped]${response.reason ? ` ${response.reason}` : ""}`);
387
+ if (response.state === "aborted") return yellow(`[aborted]${response.reason ? ` ${response.reason}` : ""}`);
388
+ const value = response.value;
389
+ if (!value) return dim("(empty)");
390
+ switch (value.kind) {
391
+ case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
392
+ case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
393
+ case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
394
+ case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
395
+ case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
396
+ case "checkboxes": {
397
+ const entries = Object.entries(value.values);
398
+ if (entries.length === 0) return dim("(no entries)");
399
+ return entries.map(([k, v]) => `${k}:${v}`).join(", ");
400
+ }
401
+ case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
402
+ case "url_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
403
+ default: return dim("(unknown)");
404
+ }
405
+ }
406
+ /**
407
+ * Format form responses for console output, showing all fields with their states.
408
+ */
409
+ function formatConsoleResponses(form, useColors) {
410
+ const lines = [];
411
+ const bold = useColors ? pc.bold : (s) => s;
412
+ const dim = useColors ? pc.dim : (s) => s;
413
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
414
+ const valueStr = formatFieldResponse(response, useColors);
415
+ lines.push(`${bold(fieldId)}: ${valueStr}`);
416
+ }
417
+ if (lines.length === 0) lines.push(dim("(no fields)"));
418
+ return lines.join("\n");
419
+ }
420
+ /**
421
+ * Register the dump command.
422
+ */
423
+ function registerDumpCommand(program) {
424
+ program.command("dump <file>").description("Extract and display form values with state (lightweight inspect)").action(async (file, _options, cmd) => {
425
+ const ctx = getCommandContext(cmd);
426
+ try {
427
+ logVerbose(ctx, `Reading file: ${file}`);
428
+ const content = await readFile$1(file);
429
+ logVerbose(ctx, "Parsing form...");
430
+ const form = parseForm(content);
431
+ if (ctx.format === "json" || ctx.format === "yaml") {
432
+ const output = formatOutput(ctx, {
433
+ values: toStructuredValues(form),
434
+ ...form.notes.length > 0 && { notes: toNotesArray(form) }
435
+ }, () => "");
436
+ console.log(output);
437
+ } else {
438
+ const output = formatOutput(ctx, form, (data, useColors) => formatConsoleResponses(data, useColors));
439
+ console.log(output);
440
+ }
441
+ } catch (error) {
442
+ logError(error instanceof Error ? error.message : String(error));
443
+ process.exit(1);
444
+ }
445
+ });
446
+ }
447
+
448
+ //#endregion
449
+ //#region src/cli/lib/patchFormat.ts
450
+ /** Maximum characters for a patch value display before truncation */
451
+ const PATCH_VALUE_MAX_LENGTH = 1e3;
452
+ /**
453
+ * Truncate a string to max length with ellipsis if needed.
454
+ */
455
+ function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
456
+ if (value.length <= maxLength) return value;
457
+ return value.slice(0, maxLength) + "…";
458
+ }
459
+ /**
460
+ * Format a patch value for display with truncation.
461
+ */
462
+ function formatPatchValue(patch) {
463
+ switch (patch.op) {
464
+ case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
465
+ case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
466
+ case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
467
+ case "set_single_select": return patch.selected ?? "(none)";
468
+ case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
469
+ case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
470
+ case "clear_field": return "(cleared)";
471
+ case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
472
+ case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
473
+ case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
474
+ case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
475
+ case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
476
+ case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
477
+ case "add_note": return truncate(`note: ${patch.text}`);
478
+ case "remove_note": return `(remove note ${patch.noteId})`;
479
+ }
480
+ }
481
+ /**
482
+ * Get a short display name for the patch operation type.
483
+ */
484
+ function formatPatchType(patch) {
485
+ switch (patch.op) {
486
+ case "set_string": return "string";
487
+ case "set_number": return "number";
488
+ case "set_string_list": return "string_list";
489
+ case "set_single_select": return "select";
490
+ case "set_multi_select": return "multi_select";
491
+ case "set_checkboxes": return "checkboxes";
492
+ case "clear_field": return "clear";
493
+ case "skip_field": return "skip";
494
+ case "abort_field": return "abort";
495
+ case "set_url": return "url";
496
+ case "set_url_list": return "url_list";
497
+ case "set_date": return "date";
498
+ case "set_year": return "year";
499
+ case "add_note": return "note";
500
+ case "remove_note": return "remove_note";
501
+ }
502
+ }
503
+
504
+ //#endregion
505
+ //#region src/cli/lib/formatting.ts
506
+ /**
507
+ * Get a short status word from an issue reason.
508
+ */
509
+ function issueReasonToStatus(reason) {
510
+ switch (reason) {
511
+ case "required_missing": return "missing";
512
+ case "validation_error": return "invalid";
513
+ case "checkbox_incomplete": return "incomplete";
514
+ case "min_items_not_met": return "too-few";
515
+ case "optional_empty": return "empty";
516
+ default: return "issue";
517
+ }
518
+ }
519
+ /**
520
+ * Format a single issue as "fieldId (status)".
521
+ */
522
+ function formatIssueBrief(issue) {
523
+ const status = issueReasonToStatus(issue.reason);
524
+ return `${issue.ref} (${status})`;
525
+ }
526
+ /**
527
+ * Format issues for turn logging - shows count and brief field list.
528
+ * Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
529
+ */
530
+ function formatTurnIssues(issues, maxShow = 5) {
531
+ const count = issues.length;
532
+ if (count === 0) return "0 issues";
533
+ return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
534
+ }
535
+
536
+ //#endregion
537
+ //#region src/cli/examples/exampleRegistry.ts
538
+ /**
539
+ * Example form registry.
540
+ * Provides form content from the examples directory for the examples CLI command.
541
+ *
542
+ * Metadata (title, description) is loaded dynamically from the form's YAML frontmatter
543
+ * rather than being duplicated here, following the single source of truth principle.
544
+ */
545
+ /**
546
+ * Example definitions without content or metadata.
547
+ * Title and description are loaded dynamically from frontmatter.
548
+ */
549
+ const EXAMPLE_DEFINITIONS = [
550
+ {
551
+ id: "simple",
552
+ filename: "simple.form.md",
553
+ path: "simple/simple.form.md",
554
+ type: "fill"
555
+ },
556
+ {
557
+ id: "movie-research-minimal",
558
+ filename: "movie-research-minimal.form.md",
559
+ path: "movie-research/movie-research-minimal.form.md",
560
+ type: "research"
561
+ },
562
+ {
563
+ id: "movie-research-basic",
564
+ filename: "movie-research-basic.form.md",
565
+ path: "movie-research/movie-research-basic.form.md",
566
+ type: "research"
567
+ },
568
+ {
569
+ id: "movie-research-deep",
570
+ filename: "movie-research-deep.form.md",
571
+ path: "movie-research/movie-research-deep.form.md",
572
+ type: "research"
573
+ },
574
+ {
575
+ id: "earnings-analysis",
576
+ filename: "earnings-analysis.form.md",
577
+ path: "earnings-analysis/earnings-analysis.form.md",
578
+ type: "research"
579
+ },
580
+ {
581
+ id: "startup-deep-research",
582
+ filename: "startup-deep-research.form.md",
583
+ path: "startup-deep-research/startup-deep-research.form.md",
584
+ type: "research"
585
+ },
586
+ {
587
+ id: "celebrity-deep-research",
588
+ filename: "celebrity-deep-research.form.md",
589
+ path: "celebrity-deep-research/celebrity-deep-research.form.md",
590
+ type: "research"
591
+ }
592
+ ];
593
+ /**
594
+ * Get the path to the examples directory.
595
+ * Works both during development and when installed as a package.
596
+ */
597
+ function getExamplesDir() {
598
+ const thisDir = dirname(fileURLToPath(import.meta.url));
599
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "examples");
600
+ return join(dirname(dirname(dirname(thisDir))), "examples");
601
+ }
602
+ /**
603
+ * Load the content of an example form.
604
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
605
+ * @returns The form content as a string
606
+ * @throws Error if the example is not found
607
+ */
608
+ function loadExampleContent(exampleId) {
609
+ const example = EXAMPLE_DEFINITIONS.find((e) => e.id === exampleId);
610
+ if (!example) throw new Error(`Unknown example: ${exampleId}`);
611
+ const filePath = join(getExamplesDir(), example.path);
612
+ try {
613
+ return readFileSync(filePath, "utf-8");
614
+ } catch (error) {
615
+ throw new Error(`Failed to load example '${exampleId}' from ${filePath}: ${error}`);
616
+ }
617
+ }
618
+ /**
619
+ * Get an example definition by ID.
620
+ */
621
+ function getExampleById(id) {
622
+ return EXAMPLE_DEFINITIONS.find((e) => e.id === id);
623
+ }
624
+ /**
625
+ * Get the absolute path to an example's source file.
626
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
627
+ * @returns The absolute path to the example form file
628
+ * @throws Error if the example is not found
629
+ */
630
+ function getExamplePath(exampleId) {
631
+ const example = EXAMPLE_DEFINITIONS.find((e) => e.id === exampleId);
632
+ if (!example) throw new Error(`Unknown example: ${exampleId}`);
633
+ return join(getExamplesDir(), example.path);
634
+ }
635
+ /**
636
+ * Extract YAML frontmatter from a markdown file content.
637
+ * @param content - The markdown file content
638
+ * @returns The parsed frontmatter object or null if no frontmatter found
639
+ */
640
+ function extractFrontmatter(content) {
641
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
642
+ if (!frontmatterMatch || !frontmatterMatch[1]) return null;
643
+ try {
644
+ return YAML.parse(frontmatterMatch[1]);
645
+ } catch {
646
+ return null;
647
+ }
648
+ }
649
+ /**
650
+ * Load metadata (title, description) from an example's YAML frontmatter.
651
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
652
+ * @returns Object with title and description from frontmatter
653
+ */
654
+ function loadExampleMetadata(exampleId) {
655
+ const frontmatter = extractFrontmatter(loadExampleContent(exampleId));
656
+ if (!frontmatter || !frontmatter.markform) return {};
657
+ const markform = frontmatter.markform;
658
+ return {
659
+ title: typeof markform.title === "string" ? markform.title : void 0,
660
+ description: typeof markform.description === "string" ? markform.description : void 0
661
+ };
662
+ }
663
+ /**
664
+ * Get all example definitions with metadata loaded from frontmatter.
665
+ * @returns Array of ExampleDefinition with title and description populated
666
+ */
667
+ function getAllExamplesWithMetadata() {
668
+ return EXAMPLE_DEFINITIONS.map((example) => {
669
+ const metadata = loadExampleMetadata(example.id);
670
+ return {
671
+ ...example,
672
+ title: metadata.title,
673
+ description: metadata.description
674
+ };
675
+ });
676
+ }
677
+
678
+ //#endregion
679
+ //#region src/cli/lib/versioning.ts
680
+ /**
681
+ * Versioned filename utilities for form output.
682
+ *
683
+ * Generates versioned filenames to avoid overwriting existing files.
684
+ * Pattern: name.form.md → name-filled1.form.md → name-filled2.form.md
685
+ */
686
+ /**
687
+ * Version pattern that matches -filledN or _filledN before the extension.
688
+ * Also matches legacy -vN pattern for backwards compatibility.
689
+ */
690
+ const VERSION_PATTERN = /^(.+?)(?:[-_]?(?:filled|v)(\d+))?(\.form\.md)$/i;
691
+ /**
692
+ * Extension pattern for fallback matching.
693
+ */
694
+ const EXTENSION_PATTERN = /^(.+)(\.form\.md)$/i;
695
+ /**
696
+ * Parse a versioned filename into its components.
697
+ *
698
+ * @param filePath - Path to parse
699
+ * @returns Parsed components or null if not a valid form file
700
+ */
701
+ function parseVersionedPath(filePath) {
702
+ const match = VERSION_PATTERN.exec(filePath);
703
+ if (match) {
704
+ const base = match[1];
705
+ const versionStr = match[2];
706
+ const ext = match[3];
707
+ if (base !== void 0 && ext !== void 0) return {
708
+ base,
709
+ version: versionStr ? parseInt(versionStr, 10) : null,
710
+ extension: ext
711
+ };
712
+ }
713
+ const extMatch = EXTENSION_PATTERN.exec(filePath);
714
+ if (extMatch) {
715
+ const base = extMatch[1];
716
+ const ext = extMatch[2];
717
+ if (base !== void 0 && ext !== void 0) return {
718
+ base,
719
+ version: null,
720
+ extension: ext
721
+ };
722
+ }
723
+ return null;
724
+ }
725
+ /**
726
+ * Generate a versioned filename that doesn't conflict with existing files.
727
+ *
728
+ * Starts from the incremented version and keeps incrementing until
729
+ * a non-existent filename is found.
730
+ *
731
+ * @param filePath - Original file path
732
+ * @returns Path to a non-existent versioned file
733
+ */
734
+ function generateVersionedPath(filePath) {
735
+ const parsed = parseVersionedPath(filePath);
736
+ if (!parsed) {
737
+ let candidate$1 = `${filePath}-filled1`;
738
+ let version$1 = 1;
739
+ while (existsSync(candidate$1)) {
740
+ version$1++;
741
+ candidate$1 = `${filePath}-filled${version$1}`;
742
+ }
743
+ return candidate$1;
744
+ }
745
+ let version = parsed.version !== null ? parsed.version + 1 : 1;
746
+ let candidate = `${parsed.base}-filled${version}${parsed.extension}`;
747
+ while (existsSync(candidate)) {
748
+ version++;
749
+ candidate = `${parsed.base}-filled${version}${parsed.extension}`;
750
+ }
751
+ return candidate;
752
+ }
753
+ /**
754
+ * Generate a versioned filename within the forms directory.
755
+ *
756
+ * Derives the base name from the input path and creates a versioned
757
+ * output path within the specified forms directory.
758
+ *
759
+ * @param inputPath - Original input file path (used to derive basename)
760
+ * @param formsDir - Absolute path to the forms directory
761
+ * @returns Absolute path to a non-existent versioned file in formsDir
762
+ */
763
+ function generateVersionedPathInFormsDir(inputPath, formsDir) {
764
+ const inputFilename = basename(inputPath);
765
+ const parsed = parseVersionedPath(inputFilename);
766
+ const baseName = parsed?.base ?? inputFilename.replace(/\.form\.md$/i, "");
767
+ const extension = parsed?.extension ?? ".form.md";
768
+ let version = 1;
769
+ let candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
770
+ while (existsSync(candidate)) {
771
+ version++;
772
+ candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
773
+ }
774
+ return candidate;
775
+ }
776
+
777
+ //#endregion
778
+ //#region src/cli/lib/interactivePrompts.ts
779
+ /**
780
+ * Interactive prompts module for console-based form filling.
781
+ *
782
+ * Uses @clack/prompts to provide a rich terminal UI for users to
783
+ * fill form fields interactively.
784
+ */
785
+ /**
786
+ * Get field description from form docs.
787
+ */
788
+ function getFieldDescription(form, fieldId) {
789
+ return form.docs.find((d) => d.ref === fieldId && (d.tag === "description" || d.tag === "instructions"))?.bodyMarkdown;
790
+ }
791
+ /**
792
+ * Get a field by ID from the form schema.
793
+ */
794
+ function getFieldById(form, fieldId) {
795
+ for (const group of form.schema.groups) {
796
+ const field = group.children.find((f) => f.id === fieldId);
797
+ if (field) return field;
798
+ }
799
+ }
800
+ /**
801
+ * Format field label with required indicator and progress.
802
+ */
803
+ function formatFieldLabel(ctx) {
804
+ const required = ctx.field.required ? pc.red("*") : "";
805
+ const progress = `(${ctx.index} of ${ctx.total})`;
806
+ return `${ctx.field.label}${required} ${progress}`;
807
+ }
808
+ /**
809
+ * Create a skip_field patch for the given field.
810
+ */
811
+ function createSkipPatch(field) {
812
+ return {
813
+ op: "skip_field",
814
+ fieldId: field.id,
815
+ role: "user",
816
+ reason: "User skipped in console"
817
+ };
818
+ }
819
+ /**
820
+ * For optional fields, prompt user to choose between filling or skipping.
821
+ * Returns "fill" if user wants to enter a value, or a skip_field patch if skipping.
822
+ * Returns null if user cancelled.
823
+ */
824
+ async function promptSkipOrFill(ctx) {
825
+ const field = ctx.field;
826
+ if (field.required) return "fill";
827
+ const result = await p.select({
828
+ message: `${formatFieldLabel(ctx)} ${pc.dim("(optional)")}`,
829
+ options: [{
830
+ value: "fill",
831
+ label: "Enter value"
832
+ }, {
833
+ value: "skip",
834
+ label: "Skip this field"
835
+ }]
836
+ });
837
+ if (p.isCancel(result)) return null;
838
+ if (result === "skip") return createSkipPatch(field);
839
+ return "fill";
840
+ }
841
+ /**
842
+ * Prompt for a string field value.
843
+ */
844
+ async function promptForString(ctx) {
845
+ const field = ctx.field;
846
+ const currentVal = ctx.currentValue?.kind === "string" ? ctx.currentValue.value : null;
847
+ const result = await p.text({
848
+ message: formatFieldLabel(ctx),
849
+ placeholder: currentVal ?? (ctx.description ? ctx.description.slice(0, 60) : void 0),
850
+ initialValue: currentVal ?? "",
851
+ validate: (value) => {
852
+ if (field.required && !value.trim()) return "This field is required";
853
+ if (field.minLength && value.length < field.minLength) return `Minimum ${field.minLength} characters required`;
854
+ if (field.maxLength && value.length > field.maxLength) return `Maximum ${field.maxLength} characters allowed`;
855
+ if (field.pattern && !new RegExp(field.pattern).test(value)) return `Must match pattern: ${field.pattern}`;
856
+ }
857
+ });
858
+ if (p.isCancel(result)) return null;
859
+ if (!result && !field.required) return null;
860
+ return {
861
+ op: "set_string",
862
+ fieldId: field.id,
863
+ value: result || null
864
+ };
865
+ }
866
+ /**
867
+ * Prompt for a number field value.
868
+ */
869
+ async function promptForNumber(ctx) {
870
+ const field = ctx.field;
871
+ const currentVal = ctx.currentValue?.kind === "number" ? ctx.currentValue.value : null;
872
+ const result = await p.text({
873
+ message: formatFieldLabel(ctx),
874
+ placeholder: currentVal !== null ? String(currentVal) : void 0,
875
+ initialValue: currentVal !== null ? String(currentVal) : "",
876
+ validate: (value) => {
877
+ if (field.required && !value.trim()) return "This field is required";
878
+ if (!value.trim()) return;
879
+ const num = Number(value);
880
+ if (isNaN(num)) return "Please enter a valid number";
881
+ if (field.integer && !Number.isInteger(num)) return "Please enter a whole number";
882
+ if (field.min !== void 0 && num < field.min) return `Minimum value is ${field.min}`;
883
+ if (field.max !== void 0 && num > field.max) return `Maximum value is ${field.max}`;
884
+ }
885
+ });
886
+ if (p.isCancel(result)) return null;
887
+ if (!result && !field.required) return null;
888
+ return {
889
+ op: "set_number",
890
+ fieldId: field.id,
891
+ value: result ? Number(result) : null
892
+ };
893
+ }
894
+ /**
895
+ * Prompt for a string list field value.
896
+ */
897
+ async function promptForStringList(ctx) {
898
+ const field = ctx.field;
899
+ const currentItems = ctx.currentValue?.kind === "string_list" ? ctx.currentValue.items : [];
900
+ const hint = ctx.description ? `${ctx.description.slice(0, 50)}... (one item per line)` : "Enter items, one per line. Press Enter twice when done.";
901
+ const result = await p.text({
902
+ message: formatFieldLabel(ctx),
903
+ placeholder: hint,
904
+ initialValue: currentItems.join("\n"),
905
+ validate: (value) => {
906
+ const items$1 = value.split("\n").map((s) => s.trim()).filter(Boolean);
907
+ if (field.required && items$1.length === 0) return "At least one item is required";
908
+ if (field.minItems && items$1.length < field.minItems) return `Minimum ${field.minItems} items required`;
909
+ if (field.maxItems && items$1.length > field.maxItems) return `Maximum ${field.maxItems} items allowed`;
910
+ }
911
+ });
912
+ if (p.isCancel(result)) return null;
913
+ const items = result.split("\n").map((s) => s.trim()).filter(Boolean);
914
+ if (items.length === 0 && !field.required) return null;
915
+ return {
916
+ op: "set_string_list",
917
+ fieldId: field.id,
918
+ items
919
+ };
920
+ }
921
+ /**
922
+ * Prompt for a single-select field value.
923
+ */
924
+ async function promptForSingleSelect(ctx) {
925
+ const field = ctx.field;
926
+ const currentSelected = ctx.currentValue?.kind === "single_select" ? ctx.currentValue.selected : null;
927
+ const options = field.options.map((opt) => ({
928
+ value: opt.id,
929
+ label: opt.label
930
+ }));
931
+ const result = await p.select({
932
+ message: formatFieldLabel(ctx),
933
+ options,
934
+ initialValue: currentSelected ?? void 0
935
+ });
936
+ if (p.isCancel(result)) return null;
937
+ return {
938
+ op: "set_single_select",
939
+ fieldId: field.id,
940
+ selected: result
941
+ };
942
+ }
943
+ /**
944
+ * Prompt for a multi-select field value.
945
+ */
946
+ async function promptForMultiSelect(ctx) {
947
+ const field = ctx.field;
948
+ const currentSelected = ctx.currentValue?.kind === "multi_select" ? ctx.currentValue.selected : [];
949
+ const options = field.options.map((opt) => ({
950
+ value: opt.id,
951
+ label: opt.label
952
+ }));
953
+ const result = await p.multiselect({
954
+ message: formatFieldLabel(ctx),
955
+ options,
956
+ initialValues: currentSelected,
957
+ required: field.required
958
+ });
959
+ if (p.isCancel(result)) return null;
960
+ return {
961
+ op: "set_multi_select",
962
+ fieldId: field.id,
963
+ selected: result
964
+ };
965
+ }
966
+ /**
967
+ * Prompt for a checkboxes field value.
968
+ *
969
+ * Behavior varies by checkboxMode:
970
+ * - simple: multiselect to pick items marked as done
971
+ * - multi: per-option select with 5 states
972
+ * - explicit: per-option yes/no/skip
973
+ */
974
+ async function promptForCheckboxes(ctx) {
975
+ const field = ctx.field;
976
+ const currentValues = ctx.currentValue?.kind === "checkboxes" ? ctx.currentValue.values : {};
977
+ if (field.checkboxMode === "simple") {
978
+ const options = field.options.map((opt) => ({
979
+ value: opt.id,
980
+ label: opt.label
981
+ }));
982
+ const currentlyDone = field.options.filter((opt) => currentValues[opt.id] === "done").map((opt) => opt.id);
983
+ const result = await p.multiselect({
984
+ message: formatFieldLabel(ctx),
985
+ options,
986
+ initialValues: currentlyDone,
987
+ required: field.required && field.minDone !== void 0 && field.minDone > 0
988
+ });
989
+ if (p.isCancel(result)) return null;
990
+ const selected = result;
991
+ const values$1 = {};
992
+ for (const opt of field.options) values$1[opt.id] = selected.includes(opt.id) ? "done" : "todo";
993
+ return {
994
+ op: "set_checkboxes",
995
+ fieldId: field.id,
996
+ values: values$1
997
+ };
998
+ }
999
+ if (field.checkboxMode === "explicit") {
1000
+ const values$1 = {};
1001
+ for (const opt of field.options) {
1002
+ const current = currentValues[opt.id];
1003
+ const result = await p.select({
1004
+ message: `${opt.label}`,
1005
+ options: [
1006
+ {
1007
+ value: "yes",
1008
+ label: "Yes"
1009
+ },
1010
+ {
1011
+ value: "no",
1012
+ label: "No"
1013
+ },
1014
+ {
1015
+ value: "unfilled",
1016
+ label: "Skip"
1017
+ }
1018
+ ],
1019
+ initialValue: current === "yes" || current === "no" ? current : "unfilled"
1020
+ });
1021
+ if (p.isCancel(result)) return null;
1022
+ values$1[opt.id] = result;
1023
+ }
1024
+ return {
1025
+ op: "set_checkboxes",
1026
+ fieldId: field.id,
1027
+ values: values$1
1028
+ };
1029
+ }
1030
+ const values = {};
1031
+ for (const opt of field.options) {
1032
+ const current = currentValues[opt.id];
1033
+ const result = await p.select({
1034
+ message: `${opt.label}`,
1035
+ options: [
1036
+ {
1037
+ value: "todo",
1038
+ label: "To do"
1039
+ },
1040
+ {
1041
+ value: "active",
1042
+ label: "In progress"
1043
+ },
1044
+ {
1045
+ value: "done",
1046
+ label: "Done"
1047
+ },
1048
+ {
1049
+ value: "incomplete",
1050
+ label: "Incomplete"
1051
+ },
1052
+ {
1053
+ value: "na",
1054
+ label: "N/A"
1055
+ }
1056
+ ],
1057
+ initialValue: current ?? "todo"
1058
+ });
1059
+ if (p.isCancel(result)) return null;
1060
+ values[opt.id] = result;
1061
+ }
1062
+ return {
1063
+ op: "set_checkboxes",
1064
+ fieldId: field.id,
1065
+ values
1066
+ };
1067
+ }
1068
+ /**
1069
+ * Prompt for a URL field value.
1070
+ */
1071
+ async function promptForUrl(ctx) {
1072
+ const field = ctx.field;
1073
+ const currentVal = ctx.currentValue?.kind === "url" ? ctx.currentValue.value : null;
1074
+ const result = await p.text({
1075
+ message: formatFieldLabel(ctx),
1076
+ placeholder: currentVal ?? "https://example.com",
1077
+ initialValue: currentVal ?? "",
1078
+ validate: (value) => {
1079
+ if (field.required && !value.trim()) return "This field is required";
1080
+ if (!value.trim()) return;
1081
+ try {
1082
+ new URL(value);
1083
+ } catch {
1084
+ return "Please enter a valid URL (e.g., https://example.com)";
1085
+ }
1086
+ }
1087
+ });
1088
+ if (p.isCancel(result)) return null;
1089
+ if (!result && !field.required) return null;
1090
+ return {
1091
+ op: "set_url",
1092
+ fieldId: field.id,
1093
+ value: result || null
1094
+ };
1095
+ }
1096
+ /**
1097
+ * Prompt for a URL list field value.
1098
+ */
1099
+ async function promptForUrlList(ctx) {
1100
+ const field = ctx.field;
1101
+ const currentItems = ctx.currentValue?.kind === "url_list" ? ctx.currentValue.items : [];
1102
+ const hint = ctx.description ? `${ctx.description.slice(0, 50)}... (one URL per line)` : "Enter URLs, one per line. Press Enter twice when done.";
1103
+ const result = await p.text({
1104
+ message: formatFieldLabel(ctx),
1105
+ placeholder: hint,
1106
+ initialValue: currentItems.join("\n"),
1107
+ validate: (value) => {
1108
+ const items$1 = value.split("\n").map((s) => s.trim()).filter(Boolean);
1109
+ if (field.required && items$1.length === 0) return "At least one URL is required";
1110
+ if (field.minItems && items$1.length < field.minItems) return `Minimum ${field.minItems} URLs required`;
1111
+ if (field.maxItems && items$1.length > field.maxItems) return `Maximum ${field.maxItems} URLs allowed`;
1112
+ for (const item of items$1) try {
1113
+ new URL(item);
1114
+ } catch {
1115
+ return `Invalid URL: ${item}`;
1116
+ }
1117
+ }
1118
+ });
1119
+ if (p.isCancel(result)) return null;
1120
+ const items = result.split("\n").map((s) => s.trim()).filter(Boolean);
1121
+ if (items.length === 0 && !field.required) return null;
1122
+ return {
1123
+ op: "set_url_list",
1124
+ fieldId: field.id,
1125
+ items
1126
+ };
1127
+ }
1128
+ /**
1129
+ * Prompt user for a single field value based on field type.
1130
+ * Returns a Patch to set the value, or null if skipped/cancelled.
1131
+ *
1132
+ * For optional fields, first offers a choice to skip or fill.
1133
+ */
1134
+ async function promptForField(ctx) {
1135
+ if (ctx.description) p.note(ctx.description, pc.dim("Instructions"));
1136
+ const skipOrFillResult = await promptSkipOrFill(ctx);
1137
+ if (skipOrFillResult === null) return null;
1138
+ if (typeof skipOrFillResult !== "string") return skipOrFillResult;
1139
+ switch (ctx.field.kind) {
1140
+ case "string": return promptForString(ctx);
1141
+ case "number": return promptForNumber(ctx);
1142
+ case "string_list": return promptForStringList(ctx);
1143
+ case "single_select": return promptForSingleSelect(ctx);
1144
+ case "multi_select": return promptForMultiSelect(ctx);
1145
+ case "checkboxes": return promptForCheckboxes(ctx);
1146
+ case "url": return promptForUrl(ctx);
1147
+ case "url_list": return promptForUrlList(ctx);
1148
+ default: return null;
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Run an interactive fill session for a list of field issues.
1153
+ * Returns patches for all filled fields.
1154
+ *
1155
+ * @param form - The parsed form
1156
+ * @param issues - The issues indicating fields to fill
1157
+ * @returns Array of patches to apply
1158
+ */
1159
+ async function runInteractiveFill(form, issues) {
1160
+ const fieldIssues = issues.filter((i) => i.scope === "field");
1161
+ const seenFieldIds = /* @__PURE__ */ new Set();
1162
+ const uniqueFieldIssues = fieldIssues.filter((issue) => {
1163
+ if (seenFieldIds.has(issue.ref)) return false;
1164
+ seenFieldIds.add(issue.ref);
1165
+ return true;
1166
+ });
1167
+ if (uniqueFieldIssues.length === 0) {
1168
+ p.note("No fields to fill for the selected role.", "Info");
1169
+ return {
1170
+ patches: [],
1171
+ cancelled: false
1172
+ };
1173
+ }
1174
+ const patches = [];
1175
+ let index = 0;
1176
+ for (const issue of uniqueFieldIssues) {
1177
+ const field = getFieldById(form, issue.ref);
1178
+ if (!field) continue;
1179
+ index++;
1180
+ const response = form.responsesByFieldId[field.id];
1181
+ const patch = await promptForField({
1182
+ field,
1183
+ currentValue: response?.state === "answered" ? response.value : void 0,
1184
+ description: getFieldDescription(form, field.id),
1185
+ index,
1186
+ total: uniqueFieldIssues.length
1187
+ });
1188
+ if (patch === null && p.isCancel(patch)) {
1189
+ const shouldContinue = await p.confirm({
1190
+ message: "Cancel and discard changes?",
1191
+ initialValue: false
1192
+ });
1193
+ if (p.isCancel(shouldContinue) || shouldContinue) return {
1194
+ patches: [],
1195
+ cancelled: true
1196
+ };
1197
+ index--;
1198
+ continue;
1199
+ }
1200
+ if (patch) patches.push(patch);
1201
+ }
1202
+ return {
1203
+ patches,
1204
+ cancelled: false
1205
+ };
1206
+ }
1207
+ /**
1208
+ * Show intro message for interactive fill session.
1209
+ */
1210
+ function showInteractiveIntro(formTitle, role, fieldCount) {
1211
+ p.intro(pc.bgCyan(pc.black(" Markform Interactive Fill ")));
1212
+ const lines = [
1213
+ `${pc.bold("Form:")} ${formTitle}`,
1214
+ `${pc.bold("Role:")} ${role}`,
1215
+ `${pc.bold("Fields:")} ${fieldCount} to fill`
1216
+ ];
1217
+ p.note(lines.join("\n"), "Session Info");
1218
+ }
1219
+ /**
1220
+ * Show outro message after interactive fill.
1221
+ */
1222
+ function showInteractiveOutro(patchCount, cancelled) {
1223
+ if (cancelled) {
1224
+ p.cancel("Interactive fill cancelled.");
1225
+ return;
1226
+ }
1227
+ if (patchCount === 0) {
1228
+ p.outro(pc.yellow("No changes made."));
1229
+ return;
1230
+ }
1231
+ p.outro(`✓ ${patchCount} field(s) updated.`);
1232
+ }
1233
+
1234
+ //#endregion
1235
+ //#region src/cli/lib/fileViewer.ts
1236
+ /**
1237
+ * File viewer utility for displaying files with colorization and pagination.
1238
+ *
1239
+ * Provides a modern console experience:
1240
+ * - Syntax highlighting for markdown and YAML
1241
+ * - Pagination using system pager (less) when available
1242
+ * - Fallback to console output when not interactive
1243
+ */
1244
+ /**
1245
+ * Check if stdout is an interactive terminal.
1246
+ */
1247
+ function isInteractive$2() {
1248
+ return process.stdout.isTTY === true;
1249
+ }
1250
+ /**
1251
+ * Apply terminal formatting to markdown content.
1252
+ * Colorizes headers, code blocks, and other elements.
1253
+ */
1254
+ function formatMarkdown$2(content) {
1255
+ const lines = content.split("\n");
1256
+ const formatted = [];
1257
+ let inCodeBlock = false;
1258
+ for (const line of lines) {
1259
+ if (line.startsWith("```")) {
1260
+ inCodeBlock = !inCodeBlock;
1261
+ formatted.push(pc.dim(line));
1262
+ continue;
1263
+ }
1264
+ if (inCodeBlock) {
1265
+ formatted.push(pc.cyan(line));
1266
+ continue;
1267
+ }
1268
+ if (line.startsWith("# ")) {
1269
+ formatted.push(pc.bold(pc.magenta(line)));
1270
+ continue;
1271
+ }
1272
+ if (line.startsWith("## ")) {
1273
+ formatted.push(pc.bold(pc.blue(line)));
1274
+ continue;
1275
+ }
1276
+ if (line.startsWith("### ")) {
1277
+ formatted.push(pc.bold(pc.cyan(line)));
1278
+ continue;
1279
+ }
1280
+ if (line.startsWith("#### ")) {
1281
+ formatted.push(pc.bold(line));
1282
+ continue;
1283
+ }
1284
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
1285
+ return pc.yellow(code);
1286
+ });
1287
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
1288
+ return pc.bold(text);
1289
+ });
1290
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
1291
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
1292
+ });
1293
+ formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
1294
+ return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
1295
+ });
1296
+ formatted.push(formattedLine);
1297
+ }
1298
+ return formatted.join("\n");
1299
+ }
1300
+ /**
1301
+ * Apply terminal formatting to YAML content.
1302
+ */
1303
+ function formatYaml(content) {
1304
+ const lines = content.split("\n");
1305
+ const formatted = [];
1306
+ for (const line of lines) {
1307
+ if (line.trim().startsWith("#")) {
1308
+ formatted.push(pc.dim(line));
1309
+ continue;
1310
+ }
1311
+ const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
1312
+ if (match) {
1313
+ const [, indent, key, colon, value] = match;
1314
+ formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
1315
+ continue;
1316
+ }
1317
+ if (line.trim().startsWith("-")) {
1318
+ formatted.push(pc.green(line));
1319
+ continue;
1320
+ }
1321
+ formatted.push(line);
1322
+ }
1323
+ return formatted.join("\n");
1324
+ }
1325
+ /**
1326
+ * Format file content based on extension.
1327
+ */
1328
+ function formatContent(content, filename) {
1329
+ if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
1330
+ if (filename.endsWith(".md")) return formatMarkdown$2(content);
1331
+ return content;
1332
+ }
1333
+ /**
1334
+ * Display content using system pager (less) if available.
1335
+ * Falls back to console.log if not interactive or pager unavailable.
1336
+ *
1337
+ * @returns Promise that resolves when viewing is complete
1338
+ */
1339
+ async function displayWithPager(content, title) {
1340
+ if (!isInteractive$2()) {
1341
+ console.log(content);
1342
+ return;
1343
+ }
1344
+ const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
1345
+ return new Promise((resolve$1) => {
1346
+ const pager = spawn("less", [
1347
+ "-R",
1348
+ "-S",
1349
+ "-X",
1350
+ "-F",
1351
+ "-K"
1352
+ ], { stdio: [
1353
+ "pipe",
1354
+ "inherit",
1355
+ "inherit"
1356
+ ] });
1357
+ pager.on("error", () => {
1358
+ console.log(header);
1359
+ console.log("");
1360
+ console.log(content);
1361
+ console.log("");
1362
+ resolve$1();
1363
+ });
1364
+ pager.on("close", () => {
1365
+ resolve$1();
1366
+ });
1367
+ pager.stdin.write(header + "\n\n");
1368
+ pager.stdin.write(content);
1369
+ pager.stdin.end();
1370
+ });
1371
+ }
1372
+ /**
1373
+ * Load and display a file with formatting and pagination.
1374
+ */
1375
+ async function viewFile(filePath) {
1376
+ const content = readFileSync(filePath, "utf-8");
1377
+ const filename = basename(filePath);
1378
+ await displayWithPager(formatContent(content, filename), filename);
1379
+ }
1380
+ /**
1381
+ * Show an interactive file viewer chooser.
1382
+ *
1383
+ * Presents a list of files to view with descriptions, plus a Skip option.
1384
+ * Loops until the user selects Skip.
1385
+ *
1386
+ * @param files Array of file options to display
1387
+ */
1388
+ async function showFileViewerChooser(files) {
1389
+ if (!isInteractive$2()) return;
1390
+ console.log("");
1391
+ while (true) {
1392
+ const options = [...files.map((file) => ({
1393
+ value: file.path,
1394
+ label: pc.green(basename(file.path)),
1395
+ hint: file.hint ?? ""
1396
+ })), {
1397
+ value: "skip",
1398
+ label: "Done viewing",
1399
+ hint: "exit file viewer"
1400
+ }];
1401
+ const selection = await p.select({
1402
+ message: "View an output file?",
1403
+ options
1404
+ });
1405
+ if (p.isCancel(selection) || selection === "skip") break;
1406
+ await viewFile(selection);
1407
+ console.log("");
1408
+ }
1409
+ }
1410
+
1411
+ //#endregion
1412
+ //#region src/cli/commands/examples.ts
1413
+ /**
1414
+ * Print non-interactive list of examples.
1415
+ */
1416
+ function printExamplesList() {
1417
+ console.log(pc.bold("Available examples:\n"));
1418
+ const examples = getAllExamplesWithMetadata();
1419
+ for (const example of examples) {
1420
+ const typeLabel = example.type === "research" ? pc.magenta("[research]") : pc.blue("[fill]");
1421
+ console.log(` ${pc.cyan(example.id)} ${typeLabel}`);
1422
+ console.log(` ${pc.bold(example.title ?? example.id)}`);
1423
+ console.log(` ${example.description ?? "No description"}`);
1424
+ console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
1425
+ console.log("");
1426
+ }
1427
+ }
1428
+ /**
1429
+ * Display API availability status at startup.
1430
+ */
1431
+ function showApiStatus() {
1432
+ console.log("API Status:");
1433
+ for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
1434
+ const info = getProviderInfo(provider);
1435
+ const hasKey = !!process.env[info.envVar];
1436
+ const status = hasKey ? pc.green("✓") : "○";
1437
+ const envVar = hasKey ? info.envVar : pc.yellow(info.envVar);
1438
+ console.log(` ${status} ${provider} (${envVar})`);
1439
+ }
1440
+ console.log("");
1441
+ }
1442
+ /**
1443
+ * Build model options for the select prompt.
1444
+ */
1445
+ function buildModelOptions() {
1446
+ const options = [];
1447
+ for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1448
+ const info = getProviderInfo(provider);
1449
+ const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1450
+ for (const model of models) options.push({
1451
+ value: `${provider}/${model}`,
1452
+ label: `${provider}/${model}`,
1453
+ hint: `${keyStatus} ${info.envVar}`
1454
+ });
1455
+ }
1456
+ options.push({
1457
+ value: "custom",
1458
+ label: "Enter custom model ID...",
1459
+ hint: "provider/model-id format"
1460
+ });
1461
+ return options;
1462
+ }
1463
+ /**
1464
+ * Prompt user to select a model for agent fill.
1465
+ */
1466
+ async function promptForModel() {
1467
+ const modelOptions = buildModelOptions();
1468
+ const selection = await p.select({
1469
+ message: "Select LLM model:",
1470
+ options: modelOptions
1471
+ });
1472
+ if (p.isCancel(selection)) return null;
1473
+ if (selection === "custom") {
1474
+ const customModel = await p.text({
1475
+ message: "Model ID (provider/model-id):",
1476
+ placeholder: "anthropic/claude-sonnet-4-20250514",
1477
+ validate: (value) => {
1478
+ if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
1479
+ }
1480
+ });
1481
+ if (p.isCancel(customModel)) return null;
1482
+ return customModel;
1483
+ }
1484
+ return selection;
1485
+ }
1486
+ /**
1487
+ * Build model options filtered to providers with web search support.
1488
+ */
1489
+ function buildWebSearchModelOptions() {
1490
+ const options = [];
1491
+ for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1492
+ if (!hasWebSearchSupport(provider)) continue;
1493
+ const info = getProviderInfo(provider);
1494
+ const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1495
+ for (const model of models) options.push({
1496
+ value: `${provider}/${model}`,
1497
+ label: `${provider}/${model}`,
1498
+ hint: `${keyStatus} ${info.envVar}`
1499
+ });
1500
+ }
1501
+ options.push({
1502
+ value: "custom",
1503
+ label: "Enter custom model ID...",
1504
+ hint: "provider/model-id format"
1505
+ });
1506
+ return options;
1507
+ }
1508
+ /**
1509
+ * Prompt user to select a model with web search capability for research workflow.
1510
+ */
1511
+ async function promptForWebSearchModel() {
1512
+ const modelOptions = buildWebSearchModelOptions();
1513
+ if (modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
1514
+ const selection = await p.select({
1515
+ message: "Select LLM model (web search required):",
1516
+ options: modelOptions
1517
+ });
1518
+ if (p.isCancel(selection)) return null;
1519
+ if (selection === "custom") {
1520
+ const customModel = await p.text({
1521
+ message: "Model ID (provider/model-id):",
1522
+ placeholder: "openai/gpt-5-mini",
1523
+ validate: (value) => {
1524
+ if (!value.includes("/")) return "Format: provider/model-id (e.g., openai/gpt-5-mini)";
1525
+ }
1526
+ });
1527
+ if (p.isCancel(customModel)) return null;
1528
+ return customModel;
1529
+ }
1530
+ return selection;
1531
+ }
1532
+ /**
1533
+ * Run the agent fill workflow.
1534
+ * Accepts optional harness config overrides - research uses different defaults.
1535
+ */
1536
+ async function runAgentFill(form, modelId, _outputPath, configOverrides) {
1537
+ const spinner = p.spinner();
1538
+ try {
1539
+ spinner.start(`Resolving model: ${modelId}`);
1540
+ const { model, provider } = await resolveModel(modelId);
1541
+ spinner.stop(`Model resolved: ${modelId}`);
1542
+ const harnessConfig = {
1543
+ maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
1544
+ maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
1545
+ maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
1546
+ targetRoles: [AGENT_ROLE],
1547
+ fillMode: "continue"
1548
+ };
1549
+ console.log("");
1550
+ console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
1551
+ const harness = createHarness(form, harnessConfig);
1552
+ const agent = createLiveAgent({
1553
+ model,
1554
+ provider,
1555
+ targetRole: AGENT_ROLE
1556
+ });
1557
+ p.log.step(pc.bold("Agent fill in progress..."));
1558
+ let stepResult = harness.step();
1559
+ while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1560
+ console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1561
+ const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1562
+ for (const patch of patches) {
1563
+ const typeName = formatPatchType(patch);
1564
+ const value = formatPatchValue(patch);
1565
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
1566
+ if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
1567
+ else console.log(` (${typeName}) = ${pc.green(value)}`);
1568
+ }
1569
+ stepResult = harness.apply(patches, stepResult.issues);
1570
+ const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
1571
+ console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
1572
+ if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1573
+ }
1574
+ if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
1575
+ else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
1576
+ Object.assign(form, harness.getForm());
1577
+ return {
1578
+ success: stepResult.isComplete,
1579
+ turnCount: harness.getTurnNumber()
1580
+ };
1581
+ } catch (error) {
1582
+ spinner.stop(pc.red("Agent fill failed"));
1583
+ throw error;
1584
+ }
1585
+ }
1586
+ /**
1587
+ * Run the interactive example scaffolding and filling flow.
1588
+ *
1589
+ * @param preselectedId Optional example ID to pre-select
1590
+ * @param formsDirOverride Optional forms directory override from CLI option
1591
+ */
1592
+ async function runInteractiveFlow(preselectedId, formsDirOverride) {
1593
+ const startTime = Date.now();
1594
+ p.intro(pc.bgCyan(pc.black(" markform examples ")));
1595
+ const formsDir = getFormsDir(formsDirOverride);
1596
+ await ensureFormsDir(formsDir);
1597
+ showApiStatus();
1598
+ let selectedId = preselectedId;
1599
+ if (!selectedId) {
1600
+ const examples = getAllExamplesWithMetadata();
1601
+ const selection = await p.select({
1602
+ message: "Select an example form to scaffold:",
1603
+ options: examples.map((example$1) => ({
1604
+ value: example$1.id,
1605
+ label: example$1.title ?? example$1.id,
1606
+ hint: example$1.description
1607
+ }))
1608
+ });
1609
+ if (p.isCancel(selection)) {
1610
+ p.cancel("Cancelled.");
1611
+ process.exit(0);
1612
+ }
1613
+ selectedId = selection;
1614
+ }
1615
+ const example = getExampleById(selectedId);
1616
+ if (!example) {
1617
+ p.cancel(`Unknown example: ${selectedId}`);
1618
+ process.exit(1);
1619
+ }
1620
+ const defaultFilename = basename(generateVersionedPathInFormsDir(example.filename, formsDir));
1621
+ const filenameResult = await p.text({
1622
+ message: `Output filename (in ${formatPath(formsDir)}):`,
1623
+ initialValue: defaultFilename,
1624
+ validate: (value) => {
1625
+ if (!value.trim()) return "Filename is required";
1626
+ if (!value.endsWith(".form.md") && !value.endsWith(".md")) return "Filename should end with .form.md or .md";
1627
+ }
1628
+ });
1629
+ if (p.isCancel(filenameResult)) {
1630
+ p.cancel("Cancelled.");
1631
+ process.exit(0);
1632
+ }
1633
+ const filename = filenameResult;
1634
+ const outputPath = join(formsDir, filename);
1635
+ if (existsSync(outputPath)) {
1636
+ const overwrite = await p.confirm({
1637
+ message: `${filename} already exists. Overwrite?`,
1638
+ initialValue: false
1639
+ });
1640
+ if (p.isCancel(overwrite) || !overwrite) {
1641
+ p.cancel("Cancelled.");
1642
+ process.exit(0);
1643
+ }
1644
+ }
1645
+ let content;
1646
+ try {
1647
+ content = loadExampleContent(selectedId);
1648
+ await writeFile(outputPath, content);
1649
+ } catch (error) {
1650
+ const message = error instanceof Error ? error.message : String(error);
1651
+ p.cancel(`Failed to write file: ${message}`);
1652
+ process.exit(1);
1653
+ }
1654
+ p.log.success(`Created ${formatPath(outputPath)}`);
1655
+ const form = parseForm(content);
1656
+ const targetRoles = [USER_ROLE];
1657
+ let userFillOutputs = null;
1658
+ const inspectResult = inspect(form, { targetRoles });
1659
+ const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
1660
+ const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
1661
+ if (uniqueFieldIds.size === 0) {
1662
+ p.log.info("No user-role fields to fill in this example.");
1663
+ if (inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field").length === 0) {
1664
+ logTiming({
1665
+ verbose: false,
1666
+ format: "console",
1667
+ dryRun: false,
1668
+ quiet: false
1669
+ }, "Total time", Date.now() - startTime);
1670
+ p.outro("Form scaffolded with no fields to fill.");
1671
+ return;
1672
+ }
1673
+ } else {
1674
+ showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
1675
+ const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
1676
+ if (cancelled) {
1677
+ showInteractiveOutro(0, true);
1678
+ process.exit(1);
1679
+ }
1680
+ if (patches.length > 0) applyPatches(form, patches);
1681
+ userFillOutputs = await exportMultiFormat(form, outputPath);
1682
+ showInteractiveOutro(patches.length, false);
1683
+ console.log("");
1684
+ p.log.success("Outputs:");
1685
+ console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(markform)")}`);
1686
+ console.log(` ${formatPath(userFillOutputs.rawPath)} ${pc.dim("(plain markdown)")}`);
1687
+ console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(values as YAML)")}`);
1688
+ logTiming({
1689
+ verbose: false,
1690
+ format: "console",
1691
+ dryRun: false,
1692
+ quiet: false
1693
+ }, "Fill time", Date.now() - startTime);
1694
+ }
1695
+ const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
1696
+ const isResearchExample = example.type === "research";
1697
+ if (agentFieldIssues.length > 0) {
1698
+ console.log("");
1699
+ const workflowLabel = isResearchExample ? "research" : "agent fill";
1700
+ p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
1701
+ const confirmMessage = isResearchExample ? "Run research now? (requires web search)" : "Run agent fill now?";
1702
+ const runAgent = await p.confirm({
1703
+ message: confirmMessage,
1704
+ initialValue: true
1705
+ });
1706
+ if (p.isCancel(runAgent) || !runAgent) {
1707
+ console.log("");
1708
+ const cliCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=<provider/model>` : ` markform fill ${formatPath(outputPath)} --model=<provider/model>`;
1709
+ console.log(`You can run ${workflowLabel} later with:`);
1710
+ console.log(cliCommand);
1711
+ if (userFillOutputs) await showFileViewerChooser([
1712
+ {
1713
+ path: userFillOutputs.formPath,
1714
+ label: "Markform",
1715
+ hint: "form with tags"
1716
+ },
1717
+ {
1718
+ path: userFillOutputs.rawPath,
1719
+ label: "Plain Markdown",
1720
+ hint: "rendered output"
1721
+ },
1722
+ {
1723
+ path: userFillOutputs.yamlPath,
1724
+ label: "YAML",
1725
+ hint: "extracted values"
1726
+ }
1727
+ ]);
1728
+ p.outro("Happy form filling!");
1729
+ return;
1730
+ }
1731
+ const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
1732
+ if (!modelId) {
1733
+ p.cancel("Cancelled.");
1734
+ process.exit(0);
1735
+ }
1736
+ const agentDefaultFilename = basename(generateVersionedPathInFormsDir(outputPath, formsDir));
1737
+ const agentFilenameResult = await p.text({
1738
+ message: `Agent output filename (in ${formatPath(formsDir)}):`,
1739
+ initialValue: agentDefaultFilename,
1740
+ validate: (value) => {
1741
+ if (!value.trim()) return "Filename is required";
1742
+ }
1743
+ });
1744
+ if (p.isCancel(agentFilenameResult)) {
1745
+ p.cancel("Cancelled.");
1746
+ process.exit(0);
1747
+ }
1748
+ const agentOutputPath = join(formsDir, agentFilenameResult);
1749
+ const agentStartTime = Date.now();
1750
+ const timingLabel = isResearchExample ? "Research time" : "Agent fill time";
1751
+ const configOverrides = isResearchExample ? {
1752
+ maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
1753
+ maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
1754
+ } : void 0;
1755
+ try {
1756
+ const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
1757
+ logTiming({
1758
+ verbose: false,
1759
+ format: "console",
1760
+ dryRun: false,
1761
+ quiet: false
1762
+ }, timingLabel, Date.now() - agentStartTime);
1763
+ const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, agentOutputPath);
1764
+ console.log("");
1765
+ const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
1766
+ p.log.success(successMessage);
1767
+ console.log(` ${formatPath(formPath)} (markform)`);
1768
+ console.log(` ${formatPath(rawPath)} (plain markdown)`);
1769
+ console.log(` ${formatPath(yamlPath)} (values as YAML)`);
1770
+ if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
1771
+ await showFileViewerChooser([
1772
+ {
1773
+ path: formPath,
1774
+ label: "Markform",
1775
+ hint: "form with tags"
1776
+ },
1777
+ {
1778
+ path: rawPath,
1779
+ label: "Plain Markdown",
1780
+ hint: "rendered output"
1781
+ },
1782
+ {
1783
+ path: yamlPath,
1784
+ label: "YAML",
1785
+ hint: "extracted values"
1786
+ }
1787
+ ]);
1788
+ } catch (error) {
1789
+ const message = error instanceof Error ? error.message : String(error);
1790
+ const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
1791
+ p.log.error(`${failMessage}: ${message}`);
1792
+ console.log("");
1793
+ console.log("You can try again with:");
1794
+ const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
1795
+ console.log(retryCommand);
1796
+ }
1797
+ }
1798
+ p.outro("Happy form filling!");
1799
+ }
1800
+ /**
1801
+ * Register the examples command.
1802
+ */
1803
+ function registerExamplesCommand(program) {
1804
+ 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) => {
1805
+ const ctx = getCommandContext(cmd);
1806
+ try {
1807
+ if (options.list) {
1808
+ printExamplesList();
1809
+ return;
1810
+ }
1811
+ if (options.name) {
1812
+ if (!getExampleById(options.name)) {
1813
+ logError(`Unknown example: ${options.name}`);
1814
+ console.log("\nAvailable examples:");
1815
+ for (const ex of EXAMPLE_DEFINITIONS) console.log(` ${ex.id}`);
1816
+ process.exit(1);
1817
+ }
1818
+ }
1819
+ await runInteractiveFlow(options.name, ctx.formsDir);
1820
+ } catch (error) {
1821
+ logError(error instanceof Error ? error.message : String(error));
1822
+ process.exit(1);
1823
+ }
1824
+ });
1825
+ }
1826
+
1827
+ //#endregion
1828
+ //#region src/cli/commands/export.ts
1829
+ /**
1830
+ * Register the export command.
1831
+ */
1832
+ function registerExportCommand(program) {
1833
+ program.command("export <file>").description("Export form as markform (default), markdown (readable), or json/yaml for structured data").option("--compact", "Output compact JSON (no formatting, only for JSON format)").action(async (file, options, cmd) => {
1834
+ const ctx = getCommandContext(cmd);
1835
+ let format = "markform";
1836
+ if (ctx.format === "json") format = "json";
1837
+ else if (ctx.format === "yaml") format = "yaml";
1838
+ else if (ctx.format === "markdown") format = "markdown";
1839
+ else if (ctx.format === "markform") format = "markform";
1840
+ try {
1841
+ logVerbose(ctx, `Reading file: ${file}`);
1842
+ const content = await readFile$1(file);
1843
+ logVerbose(ctx, "Parsing form...");
1844
+ const form = parseForm(content);
1845
+ if (format === "markform") {
1846
+ console.log(serialize(form));
1847
+ return;
1848
+ }
1849
+ if (format === "markdown") {
1850
+ console.log(serializeRawMarkdown(form));
1851
+ return;
1852
+ }
1853
+ const schema = {
1854
+ id: form.schema.id,
1855
+ title: form.schema.title,
1856
+ groups: form.schema.groups.map((group) => ({
1857
+ id: group.id,
1858
+ title: group.title,
1859
+ children: group.children.map((field) => ({
1860
+ id: field.id,
1861
+ kind: field.kind,
1862
+ label: field.label,
1863
+ required: field.required,
1864
+ ...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
1865
+ id: opt.id,
1866
+ label: opt.label
1867
+ })) } : {}
1868
+ }))
1869
+ }))
1870
+ };
1871
+ const values = {};
1872
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
1873
+ const output = {
1874
+ schema,
1875
+ values,
1876
+ notes: form.notes,
1877
+ markdown: serialize(form)
1878
+ };
1879
+ if (format === "json") if (options.compact) console.log(JSON.stringify(output));
1880
+ else console.log(JSON.stringify(output, null, 2));
1881
+ else console.log(YAML.stringify(output));
1882
+ } catch (error) {
1883
+ logError(error instanceof Error ? error.message : String(error));
1884
+ process.exit(1);
1885
+ }
1886
+ });
1887
+ }
1888
+
1889
+ //#endregion
1890
+ //#region src/cli/commands/fill.ts
1891
+ /**
1892
+ * Format session transcript for console output.
1893
+ */
1894
+ function formatConsoleSession(transcript, useColors) {
1895
+ const lines = [];
1896
+ const bold = useColors ? pc.bold : (s) => s;
1897
+ const dim = useColors ? pc.dim : (s) => s;
1898
+ const cyan = useColors ? pc.cyan : (s) => s;
1899
+ const green = useColors ? pc.green : (s) => s;
1900
+ const yellow = useColors ? pc.yellow : (s) => s;
1901
+ lines.push(bold(cyan("Session Transcript")));
1902
+ lines.push("");
1903
+ lines.push(`${bold("Form:")} ${transcript.form.path}`);
1904
+ lines.push(`${bold("Mode:")} ${transcript.mode}`);
1905
+ lines.push(`${bold("Version:")} ${transcript.sessionVersion}`);
1906
+ lines.push("");
1907
+ lines.push(bold("Harness Config:"));
1908
+ lines.push(` Max turns: ${transcript.harness.maxTurns}`);
1909
+ lines.push(` Max patches/turn: ${transcript.harness.maxPatchesPerTurn}`);
1910
+ lines.push(` Max issues/turn: ${transcript.harness.maxIssuesPerTurn}`);
1911
+ lines.push("");
1912
+ lines.push(bold(`Turns (${transcript.turns.length}):`));
1913
+ for (const turn of transcript.turns) {
1914
+ const issueCount = turn.inspect.issues.length;
1915
+ const patchCount = turn.apply.patches.length;
1916
+ const afterIssues = turn.after.requiredIssueCount;
1917
+ lines.push(` Turn ${turn.turn}: ${dim(`${issueCount} issues`)} → ${yellow(`${patchCount} patches`)} → ${afterIssues === 0 ? green("0 remaining") : dim(`${afterIssues} remaining`)}`);
1918
+ }
1919
+ lines.push("");
1920
+ const expectText = transcript.final.expectComplete ? green("✓ complete") : yellow("○ incomplete");
1921
+ lines.push(`${bold("Expected:")} ${expectText}`);
1922
+ lines.push(`${bold("Completed form:")} ${transcript.final.expectedCompletedForm}`);
1923
+ return lines.join("\n");
1924
+ }
1925
+ /**
1926
+ * Register the fill command.
1927
+ */
1928
+ function registerFillCommand(program) {
1929
+ program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-5-mini)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES_PER_TURN})`, String(DEFAULT_MAX_ISSUES_PER_TURN)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").action(async (file, options, cmd) => {
1930
+ const ctx = getCommandContext(cmd);
1931
+ const filePath = resolve(file);
1932
+ try {
1933
+ const startTime = Date.now();
1934
+ let targetRoles;
1935
+ if (options.roles) try {
1936
+ targetRoles = parseRolesFlag(options.roles);
1937
+ } catch (error) {
1938
+ logError(`Invalid --roles: ${error instanceof Error ? error.message : String(error)}`);
1939
+ process.exit(1);
1940
+ }
1941
+ else targetRoles = options.interactive ? [USER_ROLE] : [AGENT_ROLE];
1942
+ let fillMode = "continue";
1943
+ if (options.mode) {
1944
+ if (options.mode !== "continue" && options.mode !== "overwrite") {
1945
+ logError(`Invalid --mode: ${options.mode}. Valid modes: continue, overwrite`);
1946
+ process.exit(1);
1947
+ }
1948
+ fillMode = options.mode;
1949
+ }
1950
+ logVerbose(ctx, `Reading form: ${filePath}`);
1951
+ const formContent = await readFile$1(filePath);
1952
+ logVerbose(ctx, "Parsing form...");
1953
+ const form = parseForm(formContent);
1954
+ if (options.interactive) {
1955
+ if (options.mock) {
1956
+ logError("--interactive cannot be used with --mock");
1957
+ process.exit(1);
1958
+ }
1959
+ if (options.model) {
1960
+ logError("--interactive cannot be used with --model");
1961
+ process.exit(1);
1962
+ }
1963
+ if (options.mockSource) {
1964
+ logError("--interactive cannot be used with --mock-source");
1965
+ process.exit(1);
1966
+ }
1967
+ const inspectResult = inspect(form, { targetRoles });
1968
+ const formTitle = form.schema.title ?? form.schema.id;
1969
+ const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
1970
+ const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
1971
+ showInteractiveIntro(formTitle, targetRoles.join(", "), uniqueFieldIds.size);
1972
+ const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
1973
+ if (cancelled) {
1974
+ showInteractiveOutro(0, true);
1975
+ process.exit(1);
1976
+ }
1977
+ if (patches.length > 0) applyPatches(form, patches);
1978
+ const durationMs$1 = Date.now() - startTime;
1979
+ let outputPath$1;
1980
+ if (options.output) outputPath$1 = resolve(options.output);
1981
+ else {
1982
+ const formsDir = getFormsDir(ctx.formsDir);
1983
+ await ensureFormsDir(formsDir);
1984
+ outputPath$1 = generateVersionedPathInFormsDir(filePath, formsDir);
1985
+ }
1986
+ if (ctx.dryRun) {
1987
+ logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
1988
+ showInteractiveOutro(patches.length, false);
1989
+ } else {
1990
+ const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, outputPath$1);
1991
+ showInteractiveOutro(patches.length, false);
1992
+ console.log("");
1993
+ p.log.success("Outputs:");
1994
+ console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
1995
+ console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
1996
+ console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
1997
+ }
1998
+ logTiming(ctx, "Fill time", durationMs$1);
1999
+ if (patches.length > 0) {
2000
+ console.log("");
2001
+ console.log("Next step: fill remaining fields with agent");
2002
+ console.log(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`);
2003
+ }
2004
+ process.exit(0);
2005
+ }
2006
+ if (options.mock && !options.mockSource) {
2007
+ logError("--mock requires --mock-source <file>");
2008
+ process.exit(1);
2009
+ }
2010
+ if (!options.mock && !options.model) {
2011
+ logError("Live agent requires --model <provider/model-id>");
2012
+ console.log("");
2013
+ console.log(formatSuggestedLlms());
2014
+ process.exit(1);
2015
+ }
2016
+ if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
2017
+ const harnessConfig = resolveHarnessConfig(form, {
2018
+ maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
2019
+ maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
2020
+ maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
2021
+ maxFieldsPerTurn: options.maxFields ? parseInt(options.maxFields, 10) : void 0,
2022
+ maxGroupsPerTurn: options.maxGroups ? parseInt(options.maxGroups, 10) : void 0,
2023
+ targetRoles,
2024
+ fillMode
2025
+ });
2026
+ const harness = createHarness(form, harnessConfig);
2027
+ let agent;
2028
+ let mockPath;
2029
+ if (options.mock) {
2030
+ mockPath = resolve(options.mockSource);
2031
+ logVerbose(ctx, `Reading mock source: ${mockPath}`);
2032
+ agent = createMockAgent(parseForm(await readFile$1(mockPath)));
2033
+ } else {
2034
+ const modelId = options.model;
2035
+ logVerbose(ctx, `Resolving model: ${modelId}`);
2036
+ const { model, provider } = await resolveModel(modelId);
2037
+ let systemPrompt;
2038
+ if (options.instructions) {
2039
+ systemPrompt = options.instructions;
2040
+ logVerbose(ctx, "Using inline system prompt from --instructions");
2041
+ } else if (options.prompt) {
2042
+ const promptPath = resolve(options.prompt);
2043
+ logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
2044
+ systemPrompt = await readFile$1(promptPath);
2045
+ }
2046
+ const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
2047
+ const liveAgent = createLiveAgent({
2048
+ model,
2049
+ provider,
2050
+ systemPromptAddition: systemPrompt,
2051
+ targetRole: primaryRole
2052
+ });
2053
+ agent = liveAgent;
2054
+ logInfo(ctx, `Available tools: ${liveAgent.getAvailableToolNames().join(", ")}`);
2055
+ logVerbose(ctx, `Using live agent with model: ${modelId}`);
2056
+ }
2057
+ logInfo(ctx, pc.cyan(`Filling form: ${filePath}`));
2058
+ logInfo(ctx, `Agent: ${options.mock ? "mock" : "live"}${options.model ? ` (${options.model})` : ""}`);
2059
+ logVerbose(ctx, `Max turns: ${harnessConfig.maxTurns}`);
2060
+ logVerbose(ctx, `Max patches per turn: ${harnessConfig.maxPatchesPerTurn}`);
2061
+ logVerbose(ctx, `Max issues per turn: ${harnessConfig.maxIssuesPerTurn}`);
2062
+ logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
2063
+ logVerbose(ctx, `Fill mode: ${fillMode}`);
2064
+ let stepResult = harness.step();
2065
+ logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
2066
+ while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
2067
+ const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
2068
+ const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
2069
+ logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
2070
+ for (const patch of patches) {
2071
+ const typeName = formatPatchType(patch);
2072
+ const value = formatPatchValue(patch);
2073
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
2074
+ if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2075
+ else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2076
+ }
2077
+ if (stats) {
2078
+ logVerbose(ctx, ` Stats: tokens ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0}`);
2079
+ if (stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
2080
+ if (stats.prompts) {
2081
+ logVerbose(ctx, ``);
2082
+ logVerbose(ctx, pc.dim(` ─── System Prompt ───`));
2083
+ for (const line of stats.prompts.system.split("\n")) logVerbose(ctx, pc.dim(` ${line}`));
2084
+ logVerbose(ctx, ``);
2085
+ logVerbose(ctx, pc.dim(` ─── Context Prompt ───`));
2086
+ for (const line of stats.prompts.context.split("\n")) logVerbose(ctx, pc.dim(` ${line}`));
2087
+ logVerbose(ctx, ``);
2088
+ }
2089
+ }
2090
+ let llmStats;
2091
+ if (stats) llmStats = {
2092
+ inputTokens: stats.inputTokens,
2093
+ outputTokens: stats.outputTokens,
2094
+ toolCalls: stats.toolCalls.length > 0 ? stats.toolCalls : void 0
2095
+ };
2096
+ stepResult = harness.apply(patches, stepResult.issues, llmStats);
2097
+ if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
2098
+ else if (!harness.hasReachedMaxTurns()) {
2099
+ stepResult = harness.step();
2100
+ logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
2101
+ }
2102
+ }
2103
+ const durationMs = Date.now() - startTime;
2104
+ if (stepResult.isComplete) logSuccess(ctx, `Form completed in ${harness.getTurnNumber()} turn(s)`);
2105
+ else if (harness.hasReachedMaxTurns()) logWarn(ctx, `Max turns reached (${harnessConfig.maxTurns})`);
2106
+ logTiming(ctx, "Fill time", durationMs);
2107
+ let outputPath;
2108
+ if (options.output) outputPath = resolve(options.output);
2109
+ else {
2110
+ const formsDir = getFormsDir(ctx.formsDir);
2111
+ await ensureFormsDir(formsDir);
2112
+ outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
2113
+ }
2114
+ const formMarkdown = serialize(harness.getForm());
2115
+ if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
2116
+ else {
2117
+ await writeFile(outputPath, formMarkdown);
2118
+ logSuccess(ctx, `Form written to: ${outputPath}`);
2119
+ }
2120
+ const transcript = buildSessionTranscript(filePath, options.mock ? "mock" : "live", mockPath, options.model, harnessConfig, harness.getTurns(), stepResult.isComplete, outputPath);
2121
+ if (options.record) {
2122
+ const recordPath = resolve(options.record);
2123
+ const yaml = serializeSession(transcript);
2124
+ if (ctx.dryRun) {
2125
+ logInfo(ctx, `[DRY RUN] Would write session to: ${recordPath}`);
2126
+ console.log(yaml);
2127
+ } else {
2128
+ await writeFile(recordPath, yaml);
2129
+ logSuccess(ctx, `Session recorded to: ${recordPath}`);
2130
+ }
2131
+ } else {
2132
+ const output = formatOutput(ctx, transcript, (data, useColors) => formatConsoleSession(data, useColors));
2133
+ console.log(output);
2134
+ }
2135
+ process.exit(stepResult.isComplete ? 0 : 1);
2136
+ } catch (error) {
2137
+ logError(error instanceof Error ? error.message : String(error));
2138
+ process.exit(1);
2139
+ }
2140
+ });
2141
+ }
2142
+ /**
2143
+ * Build a session transcript from harness execution.
2144
+ */
2145
+ function buildSessionTranscript(formPath, mockMode, mockPath, modelId, harnessConfig, turns, expectComplete, outputPath) {
2146
+ const transcript = {
2147
+ sessionVersion: "0.1.0",
2148
+ mode: mockMode,
2149
+ form: { path: formPath },
2150
+ harness: harnessConfig,
2151
+ turns,
2152
+ final: {
2153
+ expectComplete,
2154
+ expectedCompletedForm: mockMode === "mock" ? mockPath ?? outputPath : outputPath
2155
+ }
2156
+ };
2157
+ if (mockMode === "mock" && mockPath) transcript.mock = { completedMock: mockPath };
2158
+ else if (mockMode === "live" && modelId) transcript.live = { modelId };
2159
+ return transcript;
2160
+ }
2161
+
2162
+ //#endregion
2163
+ //#region src/cli/commands/inspect.ts
2164
+ /**
2165
+ * Format state badge for console output.
2166
+ */
2167
+ function formatState$1(state, useColors) {
2168
+ const [text, colorFn] = {
2169
+ complete: ["✓ complete", pc.green],
2170
+ incomplete: ["○ incomplete", pc.yellow],
2171
+ empty: ["◌ empty", pc.dim],
2172
+ invalid: ["✗ invalid", pc.red]
2173
+ }[state] ?? [state, (s) => s];
2174
+ return useColors ? colorFn(text) : text;
2175
+ }
2176
+ /**
2177
+ * Format answer state badge for console output.
2178
+ */
2179
+ function formatAnswerState(state, useColors) {
2180
+ const [text, colorFn] = {
2181
+ answered: ["answered", pc.green],
2182
+ skipped: ["skipped", pc.yellow],
2183
+ aborted: ["aborted", pc.red],
2184
+ unanswered: ["unanswered", pc.dim]
2185
+ }[state] ?? [state, (s) => s];
2186
+ return useColors ? colorFn(text) : text;
2187
+ }
2188
+ /**
2189
+ * Format priority badge for console output.
2190
+ *
2191
+ * Priority tiers and colors:
2192
+ * - P1: bold red (critical)
2193
+ * - P2: yellow (high)
2194
+ * - P3: cyan (medium)
2195
+ * - P4: blue (low)
2196
+ * - P5: dim/gray (minimal)
2197
+ */
2198
+ function formatPriority$1(priority, useColors) {
2199
+ const label = `P${priority}`;
2200
+ if (!useColors) return label;
2201
+ switch (priority) {
2202
+ case 1: return pc.red(pc.bold(label));
2203
+ case 2: return pc.yellow(label);
2204
+ case 3: return pc.cyan(label);
2205
+ case 4: return pc.blue(label);
2206
+ case 5:
2207
+ default: return pc.dim(label);
2208
+ }
2209
+ }
2210
+ /**
2211
+ * Format severity badge for console output.
2212
+ */
2213
+ function formatSeverity$1(severity, useColors) {
2214
+ if (!useColors) return severity;
2215
+ return severity === "required" ? pc.red(severity) : pc.yellow(severity);
2216
+ }
2217
+ /**
2218
+ * Format a field value for console display.
2219
+ */
2220
+ function formatFieldValue(value, useColors) {
2221
+ const dim = useColors ? pc.dim : (s) => s;
2222
+ const green = useColors ? pc.green : (s) => s;
2223
+ if (!value) return dim("(empty)");
2224
+ switch (value.kind) {
2225
+ case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
2226
+ case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
2227
+ case "string_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
2228
+ case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
2229
+ case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
2230
+ case "checkboxes": {
2231
+ const entries = Object.entries(value.values);
2232
+ if (entries.length === 0) return dim("(no entries)");
2233
+ return entries.map(([k, v]) => `${k}:${v}`).join(", ");
2234
+ }
2235
+ case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
2236
+ case "url_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
2237
+ default: return dim("(unknown)");
2238
+ }
2239
+ }
2240
+ /**
2241
+ * Format inspect report for console output.
2242
+ */
2243
+ function formatConsoleReport$1(report, useColors) {
2244
+ const lines = [];
2245
+ const bold = useColors ? pc.bold : (s) => s;
2246
+ const dim = useColors ? pc.dim : (s) => s;
2247
+ const cyan = useColors ? pc.cyan : (s) => s;
2248
+ const yellow = useColors ? pc.yellow : (s) => s;
2249
+ lines.push(bold(cyan("Form Inspection Report")));
2250
+ if (report.title) lines.push(`${bold("Title:")} ${report.title}`);
2251
+ lines.push("");
2252
+ lines.push(`${bold("Form State:")} ${formatState$1(report.form_state, useColors)}`);
2253
+ lines.push("");
2254
+ const structure = report.structure;
2255
+ lines.push(bold("Structure:"));
2256
+ lines.push(` Groups: ${structure.groupCount}`);
2257
+ lines.push(` Fields: ${structure.fieldCount}`);
2258
+ lines.push(` Options: ${structure.optionCount}`);
2259
+ lines.push("");
2260
+ const progress = report.progress;
2261
+ lines.push(bold("Progress:"));
2262
+ lines.push(` Total fields: ${progress.counts.totalFields}`);
2263
+ lines.push(` Required: ${progress.counts.requiredFields}`);
2264
+ lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
2265
+ lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
2266
+ lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
2267
+ lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
2268
+ lines.push(` Total notes: ${progress.counts.totalNotes}`);
2269
+ lines.push("");
2270
+ lines.push(bold("Form Content:"));
2271
+ for (const group of report.groups) {
2272
+ lines.push(` ${bold(group.title ?? group.id)}`);
2273
+ for (const field of group.children) {
2274
+ const reqBadge = field.required ? yellow("[required]") : dim("[optional]");
2275
+ const roleBadge = field.role !== "agent" ? cyan(`[${field.role}]`) : "";
2276
+ const fieldProgress = progress.fields[field.id];
2277
+ const responseStateBadge = fieldProgress ? `[${formatAnswerState(fieldProgress.answerState, useColors)}]` : "";
2278
+ const notesBadge = fieldProgress?.hasNotes ? cyan(`[${fieldProgress.noteCount} note${fieldProgress.noteCount > 1 ? "s" : ""}]`) : "";
2279
+ const value = report.values[field.id];
2280
+ const valueStr = formatFieldValue(value, useColors);
2281
+ lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge} ${responseStateBadge} ${notesBadge}`.trim());
2282
+ lines.push(` ${dim("→")} ${valueStr}`);
2283
+ }
2284
+ }
2285
+ lines.push("");
2286
+ if (report.notes.length > 0) {
2287
+ lines.push(bold(`Notes (${report.notes.length}):`));
2288
+ for (const note of report.notes) {
2289
+ const roleBadge = cyan(`[${note.role}]`);
2290
+ const refLabel = dim(`${note.ref}:`);
2291
+ lines.push(` ${note.id} ${roleBadge} ${refLabel} ${note.text}`.trim());
2292
+ }
2293
+ lines.push("");
2294
+ }
2295
+ if (report.issues.length > 0) {
2296
+ lines.push(bold(`Issues (${report.issues.length}):`));
2297
+ for (const issue of report.issues) {
2298
+ const priority = formatPriority$1(issue.priority, useColors);
2299
+ const severity = formatSeverity$1(issue.severity, useColors);
2300
+ const blockedInfo = issue.blockedBy ? ` ${dim(`(blocked by: ${issue.blockedBy})`)}` : "";
2301
+ lines.push(` ${priority} (${severity}) ${dim(`[${issue.scope}]`)} ${dim(issue.ref)}: ${issue.message}${blockedInfo}`);
2302
+ }
2303
+ } else lines.push(dim("No issues found."));
2304
+ return lines.join("\n");
2305
+ }
2306
+ /**
2307
+ * Register the inspect command.
2308
+ */
2309
+ function registerInspectCommand(program) {
2310
+ program.command("inspect <file>").description("Inspect a form and display its structure, progress, and issues").option("--roles <roles>", "Filter issues by target roles (comma-separated, or '*' for all; default: all)").action(async (file, options, cmd) => {
2311
+ const ctx = getCommandContext(cmd);
2312
+ try {
2313
+ let targetRoles;
2314
+ if (options.roles) try {
2315
+ targetRoles = parseRolesFlag(options.roles);
2316
+ if (targetRoles.includes("*")) targetRoles = void 0;
2317
+ } catch (error) {
2318
+ logError(`Invalid --roles: ${error instanceof Error ? error.message : String(error)}`);
2319
+ process.exit(1);
2320
+ }
2321
+ logVerbose(ctx, `Reading file: ${file}`);
2322
+ const content = await readFile$1(file);
2323
+ logVerbose(ctx, "Parsing form...");
2324
+ const form = parseForm(content);
2325
+ logVerbose(ctx, "Running inspection...");
2326
+ const result = inspect(form, { targetRoles });
2327
+ const values = {};
2328
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
2329
+ const output = formatOutput(ctx, {
2330
+ title: form.schema.title,
2331
+ structure: result.structureSummary,
2332
+ progress: result.progressSummary,
2333
+ form_state: result.formState,
2334
+ groups: form.schema.groups.map((group) => ({
2335
+ id: group.id,
2336
+ title: group.title,
2337
+ children: group.children.map((field) => ({
2338
+ id: field.id,
2339
+ kind: field.kind,
2340
+ label: field.label,
2341
+ required: field.required,
2342
+ role: field.role
2343
+ }))
2344
+ })),
2345
+ values,
2346
+ notes: form.notes,
2347
+ issues: result.issues.map((issue) => ({
2348
+ ref: issue.ref,
2349
+ scope: issue.scope,
2350
+ reason: issue.reason,
2351
+ message: issue.message,
2352
+ priority: issue.priority,
2353
+ severity: issue.severity,
2354
+ blockedBy: issue.blockedBy
2355
+ }))
2356
+ }, (data, useColors) => formatConsoleReport$1(data, useColors));
2357
+ console.log(output);
2358
+ } catch (error) {
2359
+ logError(error instanceof Error ? error.message : String(error));
2360
+ process.exit(1);
2361
+ }
2362
+ });
2363
+ }
2364
+
2365
+ //#endregion
2366
+ //#region src/cli/commands/readme.ts
2367
+ /**
2368
+ * Get the path to the README.md file.
2369
+ * Works both during development and when installed as a package.
2370
+ */
2371
+ function getReadmePath() {
2372
+ const thisDir = dirname(fileURLToPath(import.meta.url));
2373
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "README.md");
2374
+ return join(dirname(dirname(dirname(thisDir))), "README.md");
2375
+ }
2376
+ /**
2377
+ * Load the README content.
2378
+ */
2379
+ function loadReadme() {
2380
+ const readmePath = getReadmePath();
2381
+ try {
2382
+ return readFileSync(readmePath, "utf-8");
2383
+ } catch (error) {
2384
+ const message = error instanceof Error ? error.message : String(error);
2385
+ throw new Error(`Failed to load README from ${readmePath}: ${message}`);
2386
+ }
2387
+ }
2388
+ /**
2389
+ * Apply basic terminal formatting to markdown content.
2390
+ * Colorizes headers, code blocks, and other elements for better readability.
2391
+ */
2392
+ function formatMarkdown$1(content, useColors) {
2393
+ if (!useColors) return content;
2394
+ const lines = content.split("\n");
2395
+ const formatted = [];
2396
+ let inCodeBlock = false;
2397
+ for (const line of lines) {
2398
+ if (line.startsWith("```")) {
2399
+ inCodeBlock = !inCodeBlock;
2400
+ formatted.push(pc.dim(line));
2401
+ continue;
2402
+ }
2403
+ if (inCodeBlock) {
2404
+ formatted.push(pc.dim(line));
2405
+ continue;
2406
+ }
2407
+ if (line.startsWith("# ")) {
2408
+ formatted.push(pc.bold(pc.cyan(line)));
2409
+ continue;
2410
+ }
2411
+ if (line.startsWith("## ")) {
2412
+ formatted.push(pc.bold(pc.blue(line)));
2413
+ continue;
2414
+ }
2415
+ if (line.startsWith("### ")) {
2416
+ formatted.push(pc.bold(line));
2417
+ continue;
2418
+ }
2419
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
2420
+ return pc.yellow(code);
2421
+ });
2422
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
2423
+ return pc.bold(text);
2424
+ });
2425
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
2426
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
2427
+ });
2428
+ formatted.push(formattedLine);
2429
+ }
2430
+ return formatted.join("\n");
2431
+ }
2432
+ /**
2433
+ * Check if stdout is an interactive terminal.
2434
+ */
2435
+ function isInteractive$1() {
2436
+ return process.stdout.isTTY === true;
2437
+ }
2438
+ /**
2439
+ * Display content. In a future enhancement, could pipe to a pager for long output.
2440
+ */
2441
+ function displayContent$1(content) {
2442
+ console.log(content);
2443
+ }
2444
+ /**
2445
+ * Register the readme command.
2446
+ */
2447
+ function registerReadmeCommand(program) {
2448
+ program.command("readme").description("✨Display README documentation ← START HERE!").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
2449
+ const ctx = getCommandContext(cmd);
2450
+ try {
2451
+ displayContent$1(formatMarkdown$1(loadReadme(), !options.raw && isInteractive$1() && !ctx.quiet));
2452
+ } catch (error) {
2453
+ logError(error instanceof Error ? error.message : String(error));
2454
+ process.exit(1);
2455
+ }
2456
+ });
2457
+ }
2458
+
2459
+ //#endregion
2460
+ //#region src/cli/commands/report.ts
2461
+ /**
2462
+ * Register the report command.
2463
+ */
2464
+ function registerReportCommand(program) {
2465
+ program.command("report <file>").description("Generate filtered markdown report (excludes instructions, report=false elements)").option("-o, --output <file>", "Output file path (default: stdout)").action(async (file, options, cmd) => {
2466
+ const ctx = getCommandContext(cmd);
2467
+ try {
2468
+ logVerbose(ctx, `Reading file: ${file}`);
2469
+ const content = await readFile$1(file);
2470
+ logVerbose(ctx, "Parsing form...");
2471
+ const form = parseForm(content);
2472
+ logVerbose(ctx, "Generating report...");
2473
+ const reportContent = serializeReportMarkdown(form);
2474
+ if (options.output) {
2475
+ let outputPath = options.output;
2476
+ if (!outputPath.endsWith(REPORT_EXTENSION) && !outputPath.endsWith(".md")) outputPath = outputPath + REPORT_EXTENSION;
2477
+ await writeFile(outputPath, reportContent);
2478
+ logVerbose(ctx, `Report written to: ${outputPath}`);
2479
+ } else console.log(reportContent);
2480
+ } catch (error) {
2481
+ logError(error instanceof Error ? error.message : String(error));
2482
+ process.exit(1);
2483
+ }
2484
+ });
2485
+ }
2486
+
2487
+ //#endregion
2488
+ //#region src/cli/commands/spec.ts
2489
+ /**
2490
+ * Get the path to the SPEC.md file.
2491
+ * Works both during development and when installed as a package.
2492
+ */
2493
+ function getSpecPath() {
2494
+ const thisDir = dirname(fileURLToPath(import.meta.url));
2495
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "SPEC.md");
2496
+ return join(dirname(dirname(dirname(thisDir))), "SPEC.md");
2497
+ }
2498
+ /**
2499
+ * Load the spec content.
2500
+ */
2501
+ function loadSpec() {
2502
+ const specPath = getSpecPath();
2503
+ try {
2504
+ return readFileSync(specPath, "utf-8");
2505
+ } catch (error) {
2506
+ const message = error instanceof Error ? error.message : String(error);
2507
+ throw new Error(`Failed to load SPEC from ${specPath}: ${message}`);
2508
+ }
2509
+ }
2510
+ /**
2511
+ * Apply basic terminal formatting to markdown content.
2512
+ * Colorizes headers, code blocks, and other elements for better readability.
2513
+ */
2514
+ function formatMarkdown(content, useColors) {
2515
+ if (!useColors) return content;
2516
+ const lines = content.split("\n");
2517
+ const formatted = [];
2518
+ let inCodeBlock = false;
2519
+ for (const line of lines) {
2520
+ if (line.startsWith("```")) {
2521
+ inCodeBlock = !inCodeBlock;
2522
+ formatted.push(pc.dim(line));
2523
+ continue;
2524
+ }
2525
+ if (inCodeBlock) {
2526
+ formatted.push(pc.dim(line));
2527
+ continue;
2528
+ }
2529
+ if (line.startsWith("# ")) {
2530
+ formatted.push(pc.bold(pc.cyan(line)));
2531
+ continue;
2532
+ }
2533
+ if (line.startsWith("## ")) {
2534
+ formatted.push(pc.bold(pc.blue(line)));
2535
+ continue;
2536
+ }
2537
+ if (line.startsWith("### ")) {
2538
+ formatted.push(pc.bold(line));
2539
+ continue;
2540
+ }
2541
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
2542
+ return pc.yellow(code);
2543
+ });
2544
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
2545
+ return pc.bold(text);
2546
+ });
2547
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
2548
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
2549
+ });
2550
+ formatted.push(formattedLine);
2551
+ }
2552
+ return formatted.join("\n");
2553
+ }
2554
+ /**
2555
+ * Check if stdout is an interactive terminal.
2556
+ */
2557
+ function isInteractive() {
2558
+ return process.stdout.isTTY === true;
2559
+ }
2560
+ /**
2561
+ * Display content. In a future enhancement, could pipe to a pager for long output.
2562
+ */
2563
+ function displayContent(content) {
2564
+ console.log(content);
2565
+ }
2566
+ /**
2567
+ * Register the spec command.
2568
+ */
2569
+ function registerSpecCommand(program) {
2570
+ program.command("spec").description("Display the Markform specification").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
2571
+ const ctx = getCommandContext(cmd);
2572
+ try {
2573
+ displayContent(formatMarkdown(loadSpec(), !options.raw && isInteractive() && !ctx.quiet));
2574
+ } catch (error) {
2575
+ logError(error instanceof Error ? error.message : String(error));
2576
+ process.exit(1);
2577
+ }
2578
+ });
2579
+ }
2580
+
2581
+ //#endregion
2582
+ //#region src/cli/commands/models.ts
2583
+ /**
2584
+ * Get model info for all providers or a specific one.
2585
+ */
2586
+ function getModelInfo(providerFilter) {
2587
+ const providers = getProviderNames();
2588
+ if (providerFilter && !providers.includes(providerFilter)) throw new Error(`Unknown provider: "${providerFilter}". Available: ${providers.join(", ")}`);
2589
+ return (providerFilter ? [providerFilter] : providers).map((provider) => {
2590
+ return {
2591
+ provider,
2592
+ envVar: getProviderInfo(provider).envVar,
2593
+ models: SUGGESTED_LLMS[provider] ?? []
2594
+ };
2595
+ });
2596
+ }
2597
+ /**
2598
+ * Format model info for console output.
2599
+ */
2600
+ function formatConsoleOutput(info, useColors) {
2601
+ const lines = [];
2602
+ const bold = useColors ? pc.bold : (s) => s;
2603
+ const cyan = useColors ? pc.cyan : (s) => s;
2604
+ const dim = useColors ? pc.dim : (s) => s;
2605
+ const green = useColors ? pc.green : (s) => s;
2606
+ for (const { provider, envVar, models } of info) {
2607
+ lines.push(bold(cyan(`${provider}/`)));
2608
+ lines.push(` ${dim("env:")} ${envVar}`);
2609
+ if (models.length > 0) {
2610
+ lines.push(` ${dim("models:")}`);
2611
+ for (const model of models) lines.push(` ${green(`${provider}/${model}`)}`);
2612
+ } else lines.push(` ${dim("(no suggested models)")}`);
2613
+ lines.push("");
2614
+ }
2615
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
2616
+ return lines.join("\n");
2617
+ }
2618
+ /**
2619
+ * Register the models command.
2620
+ */
2621
+ function registerModelsCommand(program) {
2622
+ program.command("models").description("List available AI providers and example models").option("-p, --provider <name>", "Filter by provider (anthropic, openai, google, xai, deepseek)").action((options, cmd) => {
2623
+ const ctx = getCommandContext(cmd);
2624
+ try {
2625
+ const output = formatOutput(ctx, getModelInfo(options.provider), (data, useColors) => formatConsoleOutput(data, useColors));
2626
+ console.log(output);
2627
+ } catch (error) {
2628
+ logError(error instanceof Error ? error.message : String(error));
2629
+ process.exit(1);
2630
+ }
2631
+ });
2632
+ }
2633
+
2634
+ //#endregion
2635
+ //#region src/cli/commands/serve.ts
2636
+ /**
2637
+ * Open a URL in the default browser (cross-platform).
2638
+ */
2639
+ function openBrowser(url) {
2640
+ const platform = process.platform;
2641
+ let command;
2642
+ if (platform === "darwin") command = `open "${url}"`;
2643
+ else if (platform === "win32") command = `start "" "${url}"`;
2644
+ else command = `xdg-open "${url}"`;
2645
+ exec(command, (error) => {
2646
+ if (error) {}
2647
+ });
2648
+ }
2649
+ /**
2650
+ * Register the serve command.
2651
+ */
2652
+ function registerServeCommand(program) {
2653
+ program.command("serve <file>").description("Serve a file as a web page (forms are interactive, others are read-only)").option("-p, --port <port>", "Port to serve on", String(DEFAULT_PORT)).option("--no-open", "Don't open browser automatically").action(async (file, options, cmd) => {
2654
+ const ctx = getCommandContext(cmd);
2655
+ const port = parseInt(options.port ?? String(DEFAULT_PORT), 10);
2656
+ const filePath = resolve(file);
2657
+ const fileType = detectFileType(filePath);
2658
+ try {
2659
+ logVerbose(ctx, `Reading file: ${filePath}`);
2660
+ const content = await readFile$1(filePath);
2661
+ let form = null;
2662
+ if (fileType === "form") form = parseForm(content);
2663
+ const server = createServer((req, res) => {
2664
+ handleRequest(req, res, filePath, fileType, form, ctx, (updatedForm) => {
2665
+ form = updatedForm;
2666
+ }).catch((err) => {
2667
+ console.error("Request error:", err);
2668
+ res.writeHead(500);
2669
+ res.end("Internal Server Error");
2670
+ });
2671
+ });
2672
+ server.listen(port, () => {
2673
+ const url = `http://localhost:${port}`;
2674
+ const typeLabel = fileType === "form" ? "Form" : fileType === "unknown" ? "File" : fileType.toUpperCase();
2675
+ logInfo(ctx, pc.green(`\n✓ ${typeLabel} server running at ${pc.bold(url)}\n`));
2676
+ logInfo(ctx, pc.dim("Press Ctrl+C to stop\n"));
2677
+ if (options.open !== false) openBrowser(url);
2678
+ });
2679
+ process.on("SIGINT", () => {
2680
+ logInfo(ctx, "\nShutting down server...");
2681
+ server.close();
2682
+ process.exit(0);
2683
+ });
2684
+ } catch (error) {
2685
+ logError(error instanceof Error ? error.message : String(error));
2686
+ process.exit(1);
2687
+ }
2688
+ });
2689
+ }
2690
+ /**
2691
+ * Handle HTTP requests.
2692
+ * Dispatches to appropriate renderer based on file type.
2693
+ */
2694
+ async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm) {
2695
+ const url = req.url ?? "/";
2696
+ if (req.method === "GET" && url === "/") if (fileType === "form" && form) {
2697
+ const html = renderFormHtml(form);
2698
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2699
+ res.end(html);
2700
+ } else if (fileType === "raw" || fileType === "report") {
2701
+ const html = renderMarkdownHtml(await readFile(filePath, "utf-8"), basename(filePath));
2702
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2703
+ res.end(html);
2704
+ } else if (fileType === "yaml") {
2705
+ const html = renderYamlHtml(await readFile(filePath, "utf-8"), basename(filePath));
2706
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2707
+ res.end(html);
2708
+ } else if (fileType === "json") {
2709
+ const html = renderJsonHtml(await readFile(filePath, "utf-8"), basename(filePath));
2710
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2711
+ res.end(html);
2712
+ } else {
2713
+ const html = renderPlainTextHtml(await readFile(filePath, "utf-8"), basename(filePath));
2714
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2715
+ res.end(html);
2716
+ }
2717
+ else if (req.method === "POST" && url === "/save" && fileType === "form" && form) await handleSave(req, res, form, filePath, ctx, updateForm);
2718
+ else if (url === "/api/form" && form) {
2719
+ res.writeHead(200, { "Content-Type": "application/json" });
2720
+ res.end(JSON.stringify({ schema: form.schema }));
2721
+ } else {
2722
+ res.writeHead(404);
2723
+ res.end("Not Found");
2724
+ }
2725
+ }
2726
+ /**
2727
+ * Parse form body data.
2728
+ */
2729
+ function parseFormBody(body) {
2730
+ const result = {};
2731
+ const params = new URLSearchParams(body);
2732
+ for (const [key, value] of params) {
2733
+ const existing = result[key];
2734
+ if (existing !== void 0) if (Array.isArray(existing)) existing.push(value);
2735
+ else result[key] = [existing, value];
2736
+ else result[key] = value;
2737
+ }
2738
+ return result;
2739
+ }
2740
+ /**
2741
+ * Convert form data to patches.
2742
+ */
2743
+ function formDataToPatches(formData, form) {
2744
+ const patches = [];
2745
+ const fields = form.schema.groups.flatMap((g) => g.children);
2746
+ for (const field of fields) {
2747
+ const fieldId = field.id;
2748
+ if (formData[`__skip__${fieldId}`] === "1" && !field.required) {
2749
+ patches.push({
2750
+ op: "skip_field",
2751
+ fieldId,
2752
+ role: "user"
2753
+ });
2754
+ continue;
2755
+ }
2756
+ switch (field.kind) {
2757
+ case "string": {
2758
+ const value = formData[fieldId];
2759
+ if (typeof value === "string" && value.trim() !== "") patches.push({
2760
+ op: "set_string",
2761
+ fieldId,
2762
+ value: value.trim()
2763
+ });
2764
+ else if (!value || typeof value === "string" && value.trim() === "") patches.push({
2765
+ op: "clear_field",
2766
+ fieldId
2767
+ });
2768
+ break;
2769
+ }
2770
+ case "number": {
2771
+ const value = formData[fieldId];
2772
+ if (typeof value === "string" && value.trim() !== "") {
2773
+ const num = parseFloat(value);
2774
+ if (!isNaN(num)) patches.push({
2775
+ op: "set_number",
2776
+ fieldId,
2777
+ value: num
2778
+ });
2779
+ } else patches.push({
2780
+ op: "clear_field",
2781
+ fieldId
2782
+ });
2783
+ break;
2784
+ }
2785
+ case "string_list": {
2786
+ const value = formData[fieldId];
2787
+ if (typeof value === "string" && value.trim() !== "") {
2788
+ const items = value.split("\n").map((s) => s.trim()).filter((s) => s !== "");
2789
+ if (items.length > 0) patches.push({
2790
+ op: "set_string_list",
2791
+ fieldId,
2792
+ items
2793
+ });
2794
+ else patches.push({
2795
+ op: "clear_field",
2796
+ fieldId
2797
+ });
2798
+ } else patches.push({
2799
+ op: "clear_field",
2800
+ fieldId
2801
+ });
2802
+ break;
2803
+ }
2804
+ case "single_select": {
2805
+ const value = formData[fieldId];
2806
+ if (typeof value === "string" && value !== "") patches.push({
2807
+ op: "set_single_select",
2808
+ fieldId,
2809
+ selected: value
2810
+ });
2811
+ else patches.push({
2812
+ op: "clear_field",
2813
+ fieldId
2814
+ });
2815
+ break;
2816
+ }
2817
+ case "multi_select": {
2818
+ const value = formData[fieldId];
2819
+ const selected = Array.isArray(value) ? value : value ? [value] : [];
2820
+ if (selected.length > 0 && selected[0] !== "") patches.push({
2821
+ op: "set_multi_select",
2822
+ fieldId,
2823
+ selected
2824
+ });
2825
+ else patches.push({
2826
+ op: "clear_field",
2827
+ fieldId
2828
+ });
2829
+ break;
2830
+ }
2831
+ case "checkboxes":
2832
+ if ((field.checkboxMode ?? "multi") === "simple") {
2833
+ const value = formData[fieldId];
2834
+ const checked = Array.isArray(value) ? value : value ? [value] : [];
2835
+ const values = {};
2836
+ for (const opt of field.options) values[opt.id] = checked.includes(opt.id) ? "done" : "todo";
2837
+ patches.push({
2838
+ op: "set_checkboxes",
2839
+ fieldId,
2840
+ values
2841
+ });
2842
+ } else {
2843
+ const values = {};
2844
+ for (const opt of field.options) {
2845
+ const selectValue = formData[`${fieldId}.${opt.id}`];
2846
+ if (typeof selectValue === "string" && selectValue !== "") values[opt.id] = selectValue;
2847
+ }
2848
+ if (Object.keys(values).length > 0) patches.push({
2849
+ op: "set_checkboxes",
2850
+ fieldId,
2851
+ values
2852
+ });
2853
+ }
2854
+ break;
2855
+ case "url": {
2856
+ const value = formData[fieldId];
2857
+ if (typeof value === "string" && value.trim() !== "") patches.push({
2858
+ op: "set_url",
2859
+ fieldId,
2860
+ value: value.trim()
2861
+ });
2862
+ else patches.push({
2863
+ op: "clear_field",
2864
+ fieldId
2865
+ });
2866
+ break;
2867
+ }
2868
+ case "url_list": {
2869
+ const value = formData[fieldId];
2870
+ if (typeof value === "string" && value.trim() !== "") {
2871
+ const items = value.split("\n").map((s) => s.trim()).filter((s) => s !== "");
2872
+ if (items.length > 0) patches.push({
2873
+ op: "set_url_list",
2874
+ fieldId,
2875
+ items
2876
+ });
2877
+ else patches.push({
2878
+ op: "clear_field",
2879
+ fieldId
2880
+ });
2881
+ } else patches.push({
2882
+ op: "clear_field",
2883
+ fieldId
2884
+ });
2885
+ break;
2886
+ }
2887
+ }
2888
+ }
2889
+ return patches;
2890
+ }
2891
+ /**
2892
+ * Handle form save request.
2893
+ */
2894
+ async function handleSave(req, res, form, filePath, ctx, updateForm) {
2895
+ try {
2896
+ const chunks = [];
2897
+ for await (const chunk of req) chunks.push(chunk);
2898
+ applyPatches(form, formDataToPatches(parseFormBody(Buffer.concat(chunks).toString("utf-8")), form));
2899
+ updateForm(form);
2900
+ const newPath = generateVersionedPath(filePath);
2901
+ const content = serialize(form);
2902
+ if (ctx.dryRun) {
2903
+ logInfo(ctx, `[DRY RUN] Would save to: ${newPath}`);
2904
+ res.writeHead(200, { "Content-Type": "application/json" });
2905
+ res.end(JSON.stringify({
2906
+ success: true,
2907
+ path: newPath,
2908
+ dryRun: true
2909
+ }));
2910
+ return;
2911
+ }
2912
+ await writeFile(newPath, content);
2913
+ logInfo(ctx, pc.green(`Saved to: ${newPath}`));
2914
+ res.writeHead(200, { "Content-Type": "application/json" });
2915
+ res.end(JSON.stringify({
2916
+ success: true,
2917
+ path: newPath
2918
+ }));
2919
+ } catch (error) {
2920
+ const message = error instanceof Error ? error.message : String(error);
2921
+ res.writeHead(500, { "Content-Type": "application/json" });
2922
+ res.end(JSON.stringify({
2923
+ success: false,
2924
+ error: message
2925
+ }));
2926
+ }
2927
+ }
2928
+ /**
2929
+ * Render the form as HTML.
2930
+ * @public Exported for testing.
2931
+ */
2932
+ function renderFormHtml(form) {
2933
+ const { schema, responsesByFieldId } = form;
2934
+ const formTitle = schema.title ?? schema.id;
2935
+ const groupsHtml = schema.groups.map((group) => renderGroup(group, responsesByFieldId)).join("\n");
2936
+ return `<!DOCTYPE html>
2937
+ <html lang="en">
2938
+ <head>
2939
+ <meta charset="UTF-8">
2940
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2941
+ <title>${escapeHtml(formTitle)} - Markform</title>
2942
+ <style>
2943
+ * { box-sizing: border-box; }
2944
+ body {
2945
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2946
+ line-height: 1.6;
2947
+ max-width: 800px;
2948
+ margin: 0 auto;
2949
+ padding: 2rem;
2950
+ background: #f8f9fa;
2951
+ color: #212529;
2952
+ }
2953
+ h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; }
2954
+ h2 { color: #6c757d; font-size: 1.25rem; }
2955
+ .group {
2956
+ background: white;
2957
+ border-radius: 8px;
2958
+ padding: 1.5rem;
2959
+ margin-bottom: 1.5rem;
2960
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
2961
+ }
2962
+ .field {
2963
+ margin-bottom: 1.5rem;
2964
+ padding-bottom: 1rem;
2965
+ border-bottom: 1px solid #e9ecef;
2966
+ }
2967
+ .field:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
2968
+ .field-label {
2969
+ font-weight: 600;
2970
+ color: #495057;
2971
+ display: block;
2972
+ margin-bottom: 0.5rem;
2973
+ }
2974
+ .required { color: #dc3545; }
2975
+ .type-badge {
2976
+ font-size: 0.7rem;
2977
+ padding: 0.1rem 0.3rem;
2978
+ background: #e9ecef;
2979
+ border-radius: 3px;
2980
+ color: #6c757d;
2981
+ margin-left: 0.5rem;
2982
+ font-weight: normal;
2983
+ }
2984
+ input[type="text"],
2985
+ input[type="number"],
2986
+ textarea,
2987
+ select {
2988
+ width: 100%;
2989
+ padding: 0.5rem 0.75rem;
2990
+ font-size: 1rem;
2991
+ border: 1px solid #ced4da;
2992
+ border-radius: 4px;
2993
+ background: #fff;
2994
+ transition: border-color 0.15s ease-in-out;
2995
+ }
2996
+ input[type="text"]:focus,
2997
+ input[type="number"]:focus,
2998
+ textarea:focus,
2999
+ select:focus {
3000
+ outline: none;
3001
+ border-color: #80bdff;
3002
+ box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
3003
+ }
3004
+ textarea {
3005
+ min-height: 100px;
3006
+ resize: vertical;
3007
+ }
3008
+ .checkbox-group {
3009
+ display: flex;
3010
+ flex-direction: column;
3011
+ gap: 0.5rem;
3012
+ }
3013
+ .checkbox-item {
3014
+ display: flex;
3015
+ align-items: center;
3016
+ gap: 0.5rem;
3017
+ }
3018
+ .checkbox-item input[type="checkbox"] {
3019
+ width: auto;
3020
+ margin: 0;
3021
+ }
3022
+ .checkbox-item label {
3023
+ margin: 0;
3024
+ font-weight: normal;
3025
+ cursor: pointer;
3026
+ }
3027
+ .checkbox-item select {
3028
+ width: auto;
3029
+ min-width: 120px;
3030
+ }
3031
+ .option-row {
3032
+ display: flex;
3033
+ align-items: center;
3034
+ gap: 0.75rem;
3035
+ margin-bottom: 0.5rem;
3036
+ }
3037
+ .option-row:last-child { margin-bottom: 0; }
3038
+ .option-label {
3039
+ flex: 1;
3040
+ }
3041
+ .toolbar {
3042
+ position: fixed;
3043
+ bottom: 2rem;
3044
+ right: 2rem;
3045
+ display: flex;
3046
+ gap: 0.5rem;
3047
+ }
3048
+ .btn {
3049
+ padding: 0.75rem 1.5rem;
3050
+ border: none;
3051
+ border-radius: 6px;
3052
+ font-size: 1rem;
3053
+ cursor: pointer;
3054
+ transition: all 0.2s;
3055
+ }
3056
+ .btn-primary {
3057
+ background: #0d6efd;
3058
+ color: white;
3059
+ }
3060
+ .btn-primary:hover { background: #0b5ed7; }
3061
+ .field-help {
3062
+ font-size: 0.85rem;
3063
+ color: #6c757d;
3064
+ margin-top: 0.25rem;
3065
+ }
3066
+ .field-actions {
3067
+ display: flex;
3068
+ gap: 0.5rem;
3069
+ margin-top: 0.5rem;
3070
+ }
3071
+ .btn-skip {
3072
+ padding: 0.25rem 0.75rem;
3073
+ font-size: 0.85rem;
3074
+ background: #f8f9fa;
3075
+ border: 1px solid #ced4da;
3076
+ border-radius: 4px;
3077
+ color: #6c757d;
3078
+ cursor: pointer;
3079
+ }
3080
+ .btn-skip:hover {
3081
+ background: #e9ecef;
3082
+ color: #495057;
3083
+ }
3084
+ .field-skipped {
3085
+ opacity: 0.6;
3086
+ }
3087
+ .field-skipped input,
3088
+ .field-skipped textarea,
3089
+ .field-skipped select {
3090
+ background: #f8f9fa;
3091
+ }
3092
+ .skipped-badge {
3093
+ font-size: 0.75rem;
3094
+ padding: 0.15rem 0.4rem;
3095
+ background: #6c757d;
3096
+ color: white;
3097
+ border-radius: 3px;
3098
+ margin-left: 0.5rem;
3099
+ }
3100
+ </style>
3101
+ </head>
3102
+ <body>
3103
+ <h1>${escapeHtml(formTitle)}</h1>
3104
+ <form method="POST" action="/save" id="markform">
3105
+ ${groupsHtml}
3106
+ <div class="toolbar">
3107
+ <button type="submit" class="btn btn-primary">Save</button>
3108
+ </div>
3109
+ </form>
3110
+ <script>
3111
+ // Track fields marked for skip
3112
+ const skippedFields = new Set();
3113
+
3114
+ // Handle skip button clicks
3115
+ document.querySelectorAll('.btn-skip').forEach(btn => {
3116
+ btn.addEventListener('click', (e) => {
3117
+ const fieldId = e.target.dataset.skipField;
3118
+ if (!fieldId) return;
3119
+
3120
+ // Toggle skip state
3121
+ if (skippedFields.has(fieldId)) {
3122
+ skippedFields.delete(fieldId);
3123
+ e.target.textContent = 'Skip';
3124
+ e.target.classList.remove('btn-skip-active');
3125
+ // Re-enable the field input
3126
+ const fieldDiv = e.target.closest('.field');
3127
+ fieldDiv.classList.remove('field-skipped');
3128
+ fieldDiv.querySelectorAll('input, select, textarea').forEach(input => {
3129
+ input.disabled = false;
3130
+ });
3131
+ } else {
3132
+ skippedFields.add(fieldId);
3133
+ e.target.textContent = 'Unskip';
3134
+ e.target.classList.add('btn-skip-active');
3135
+ // Disable the field input to show it's skipped
3136
+ const fieldDiv = e.target.closest('.field');
3137
+ fieldDiv.classList.add('field-skipped');
3138
+ fieldDiv.querySelectorAll('input, select, textarea').forEach(input => {
3139
+ input.disabled = true;
3140
+ });
3141
+ }
3142
+ });
3143
+ });
3144
+
3145
+ document.getElementById('markform').addEventListener('submit', async (e) => {
3146
+ e.preventDefault();
3147
+ const formData = new FormData(e.target);
3148
+ const params = new URLSearchParams();
3149
+
3150
+ // Add skip markers for skipped fields
3151
+ for (const fieldId of skippedFields) {
3152
+ params.append('__skip__' + fieldId, '1');
3153
+ }
3154
+
3155
+ for (const [key, value] of formData) {
3156
+ params.append(key, value);
3157
+ }
3158
+ try {
3159
+ const res = await fetch('/save', {
3160
+ method: 'POST',
3161
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
3162
+ body: params.toString()
3163
+ });
3164
+ const data = await res.json();
3165
+ if (data.success) {
3166
+ alert('Saved to: ' + data.path);
3167
+ location.reload();
3168
+ } else {
3169
+ alert('Error: ' + data.error);
3170
+ }
3171
+ } catch (err) {
3172
+ alert('Save failed: ' + err.message);
3173
+ }
3174
+ });
3175
+ <\/script>
3176
+ </body>
3177
+ </html>`;
3178
+ }
3179
+ /**
3180
+ * Render a field group as HTML.
3181
+ */
3182
+ function renderGroup(group, responses) {
3183
+ const groupTitle = group.title ?? group.id;
3184
+ const fieldsHtml = group.children.map((field) => {
3185
+ const response = responses[field.id];
3186
+ return renderFieldHtml(field, response?.state === "answered" ? response.value : void 0, response?.state === "skipped");
3187
+ }).join("\n");
3188
+ return `
3189
+ <div class="group">
3190
+ <h2>${escapeHtml(groupTitle)}</h2>
3191
+ ${fieldsHtml}
3192
+ </div>`;
3193
+ }
3194
+ /**
3195
+ * Render a field as HTML.
3196
+ * @public Exported for testing.
3197
+ */
3198
+ function renderFieldHtml(field, value, isSkipped) {
3199
+ const skipped = isSkipped === true;
3200
+ const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
3201
+ const typeLabel = `<span class="type-badge">${field.kind}</span>`;
3202
+ const skippedBadge = skipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
3203
+ const fieldClass = skipped ? "field field-skipped" : "field";
3204
+ const disabledAttr = skipped ? " disabled" : "";
3205
+ let inputHtml;
3206
+ switch (field.kind) {
3207
+ case "string":
3208
+ inputHtml = renderStringInput(field, value, disabledAttr);
3209
+ break;
3210
+ case "number":
3211
+ inputHtml = renderNumberInput(field, value, disabledAttr);
3212
+ break;
3213
+ case "string_list":
3214
+ inputHtml = renderStringListInput(field, value, disabledAttr);
3215
+ break;
3216
+ case "single_select":
3217
+ inputHtml = renderSingleSelectInput(field, value, disabledAttr);
3218
+ break;
3219
+ case "multi_select":
3220
+ inputHtml = renderMultiSelectInput(field, value, disabledAttr);
3221
+ break;
3222
+ case "checkboxes":
3223
+ inputHtml = renderCheckboxesInput(field, value, disabledAttr);
3224
+ break;
3225
+ case "url":
3226
+ inputHtml = renderUrlInput(field, value, disabledAttr);
3227
+ break;
3228
+ case "url_list":
3229
+ inputHtml = renderUrlListInput(field, value, disabledAttr);
3230
+ break;
3231
+ default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
3232
+ }
3233
+ const skipButton = !field.required && !skipped ? `<div class="field-actions">
3234
+ <button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
3235
+ </div>` : "";
3236
+ return `
3237
+ <div class="${fieldClass}">
3238
+ <label class="field-label" for="field-${field.id}">
3239
+ ${escapeHtml(field.label)} ${requiredMark} ${typeLabel} ${skippedBadge}
3240
+ </label>
3241
+ ${inputHtml}
3242
+ ${skipButton}
3243
+ </div>`;
3244
+ }
3245
+ /**
3246
+ * Render a string field as text input.
3247
+ */
3248
+ function renderStringInput(field, value, disabledAttr) {
3249
+ const currentValue = value?.kind === "string" && value.value !== null ? value.value : "";
3250
+ const requiredAttr = field.required ? " required" : "";
3251
+ const minLengthAttr = field.minLength !== void 0 ? ` minlength="${field.minLength}"` : "";
3252
+ const maxLengthAttr = field.maxLength !== void 0 ? ` maxlength="${field.maxLength}"` : "";
3253
+ return `<input type="text" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minLengthAttr}${maxLengthAttr}${disabledAttr}>`;
3254
+ }
3255
+ /**
3256
+ * Render a number field as number input.
3257
+ */
3258
+ function renderNumberInput(field, value, disabledAttr) {
3259
+ const currentValue = value?.kind === "number" && value.value !== null ? String(value.value) : "";
3260
+ const requiredAttr = field.required ? " required" : "";
3261
+ const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
3262
+ const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
3263
+ const stepAttr = field.integer ? " step=\"1\"" : "";
3264
+ return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${stepAttr}${disabledAttr}>`;
3265
+ }
3266
+ /**
3267
+ * Render a string list field as textarea.
3268
+ */
3269
+ function renderStringListInput(field, value, disabledAttr) {
3270
+ const currentValue = (value?.kind === "string_list" ? value.items : []).join("\n");
3271
+ const requiredAttr = field.required ? " required" : "";
3272
+ return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one item per line"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3273
+ }
3274
+ /**
3275
+ * Render a URL field as url input.
3276
+ */
3277
+ function renderUrlInput(field, value, disabledAttr) {
3278
+ const currentValue = value?.kind === "url" && value.value !== null ? value.value : "";
3279
+ const requiredAttr = field.required ? " required" : "";
3280
+ return `<input type="url" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}" placeholder="https://example.com"${requiredAttr}${disabledAttr}>`;
3281
+ }
3282
+ /**
3283
+ * Render a URL list field as textarea.
3284
+ */
3285
+ function renderUrlListInput(field, value, disabledAttr) {
3286
+ const currentValue = (value?.kind === "url_list" ? value.items : []).join("\n");
3287
+ const requiredAttr = field.required ? " required" : "";
3288
+ return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one URL per line"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3289
+ }
3290
+ /**
3291
+ * Render a single-select field as select element.
3292
+ */
3293
+ function renderSingleSelectInput(field, value, disabledAttr) {
3294
+ const selected = value?.selected ?? null;
3295
+ const requiredAttr = field.required ? " required" : "";
3296
+ const options = field.options.map((opt) => {
3297
+ const selectedAttr = selected === opt.id ? " selected" : "";
3298
+ return `<option value="${escapeHtml(opt.id)}"${selectedAttr}>${escapeHtml(opt.label)}</option>`;
3299
+ }).join("\n ");
3300
+ return `<select id="field-${field.id}" name="${field.id}"${requiredAttr}${disabledAttr}>
3301
+ <option value="">-- Select --</option>
3302
+ ${options}
3303
+ </select>`;
3304
+ }
3305
+ /**
3306
+ * Render a multi-select field as checkboxes.
3307
+ */
3308
+ function renderMultiSelectInput(field, value, disabledAttr) {
3309
+ const selected = value?.selected ?? [];
3310
+ return `<div class="checkbox-group">
3311
+ ${field.options.map((opt) => {
3312
+ const checkedAttr = selected.includes(opt.id) ? " checked" : "";
3313
+ const checkboxId = `field-${field.id}-${opt.id}`;
3314
+ return `<div class="checkbox-item">
3315
+ <input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}${disabledAttr}>
3316
+ <label for="${checkboxId}">${escapeHtml(opt.label)}</label>
3317
+ </div>`;
3318
+ }).join("\n ")}
3319
+ </div>`;
3320
+ }
3321
+ /**
3322
+ * Render checkboxes field based on mode.
3323
+ */
3324
+ function renderCheckboxesInput(field, value, disabledAttr) {
3325
+ const checkboxValues = value?.values ?? {};
3326
+ const mode = field.checkboxMode ?? "multi";
3327
+ if (mode === "simple") return `<div class="checkbox-group">
3328
+ ${field.options.map((opt) => {
3329
+ const checkedAttr = checkboxValues[opt.id] === "done" ? " checked" : "";
3330
+ const checkboxId = `field-${field.id}-${opt.id}`;
3331
+ return `<div class="checkbox-item">
3332
+ <input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}${disabledAttr}>
3333
+ <label for="${checkboxId}">${escapeHtml(opt.label)}</label>
3334
+ </div>`;
3335
+ }).join("\n ")}
3336
+ </div>`;
3337
+ if (mode === "explicit") return `<div class="checkbox-group">
3338
+ ${field.options.map((opt) => {
3339
+ const state = checkboxValues[opt.id] ?? "unfilled";
3340
+ const selectId = `field-${field.id}-${opt.id}`;
3341
+ const selectName = `${field.id}.${opt.id}`;
3342
+ return `<div class="option-row">
3343
+ <span class="option-label">${escapeHtml(opt.label)}</span>
3344
+ <select id="${selectId}" name="${selectName}"${disabledAttr}>
3345
+ <option value="unfilled"${state === "unfilled" ? " selected" : ""}>-- Select --</option>
3346
+ <option value="yes"${state === "yes" ? " selected" : ""}>Yes</option>
3347
+ <option value="no"${state === "no" ? " selected" : ""}>No</option>
3348
+ </select>
3349
+ </div>`;
3350
+ }).join("\n ")}
3351
+ </div>`;
3352
+ return `<div class="checkbox-group">
3353
+ ${field.options.map((opt) => {
3354
+ const state = checkboxValues[opt.id] ?? "todo";
3355
+ const selectId = `field-${field.id}-${opt.id}`;
3356
+ const selectName = `${field.id}.${opt.id}`;
3357
+ return `<div class="option-row">
3358
+ <span class="option-label">${escapeHtml(opt.label)}</span>
3359
+ <select id="${selectId}" name="${selectName}"${disabledAttr}>
3360
+ <option value="todo"${state === "todo" ? " selected" : ""}>To Do</option>
3361
+ <option value="active"${state === "active" ? " selected" : ""}>Active</option>
3362
+ <option value="done"${state === "done" ? " selected" : ""}>Done</option>
3363
+ <option value="incomplete"${state === "incomplete" ? " selected" : ""}>Incomplete</option>
3364
+ <option value="na"${state === "na" ? " selected" : ""}>N/A</option>
3365
+ </select>
3366
+ </div>`;
3367
+ }).join("\n ")}
3368
+ </div>`;
3369
+ }
3370
+ /**
3371
+ * Escape HTML special characters.
3372
+ * @public Exported for testing.
3373
+ */
3374
+ function escapeHtml(str) {
3375
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3376
+ }
3377
+ /** Common styles for read-only viewers */
3378
+ const READ_ONLY_STYLES = `
3379
+ * { box-sizing: border-box; }
3380
+ body {
3381
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3382
+ line-height: 1.6;
3383
+ max-width: 900px;
3384
+ margin: 0 auto;
3385
+ padding: 2rem;
3386
+ background: #f8f9fa;
3387
+ color: #212529;
3388
+ }
3389
+ h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; font-size: 1.5rem; }
3390
+ .content {
3391
+ background: white;
3392
+ border-radius: 8px;
3393
+ padding: 1.5rem;
3394
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
3395
+ }
3396
+ pre {
3397
+ background: #1e1e1e;
3398
+ color: #d4d4d4;
3399
+ padding: 1rem;
3400
+ border-radius: 6px;
3401
+ overflow-x: auto;
3402
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
3403
+ font-size: 0.9rem;
3404
+ line-height: 1.5;
3405
+ }
3406
+ .badge {
3407
+ font-size: 0.75rem;
3408
+ padding: 0.2rem 0.5rem;
3409
+ background: #e9ecef;
3410
+ border-radius: 4px;
3411
+ color: #6c757d;
3412
+ margin-left: 0.75rem;
3413
+ font-weight: normal;
3414
+ }
3415
+ `;
3416
+ /**
3417
+ * Render markdown content as read-only HTML.
3418
+ * Simple rendering without full markdown parsing.
3419
+ */
3420
+ function renderMarkdownHtml(content, filename) {
3421
+ const lines = content.split("\n");
3422
+ let html = "";
3423
+ let inParagraph = false;
3424
+ for (const line of lines) {
3425
+ const trimmed = line.trim();
3426
+ if (trimmed.startsWith("# ")) {
3427
+ if (inParagraph) {
3428
+ html += "</p>";
3429
+ inParagraph = false;
3430
+ }
3431
+ html += `<h2>${escapeHtml(trimmed.slice(2))}</h2>`;
3432
+ } else if (trimmed.startsWith("## ")) {
3433
+ if (inParagraph) {
3434
+ html += "</p>";
3435
+ inParagraph = false;
3436
+ }
3437
+ html += `<h3>${escapeHtml(trimmed.slice(3))}</h3>`;
3438
+ } else if (trimmed.startsWith("### ")) {
3439
+ if (inParagraph) {
3440
+ html += "</p>";
3441
+ inParagraph = false;
3442
+ }
3443
+ html += `<h4>${escapeHtml(trimmed.slice(4))}</h4>`;
3444
+ } else if (trimmed === "") {
3445
+ if (inParagraph) {
3446
+ html += "</p>";
3447
+ inParagraph = false;
3448
+ }
3449
+ } else {
3450
+ if (!inParagraph) {
3451
+ html += "<p>";
3452
+ inParagraph = true;
3453
+ } else html += "<br>";
3454
+ html += escapeHtml(trimmed);
3455
+ }
3456
+ }
3457
+ if (inParagraph) html += "</p>";
3458
+ return `<!DOCTYPE html>
3459
+ <html lang="en">
3460
+ <head>
3461
+ <meta charset="UTF-8">
3462
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3463
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3464
+ <style>${READ_ONLY_STYLES}
3465
+ h2 { color: #495057; font-size: 1.3rem; margin-top: 1.5rem; }
3466
+ h3 { color: #6c757d; font-size: 1.1rem; margin-top: 1.25rem; }
3467
+ h4 { color: #6c757d; font-size: 1rem; margin-top: 1rem; }
3468
+ p { margin: 0.75rem 0; }
3469
+ </style>
3470
+ </head>
3471
+ <body>
3472
+ <h1>${escapeHtml(filename)}<span class="badge">Markdown</span></h1>
3473
+ <div class="content">
3474
+ ${html}
3475
+ </div>
3476
+ </body>
3477
+ </html>`;
3478
+ }
3479
+ /**
3480
+ * Render YAML content with syntax highlighting.
3481
+ */
3482
+ function renderYamlHtml(content, filename) {
3483
+ const highlighted = content.split("\n").map((line) => {
3484
+ const colonIndex = line.indexOf(":");
3485
+ if (colonIndex > 0 && !line.trim().startsWith("#") && !line.trim().startsWith("-")) return `<span style="color:#9cdcfe">${escapeHtml(line.slice(0, colonIndex))}</span>${escapeHtml(line.slice(colonIndex))}`;
3486
+ if (line.trim().startsWith("#")) return `<span style="color:#6a9955">${escapeHtml(line)}</span>`;
3487
+ return escapeHtml(line);
3488
+ }).join("\n");
3489
+ return `<!DOCTYPE html>
3490
+ <html lang="en">
3491
+ <head>
3492
+ <meta charset="UTF-8">
3493
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3494
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3495
+ <style>${READ_ONLY_STYLES}</style>
3496
+ </head>
3497
+ <body>
3498
+ <h1>${escapeHtml(filename)}<span class="badge">YAML</span></h1>
3499
+ <div class="content">
3500
+ <pre>${highlighted}</pre>
3501
+ </div>
3502
+ </body>
3503
+ </html>`;
3504
+ }
3505
+ /**
3506
+ * Render JSON content with syntax highlighting and formatting.
3507
+ */
3508
+ function renderJsonHtml(content, filename) {
3509
+ let formatted;
3510
+ try {
3511
+ const parsed = JSON.parse(content);
3512
+ formatted = JSON.stringify(parsed, null, 2);
3513
+ } catch {
3514
+ formatted = content;
3515
+ }
3516
+ const highlighted = formatted.replace(/"([^"]+)":/g, "<span style=\"color:#9cdcfe\">\"$1\"</span>:").replace(/: "([^"]+)"/g, ": <span style=\"color:#ce9178\">\"$1\"</span>").replace(/: (\d+\.?\d*)/g, ": <span style=\"color:#b5cea8\">$1</span>").replace(/: (true|false|null)/g, ": <span style=\"color:#569cd6\">$1</span>");
3517
+ return `<!DOCTYPE html>
3518
+ <html lang="en">
3519
+ <head>
3520
+ <meta charset="UTF-8">
3521
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3522
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3523
+ <style>${READ_ONLY_STYLES}</style>
3524
+ </head>
3525
+ <body>
3526
+ <h1>${escapeHtml(filename)}<span class="badge">JSON</span></h1>
3527
+ <div class="content">
3528
+ <pre>${highlighted}</pre>
3529
+ </div>
3530
+ </body>
3531
+ </html>`;
3532
+ }
3533
+ /**
3534
+ * Render plain text content.
3535
+ */
3536
+ function renderPlainTextHtml(content, filename) {
3537
+ return `<!DOCTYPE html>
3538
+ <html lang="en">
3539
+ <head>
3540
+ <meta charset="UTF-8">
3541
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3542
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3543
+ <style>${READ_ONLY_STYLES}</style>
3544
+ </head>
3545
+ <body>
3546
+ <h1>${escapeHtml(filename)}<span class="badge">Text</span></h1>
3547
+ <div class="content">
3548
+ <pre>${escapeHtml(content)}</pre>
3549
+ </div>
3550
+ </body>
3551
+ </html>`;
3552
+ }
3553
+
3554
+ //#endregion
3555
+ //#region src/cli/commands/render.ts
3556
+ /**
3557
+ * Generate default output path by replacing .form.md with .form.html.
3558
+ */
3559
+ function getDefaultOutputPath(inputPath) {
3560
+ const dir = dirname(inputPath);
3561
+ const base = basename(inputPath);
3562
+ const newBase = base.replace(/\.form\.md$/i, ".form.html");
3563
+ if (newBase === base) return `${inputPath}.html`;
3564
+ return resolve(dir, newBase);
3565
+ }
3566
+ /**
3567
+ * Register the render command.
3568
+ */
3569
+ function registerRenderCommand(program) {
3570
+ program.command("render <file>").description("Render a form as static HTML output").option("-o, --output <path>", "Output file path (default: same stem + .html)").action(async (file, options, cmd) => {
3571
+ const ctx = getCommandContext(cmd);
3572
+ const filePath = resolve(file);
3573
+ const outputPath = options.output ? resolve(options.output) : getDefaultOutputPath(filePath);
3574
+ try {
3575
+ logVerbose(ctx, `Reading file: ${filePath}`);
3576
+ const content = await readFile$1(filePath);
3577
+ logVerbose(ctx, "Parsing form...");
3578
+ const form = parseForm(content);
3579
+ logVerbose(ctx, "Rendering HTML...");
3580
+ const html = renderFormHtml(form);
3581
+ if (ctx.dryRun) {
3582
+ logDryRun(`Would write HTML to: ${outputPath}`);
3583
+ return;
3584
+ }
3585
+ await writeFile(outputPath, html);
3586
+ logSuccess(ctx, pc.green(`✓ Rendered to ${outputPath}`));
3587
+ } catch (error) {
3588
+ logError(error instanceof Error ? error.message : String(error));
3589
+ process.exit(1);
3590
+ }
3591
+ });
3592
+ }
3593
+
3594
+ //#endregion
3595
+ //#region src/cli/lib/initialValues.ts
3596
+ /**
3597
+ * Parse initial value inputs from CLI flags.
3598
+ *
3599
+ * Supports formats:
3600
+ * - fieldId=value (string values)
3601
+ * - fieldId:number=123 (explicit number)
3602
+ * - fieldId:list=a,b,c (comma-separated list)
3603
+ *
3604
+ * @param inputs Array of input strings in "fieldId=value" format
3605
+ * @returns Array of patches to apply as initial values
3606
+ */
3607
+ function parseInitialValues(inputs) {
3608
+ const patches = [];
3609
+ for (const input of inputs) {
3610
+ const equalsIndex = input.indexOf("=");
3611
+ if (equalsIndex === -1) throw new Error(`Invalid input format: "${input}" (expected "fieldId=value")`);
3612
+ const fieldSpec = input.slice(0, equalsIndex);
3613
+ const value = input.slice(equalsIndex + 1);
3614
+ const colonIndex = fieldSpec.indexOf(":");
3615
+ let fieldId;
3616
+ let type = null;
3617
+ if (colonIndex !== -1) {
3618
+ fieldId = fieldSpec.slice(0, colonIndex);
3619
+ type = fieldSpec.slice(colonIndex + 1).toLowerCase();
3620
+ } else fieldId = fieldSpec;
3621
+ if (type === "number") {
3622
+ const numValue = parseFloat(value);
3623
+ if (isNaN(numValue)) throw new Error(`Invalid number value for "${fieldId}": "${value}"`);
3624
+ patches.push({
3625
+ op: "set_number",
3626
+ fieldId,
3627
+ value: numValue
3628
+ });
3629
+ } else if (type === "list") {
3630
+ const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3631
+ patches.push({
3632
+ op: "set_string_list",
3633
+ fieldId,
3634
+ items
3635
+ });
3636
+ } else patches.push({
3637
+ op: "set_string",
3638
+ fieldId,
3639
+ value
3640
+ });
3641
+ }
3642
+ return patches;
3643
+ }
3644
+ /**
3645
+ * Validate that all initial values reference valid field IDs.
3646
+ *
3647
+ * @param patches Patches to validate
3648
+ * @param validFieldIds Set of valid field IDs from the form
3649
+ * @returns Array of invalid field IDs (empty if all valid)
3650
+ */
3651
+ function validateInitialValueFields(patches, validFieldIds) {
3652
+ const invalid = [];
3653
+ for (const patch of patches) if ("fieldId" in patch && patch.fieldId && !validFieldIds.has(patch.fieldId)) invalid.push(patch.fieldId);
3654
+ return invalid;
3655
+ }
3656
+
3657
+ //#endregion
3658
+ //#region src/cli/commands/research.ts
3659
+ /**
3660
+ * Register the research command.
3661
+ */
3662
+ function registerResearchCommand(program) {
3663
+ program.command("research <input>").description("Fill a form using a web-search-enabled model").option("--model <provider/model>", "LLM model to use (e.g., google/gemini-2.5-flash). Required.").option("--output <path>", "Output path for filled form (default: auto-generated in forms directory)").option("--input <fieldId=value>", "Set initial field value (can be used multiple times)", (value, previous) => previous.concat([value]), []).option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN})`, String(DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues per turn (default: ${DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN})`, String(DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN)).option("--transcript", "Save session transcript").action(async (input, options, cmd) => {
3664
+ const ctx = getCommandContext(cmd);
3665
+ const startTime = Date.now();
3666
+ try {
3667
+ if (!options.model) {
3668
+ logError("--model is required");
3669
+ console.log("");
3670
+ console.log(formatSuggestedLlms());
3671
+ process.exit(1);
3672
+ }
3673
+ const modelId = options.model;
3674
+ if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
3675
+ const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
3676
+ logError(`Model "${modelId}" does not support web search.`);
3677
+ console.log("");
3678
+ console.log(pc.yellow("Research forms require web search capabilities."));
3679
+ console.log(`Use a model from: ${webSearchProviders.join(", ")}`);
3680
+ console.log("");
3681
+ console.log("Examples:");
3682
+ console.log(" --model openai/gpt-5-mini");
3683
+ console.log(" --model anthropic/claude-sonnet-4-5");
3684
+ console.log(" --model google/gemini-2.5-flash");
3685
+ console.log(" --model xai/grok-4");
3686
+ process.exit(1);
3687
+ }
3688
+ const inputPath = resolve(input);
3689
+ logVerbose(ctx, `Input: ${inputPath}`);
3690
+ const form = parseForm(await readFile$1(inputPath));
3691
+ logVerbose(ctx, `Parsed form: ${form.schema.id}`);
3692
+ const initialInputs = options.input ?? [];
3693
+ if (initialInputs.length > 0) {
3694
+ const patches = parseInitialValues(initialInputs);
3695
+ const invalidFields = validateInitialValueFields(patches, new Set(form.orderIndex));
3696
+ if (invalidFields.length > 0) logWarn(ctx, `Unknown field IDs: ${invalidFields.join(", ")}`);
3697
+ applyPatches(form, patches);
3698
+ logInfo(ctx, `Applied ${patches.length} initial value(s)`);
3699
+ }
3700
+ const formsDir = getFormsDir(ctx.formsDir);
3701
+ let outputPath;
3702
+ if (options.output) outputPath = resolve(options.output);
3703
+ else outputPath = generateVersionedPathInFormsDir(inputPath, formsDir);
3704
+ logVerbose(ctx, `Output: ${outputPath}`);
3705
+ const maxTurns = parseInt(options.maxTurns, 10);
3706
+ const maxPatchesPerTurn = parseInt(options.maxPatches, 10);
3707
+ const maxIssuesPerTurn = parseInt(options.maxIssues, 10);
3708
+ logInfo(ctx, `Research fill with model: ${modelId}`);
3709
+ logVerbose(ctx, `Max turns: ${maxTurns}`);
3710
+ logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
3711
+ logVerbose(ctx, `Max issues/turn: ${maxIssuesPerTurn}`);
3712
+ const result = await runResearch(form, {
3713
+ model: modelId,
3714
+ maxTurns,
3715
+ maxPatchesPerTurn,
3716
+ maxIssuesPerTurn,
3717
+ targetRoles: [AGENT_ROLE],
3718
+ fillMode: "continue"
3719
+ });
3720
+ if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
3721
+ logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
3722
+ logInfo(ctx, `Turns: ${result.totalTurns}`);
3723
+ if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
3724
+ const { formPath, rawPath, yamlPath } = await exportMultiFormat(result.form, outputPath);
3725
+ logSuccess(ctx, "Outputs:");
3726
+ console.log(` ${formPath} ${pc.dim("(markform)")}`);
3727
+ console.log(` ${rawPath} ${pc.dim("(plain markdown)")}`);
3728
+ console.log(` ${yamlPath} ${pc.dim("(values as YAML)")}`);
3729
+ if (options.transcript && result.transcript) {
3730
+ const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
3731
+ const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
3732
+ const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
3733
+ await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
3734
+ logInfo(ctx, `Transcript: ${transcriptPath}`);
3735
+ }
3736
+ logTiming(ctx, "Research fill", Date.now() - startTime);
3737
+ } catch (error) {
3738
+ logError(error instanceof Error ? error.message : String(error));
3739
+ process.exit(1);
3740
+ }
3741
+ });
3742
+ }
3743
+
3744
+ //#endregion
3745
+ //#region src/cli/commands/validate.ts
3746
+ /**
3747
+ * Format state badge for console output.
3748
+ */
3749
+ function formatState(state, useColors) {
3750
+ const [text, colorFn] = {
3751
+ complete: ["✓ complete", pc.green],
3752
+ incomplete: ["○ incomplete", pc.yellow],
3753
+ empty: ["◌ empty", pc.dim],
3754
+ invalid: ["✗ invalid", pc.red]
3755
+ }[state] ?? [state, (s) => s];
3756
+ return useColors ? colorFn(text) : text;
3757
+ }
3758
+ /**
3759
+ * Format priority badge for console output.
3760
+ */
3761
+ function formatPriority(priority, useColors) {
3762
+ const label = `P${priority}`;
3763
+ if (!useColors) return label;
3764
+ switch (priority) {
3765
+ case 1: return pc.red(pc.bold(label));
3766
+ case 2: return pc.yellow(label);
3767
+ case 3: return pc.cyan(label);
3768
+ case 4: return pc.blue(label);
3769
+ case 5:
3770
+ default: return pc.dim(label);
3771
+ }
3772
+ }
3773
+ /**
3774
+ * Format severity badge for console output.
3775
+ */
3776
+ function formatSeverity(severity, useColors) {
3777
+ if (!useColors) return severity;
3778
+ return severity === "required" ? pc.red(severity) : pc.yellow(severity);
3779
+ }
3780
+ /**
3781
+ * Format validate report for console output.
3782
+ */
3783
+ function formatConsoleReport(report, useColors) {
3784
+ const lines = [];
3785
+ const bold = useColors ? pc.bold : (s) => s;
3786
+ const dim = useColors ? pc.dim : (s) => s;
3787
+ const cyan = useColors ? pc.cyan : (s) => s;
3788
+ lines.push(bold(cyan("Form Validation Report")));
3789
+ if (report.title) lines.push(`${bold("Title:")} ${report.title}`);
3790
+ lines.push("");
3791
+ lines.push(`${bold("Form State:")} ${formatState(report.form_state, useColors)}`);
3792
+ lines.push("");
3793
+ const structure = report.structure;
3794
+ lines.push(bold("Structure:"));
3795
+ lines.push(` Groups: ${structure.groupCount}`);
3796
+ lines.push(` Fields: ${structure.fieldCount}`);
3797
+ lines.push(` Options: ${structure.optionCount}`);
3798
+ lines.push("");
3799
+ const progress = report.progress;
3800
+ lines.push(bold("Progress:"));
3801
+ lines.push(` Total fields: ${progress.counts.totalFields}`);
3802
+ lines.push(` Required: ${progress.counts.requiredFields}`);
3803
+ lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
3804
+ lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
3805
+ lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
3806
+ lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
3807
+ lines.push("");
3808
+ if (report.issues.length > 0) {
3809
+ lines.push(bold(`Issues (${report.issues.length}):`));
3810
+ for (const issue of report.issues) {
3811
+ const priority = formatPriority(issue.priority, useColors);
3812
+ const severity = formatSeverity(issue.severity, useColors);
3813
+ lines.push(` ${priority} (${severity}) ${dim(`[${issue.scope}]`)} ${dim(issue.ref)}: ${issue.message}`);
3814
+ }
3815
+ } else lines.push(dim("No issues found."));
3816
+ return lines.join("\n");
3817
+ }
3818
+ /**
3819
+ * Register the validate command.
3820
+ */
3821
+ function registerValidateCommand(program) {
3822
+ program.command("validate <file>").description("Validate a form and display summary and issues (no form content)").action(async (file, _options, cmd) => {
3823
+ const ctx = getCommandContext(cmd);
3824
+ try {
3825
+ logVerbose(ctx, `Reading file: ${file}`);
3826
+ const content = await readFile$1(file);
3827
+ logVerbose(ctx, "Parsing form...");
3828
+ const form = parseForm(content);
3829
+ logVerbose(ctx, "Running validation...");
3830
+ const result = inspect(form);
3831
+ const output = formatOutput(ctx, {
3832
+ title: form.schema.title,
3833
+ structure: result.structureSummary,
3834
+ progress: result.progressSummary,
3835
+ form_state: result.formState,
3836
+ issues: result.issues.map((issue) => ({
3837
+ ref: issue.ref,
3838
+ scope: issue.scope,
3839
+ reason: issue.reason,
3840
+ message: issue.message,
3841
+ priority: issue.priority,
3842
+ severity: issue.severity
3843
+ }))
3844
+ }, (data, useColors) => formatConsoleReport(data, useColors));
3845
+ console.log(output);
3846
+ } catch (error) {
3847
+ logError(error instanceof Error ? error.message : String(error));
3848
+ process.exit(1);
3849
+ }
3850
+ });
3851
+ }
3852
+
3853
+ //#endregion
3854
+ //#region src/cli/cli.ts
3855
+ /**
3856
+ * CLI implementation for markform.
3857
+ *
3858
+ * Provides commands for inspecting, applying patches, exporting,
3859
+ * serving, and running harness loops on .form.md files.
3860
+ */
3861
+ /**
3862
+ * Configure Commander with colored help text and global options display.
3863
+ */
3864
+ function withColoredHelp(cmd) {
3865
+ cmd.configureHelp({
3866
+ styleTitle: (str) => pc.bold(pc.cyan(str)),
3867
+ styleCommandText: (str) => pc.green(str),
3868
+ styleOptionText: (str) => pc.yellow(str),
3869
+ showGlobalOptions: true
3870
+ });
3871
+ return cmd;
3872
+ }
3873
+ /**
3874
+ * Create and configure the CLI program.
3875
+ */
3876
+ function createProgram() {
3877
+ const program = withColoredHelp(new Command());
3878
+ program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION).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})`);
3879
+ registerReadmeCommand(program);
3880
+ registerDocsCommand(program);
3881
+ registerSpecCommand(program);
3882
+ registerApplyCommand(program);
3883
+ registerDumpCommand(program);
3884
+ registerExamplesCommand(program);
3885
+ registerExportCommand(program);
3886
+ registerFillCommand(program);
3887
+ registerInspectCommand(program);
3888
+ registerModelsCommand(program);
3889
+ registerRenderCommand(program);
3890
+ registerReportCommand(program);
3891
+ registerResearchCommand(program);
3892
+ registerServeCommand(program);
3893
+ registerValidateCommand(program);
3894
+ return program;
3895
+ }
3896
+ /**
3897
+ * Run the CLI.
3898
+ */
3899
+ async function runCli() {
3900
+ await createProgram().parseAsync(process.argv);
3901
+ }
3902
+
3903
+ //#endregion
3904
+ export { runCli as t };