markform 0.1.2 → 0.1.4

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 (35) hide show
  1. package/README.md +97 -42
  2. package/dist/ai-sdk.d.mts +2 -2
  3. package/dist/ai-sdk.mjs +5 -5
  4. package/dist/{apply-BfAGTHMh.mjs → apply-C54EMAJ1.mjs} +383 -26
  5. package/dist/bin.mjs +6 -6
  6. package/dist/{cli-B3NVm6zL.mjs → cli-BhWhn6L9.mjs} +456 -141
  7. package/dist/cli.mjs +6 -6
  8. package/dist/{coreTypes-BXhhz9Iq.d.mts → coreTypes-cbNTYAcb.d.mts} +1878 -325
  9. package/dist/{coreTypes-Dful87E0.mjs → coreTypes-pyctKRgc.mjs} +79 -5
  10. package/dist/index.d.mts +146 -9
  11. package/dist/index.mjs +5 -5
  12. package/dist/session-B_stoXQn.mjs +4 -0
  13. package/dist/{session-Bqnwi9wp.mjs → session-uF0e6m6k.mjs} +9 -5
  14. package/dist/{shared-N_s1M-_K.mjs → shared-BqPnYXrn.mjs} +82 -1
  15. package/dist/shared-CZsyShck.mjs +3 -0
  16. package/dist/{src-BXRkGFpG.mjs → src-BNh7Cx9P.mjs} +801 -121
  17. package/docs/markform-apis.md +194 -0
  18. package/{DOCS.md → docs/markform-reference.md} +111 -50
  19. package/{SPEC.md → docs/markform-spec.md} +342 -91
  20. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +196 -141
  21. package/examples/earnings-analysis/earnings-analysis.form.md +236 -226
  22. package/examples/movie-research/movie-research-basic.form.md +25 -21
  23. package/examples/movie-research/movie-research-deep.form.md +74 -62
  24. package/examples/movie-research/movie-research-minimal.form.md +29 -34
  25. package/examples/simple/simple-mock-filled.form.md +93 -29
  26. package/examples/simple/simple-skipped-filled.form.md +91 -29
  27. package/examples/simple/simple-with-skips.session.yaml +93 -25
  28. package/examples/simple/simple.form.md +74 -20
  29. package/examples/simple/simple.session.yaml +98 -25
  30. package/examples/startup-deep-research/startup-deep-research.form.md +108 -81
  31. package/examples/startup-research/startup-research-mock-filled.form.md +43 -43
  32. package/examples/startup-research/startup-research.form.md +24 -24
  33. package/package.json +18 -27
  34. package/dist/session-DdAtY2Ni.mjs +0 -4
  35. package/dist/shared-D7gf27Tr.mjs +0 -3
@@ -1,19 +1,113 @@
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";
1
+ import { L as PatchSchema } from "./coreTypes-pyctKRgc.mjs";
2
+ import { A as parseRolesFlag, D as deriveReportPath, E as deriveExportPath, F as hasWebSearchSupport, I as parseModelIdForDisplay, M as WEB_SEARCH_CONFIG, N as formatSuggestedLlms, O as detectFileType, T as USER_ROLE, _ as DEFAULT_MAX_TURNS, b as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, d as serializeRawMarkdown, f as serializeReportMarkdown, g as DEFAULT_MAX_PATCHES_PER_TURN, h as DEFAULT_MAX_ISSUES_PER_TURN, j as SUGGESTED_LLMS, k as getFormsDir, m as DEFAULT_FORMS_DIR, p as AGENT_ROLE, r as inspect, t as applyPatches, u as serialize, v as DEFAULT_PORT, w as REPORT_EXTENSION, x as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN } from "./apply-C54EMAJ1.mjs";
3
+ import { a as resolveHarnessConfig, c as getProviderNames, f as createMockAgent, i as runResearch, l as resolveModel, m as createHarness, s as getProviderInfo, t as VERSION, u as createLiveAgent, w as parseForm } from "./src-BNh7Cx9P.mjs";
4
+ import { n as serializeSession } from "./session-uF0e6m6k.mjs";
5
+ import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-BqPnYXrn.mjs";
6
6
  import YAML from "yaml";
7
7
  import { basename, dirname, join, resolve } from "node:path";
8
8
  import { Command } from "commander";
9
9
  import pc from "picocolors";
10
- import { readFile } from "node:fs/promises";
11
10
  import { existsSync, readFileSync } from "node:fs";
12
11
  import { fileURLToPath } from "node:url";
12
+ import { readFile } from "node:fs/promises";
13
13
  import * as p from "@clack/prompts";
14
14
  import { exec, spawn } from "node:child_process";
15
15
  import { createServer } from "node:http";
16
16
 
17
+ //#region src/cli/commands/apis.ts
18
+ /**
19
+ * Get the path to the markform-apis.md file.
20
+ * Works both during development and when installed as a package.
21
+ */
22
+ function getApisPath() {
23
+ const thisDir = dirname(fileURLToPath(import.meta.url));
24
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-apis.md");
25
+ return join(dirname(dirname(dirname(thisDir))), "docs", "markform-apis.md");
26
+ }
27
+ /**
28
+ * Load the APIs documentation content.
29
+ */
30
+ function loadApis() {
31
+ const apisPath = getApisPath();
32
+ try {
33
+ return readFileSync(apisPath, "utf-8");
34
+ } catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ throw new Error(`Failed to load API docs from ${apisPath}: ${message}`);
37
+ }
38
+ }
39
+ /**
40
+ * Apply basic terminal formatting to markdown content.
41
+ * Colorizes headers, code blocks, and other elements for better readability.
42
+ */
43
+ function formatMarkdown$4(content, useColors) {
44
+ if (!useColors) return content;
45
+ const lines = content.split("\n");
46
+ const formatted = [];
47
+ let inCodeBlock = false;
48
+ for (const line of lines) {
49
+ if (line.startsWith("```")) {
50
+ inCodeBlock = !inCodeBlock;
51
+ formatted.push(pc.dim(line));
52
+ continue;
53
+ }
54
+ if (inCodeBlock) {
55
+ formatted.push(pc.dim(line));
56
+ continue;
57
+ }
58
+ if (line.startsWith("# ")) {
59
+ formatted.push(pc.bold(pc.cyan(line)));
60
+ continue;
61
+ }
62
+ if (line.startsWith("## ")) {
63
+ formatted.push(pc.bold(pc.blue(line)));
64
+ continue;
65
+ }
66
+ if (line.startsWith("### ")) {
67
+ formatted.push(pc.bold(line));
68
+ continue;
69
+ }
70
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
71
+ return pc.yellow(code);
72
+ });
73
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
74
+ return pc.bold(text);
75
+ });
76
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
77
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
78
+ });
79
+ formatted.push(formattedLine);
80
+ }
81
+ return formatted.join("\n");
82
+ }
83
+ /**
84
+ * Check if stdout is an interactive terminal.
85
+ */
86
+ function isInteractive$4() {
87
+ return process.stdout.isTTY === true;
88
+ }
89
+ /**
90
+ * Display content.
91
+ */
92
+ function displayContent$3(content) {
93
+ console.log(content);
94
+ }
95
+ /**
96
+ * Register the apis command.
97
+ */
98
+ function registerApisCommand(program) {
99
+ program.command("apis").description("Display Markform TypeScript and AI SDK API documentation").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
100
+ const ctx = getCommandContext(cmd);
101
+ try {
102
+ displayContent$3(formatMarkdown$4(loadApis(), !options.raw && isInteractive$4() && !ctx.quiet));
103
+ } catch (error) {
104
+ logError(error instanceof Error ? error.message : String(error));
105
+ process.exit(1);
106
+ }
107
+ });
108
+ }
109
+
110
+ //#endregion
17
111
  //#region src/cli/commands/apply.ts
18
112
  /**
19
113
  * Format state badge for console output.
@@ -146,13 +240,13 @@ function registerApplyCommand(program) {
146
240
  //#endregion
147
241
  //#region src/cli/commands/docs.ts
148
242
  /**
149
- * Get the path to the DOCS.md file.
243
+ * Get the path to the markform-reference.md file.
150
244
  * Works both during development and when installed as a package.
151
245
  */
152
246
  function getDocsPath() {
153
247
  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");
248
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-reference.md");
249
+ return join(dirname(dirname(dirname(thisDir))), "docs", "markform-reference.md");
156
250
  }
157
251
  /**
158
252
  * Load the docs content.
@@ -163,7 +257,7 @@ function loadDocs() {
163
257
  return readFileSync(docsPath, "utf-8");
164
258
  } catch (error) {
165
259
  const message = error instanceof Error ? error.message : String(error);
166
- throw new Error(`Failed to load DOCS from ${docsPath}: ${message}`);
260
+ throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);
167
261
  }
168
262
  }
169
263
  /**
@@ -334,23 +428,29 @@ function toNotesArray(form) {
334
428
  * Derive export paths from a base form path.
335
429
  * Uses centralized extension constants from settings.ts.
336
430
  *
431
+ * Standard exports: report, values (yaml), form.
432
+ * Raw markdown is available via CLI but not in standard exports.
433
+ *
337
434
  * @param basePath - Path to the .form.md file
338
435
  * @returns Object with paths for all export formats
339
436
  */
340
437
  function deriveExportPaths(basePath) {
341
438
  return {
342
- formPath: deriveExportPath(basePath, "form"),
343
- rawPath: deriveExportPath(basePath, "raw"),
344
- yamlPath: deriveExportPath(basePath, "yaml")
439
+ reportPath: deriveReportPath(basePath),
440
+ yamlPath: deriveExportPath(basePath, "yaml"),
441
+ formPath: deriveExportPath(basePath, "form")
345
442
  };
346
443
  }
347
444
  /**
348
445
  * Export form to multiple formats.
349
446
  *
350
- * Writes:
447
+ * Standard exports:
448
+ * - Report format (.report.md) - filtered markdown (excludes instructions, report=false)
449
+ * - YAML values (.yml) - structured format with state and notes
351
450
  * - 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)
451
+ *
452
+ * Note: Raw markdown (.raw.md) is available via CLI `markform export --raw`
453
+ * but is not included in standard multi-format export.
354
454
  *
355
455
  * @param form - The parsed form to export
356
456
  * @param basePath - Base path for the .form.md file (other paths are derived)
@@ -358,10 +458,8 @@ function deriveExportPaths(basePath) {
358
458
  */
359
459
  async function exportMultiFormat(form, basePath) {
360
460
  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);
461
+ const reportContent = serializeReportMarkdown(form);
462
+ await writeFile(paths.reportPath, reportContent);
365
463
  const values = toStructuredValues(form);
366
464
  const notes = toNotesArray(form);
367
465
  const exportData = {
@@ -370,6 +468,8 @@ async function exportMultiFormat(form, basePath) {
370
468
  };
371
469
  const yamlContent = YAML.stringify(exportData);
372
470
  await writeFile(paths.yamlPath, yamlContent);
471
+ const formContent = serialize(form);
472
+ await writeFile(paths.formPath, formContent);
373
473
  return paths;
374
474
  }
375
475
 
@@ -474,6 +574,7 @@ function formatPatchValue(patch) {
474
574
  case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
475
575
  case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
476
576
  case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
577
+ case "set_table": return patch.rows.length > 0 ? truncate(`[${patch.rows.length} rows]`) : "(empty)";
477
578
  case "add_note": return truncate(`note: ${patch.text}`);
478
579
  case "remove_note": return `(remove note ${patch.noteId})`;
479
580
  }
@@ -496,6 +597,7 @@ function formatPatchType(patch) {
496
597
  case "set_url_list": return "url_list";
497
598
  case "set_date": return "date";
498
599
  case "set_year": return "year";
600
+ case "set_table": return "table";
499
601
  case "add_note": return "note";
500
602
  case "remove_note": return "remove_note";
501
603
  }
@@ -844,9 +946,10 @@ async function promptSkipOrFill(ctx) {
844
946
  async function promptForString(ctx) {
845
947
  const field = ctx.field;
846
948
  const currentVal = ctx.currentValue?.kind === "string" ? ctx.currentValue.value : null;
949
+ const placeholderText = field.placeholder ?? currentVal ?? (ctx.description ? ctx.description.slice(0, 60) : void 0);
847
950
  const result = await p.text({
848
951
  message: formatFieldLabel(ctx),
849
- placeholder: currentVal ?? (ctx.description ? ctx.description.slice(0, 60) : void 0),
952
+ placeholder: placeholderText,
850
953
  initialValue: currentVal ?? "",
851
954
  validate: (value) => {
852
955
  if (field.required && !value.trim()) return "This field is required";
@@ -869,9 +972,10 @@ async function promptForString(ctx) {
869
972
  async function promptForNumber(ctx) {
870
973
  const field = ctx.field;
871
974
  const currentVal = ctx.currentValue?.kind === "number" ? ctx.currentValue.value : null;
975
+ const placeholderText = field.placeholder ?? (currentVal !== null ? String(currentVal) : void 0);
872
976
  const result = await p.text({
873
977
  message: formatFieldLabel(ctx),
874
- placeholder: currentVal !== null ? String(currentVal) : void 0,
978
+ placeholder: placeholderText,
875
979
  initialValue: currentVal !== null ? String(currentVal) : "",
876
980
  validate: (value) => {
877
981
  if (field.required && !value.trim()) return "This field is required";
@@ -897,7 +1001,10 @@ async function promptForNumber(ctx) {
897
1001
  async function promptForStringList(ctx) {
898
1002
  const field = ctx.field;
899
1003
  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.";
1004
+ let hint;
1005
+ if (field.placeholder) hint = `${field.placeholder} (one item per line)`;
1006
+ else if (ctx.description) hint = `${ctx.description.slice(0, 50)}... (one item per line)`;
1007
+ else hint = "Enter items, one per line. Press Enter twice when done.";
901
1008
  const result = await p.text({
902
1009
  message: formatFieldLabel(ctx),
903
1010
  placeholder: hint,
@@ -1071,9 +1178,10 @@ async function promptForCheckboxes(ctx) {
1071
1178
  async function promptForUrl(ctx) {
1072
1179
  const field = ctx.field;
1073
1180
  const currentVal = ctx.currentValue?.kind === "url" ? ctx.currentValue.value : null;
1181
+ const placeholderText = field.placeholder ?? currentVal ?? "https://example.com";
1074
1182
  const result = await p.text({
1075
1183
  message: formatFieldLabel(ctx),
1076
- placeholder: currentVal ?? "https://example.com",
1184
+ placeholder: placeholderText,
1077
1185
  initialValue: currentVal ?? "",
1078
1186
  validate: (value) => {
1079
1187
  if (field.required && !value.trim()) return "This field is required";
@@ -1094,12 +1202,87 @@ async function promptForUrl(ctx) {
1094
1202
  };
1095
1203
  }
1096
1204
  /**
1205
+ * Check if a string is a valid ISO 8601 date (YYYY-MM-DD).
1206
+ */
1207
+ function isValidDate(str) {
1208
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) return false;
1209
+ const date = new Date(str);
1210
+ if (isNaN(date.getTime())) return false;
1211
+ const [year, month, day] = str.split("-").map(Number);
1212
+ return date.getUTCFullYear() === year && date.getUTCMonth() + 1 === month && date.getUTCDate() === day;
1213
+ }
1214
+ /**
1215
+ * Prompt for a date field value.
1216
+ */
1217
+ async function promptForDate(ctx) {
1218
+ const field = ctx.field;
1219
+ const currentVal = ctx.currentValue?.kind === "date" ? ctx.currentValue.value : null;
1220
+ const constraints = [];
1221
+ if (field.min) constraints.push(`min: ${field.min}`);
1222
+ if (field.max) constraints.push(`max: ${field.max}`);
1223
+ const formatHint = constraints.length > 0 ? ` (${constraints.join(", ")})` : "";
1224
+ const result = await p.text({
1225
+ message: formatFieldLabel(ctx),
1226
+ placeholder: currentVal ?? `YYYY-MM-DD${formatHint}`,
1227
+ initialValue: currentVal ?? "",
1228
+ validate: (value) => {
1229
+ if (field.required && !value.trim()) return "This field is required";
1230
+ if (!value.trim()) return;
1231
+ if (!isValidDate(value)) return "Please enter a valid date in YYYY-MM-DD format";
1232
+ if (field.min && value < field.min) return `Date must be on or after ${field.min}`;
1233
+ if (field.max && value > field.max) return `Date must be on or before ${field.max}`;
1234
+ }
1235
+ });
1236
+ if (p.isCancel(result)) return null;
1237
+ if (!result && !field.required) return null;
1238
+ return {
1239
+ op: "set_date",
1240
+ fieldId: field.id,
1241
+ value: result || null
1242
+ };
1243
+ }
1244
+ /** Default year range for validation */
1245
+ const DEFAULT_MIN_YEAR = 1e3;
1246
+ const DEFAULT_MAX_YEAR = 2500;
1247
+ /**
1248
+ * Prompt for a year field value.
1249
+ */
1250
+ async function promptForYear(ctx) {
1251
+ const field = ctx.field;
1252
+ const currentVal = ctx.currentValue?.kind === "year" ? ctx.currentValue.value : null;
1253
+ const minYear = field.min ?? DEFAULT_MIN_YEAR;
1254
+ const maxYear = field.max ?? DEFAULT_MAX_YEAR;
1255
+ const result = await p.text({
1256
+ message: formatFieldLabel(ctx),
1257
+ placeholder: currentVal !== null ? String(currentVal) : `Year (${minYear}-${maxYear})`,
1258
+ initialValue: currentVal !== null ? String(currentVal) : "",
1259
+ validate: (value) => {
1260
+ if (field.required && !value.trim()) return "This field is required";
1261
+ if (!value.trim()) return;
1262
+ const num = Number(value);
1263
+ if (isNaN(num) || !Number.isInteger(num)) return "Please enter a valid year (e.g., 2025)";
1264
+ if (num < minYear) return `Year must be ${minYear} or later`;
1265
+ if (num > maxYear) return `Year must be ${maxYear} or earlier`;
1266
+ }
1267
+ });
1268
+ if (p.isCancel(result)) return null;
1269
+ if (!result && !field.required) return null;
1270
+ return {
1271
+ op: "set_year",
1272
+ fieldId: field.id,
1273
+ value: result ? Number(result) : null
1274
+ };
1275
+ }
1276
+ /**
1097
1277
  * Prompt for a URL list field value.
1098
1278
  */
1099
1279
  async function promptForUrlList(ctx) {
1100
1280
  const field = ctx.field;
1101
1281
  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.";
1282
+ let hint;
1283
+ if (field.placeholder) hint = `${field.placeholder} (one URL per line)`;
1284
+ else if (ctx.description) hint = `${ctx.description.slice(0, 50)}... (one URL per line)`;
1285
+ else hint = "Enter URLs, one per line. Press Enter twice when done.";
1103
1286
  const result = await p.text({
1104
1287
  message: formatFieldLabel(ctx),
1105
1288
  placeholder: hint,
@@ -1126,7 +1309,7 @@ async function promptForUrlList(ctx) {
1126
1309
  };
1127
1310
  }
1128
1311
  /**
1129
- * Prompt user for a single field value based on field type.
1312
+ * Prompt user for a single field value based on field kind.
1130
1313
  * Returns a Patch to set the value, or null if skipped/cancelled.
1131
1314
  *
1132
1315
  * For optional fields, first offers a choice to skip or fill.
@@ -1145,6 +1328,8 @@ async function promptForField(ctx) {
1145
1328
  case "checkboxes": return promptForCheckboxes(ctx);
1146
1329
  case "url": return promptForUrl(ctx);
1147
1330
  case "url_list": return promptForUrlList(ctx);
1331
+ case "date": return promptForDate(ctx);
1332
+ case "year": return promptForYear(ctx);
1148
1333
  default: return null;
1149
1334
  }
1150
1335
  }
@@ -1380,29 +1565,42 @@ async function viewFile(filePath) {
1380
1565
  /**
1381
1566
  * Show an interactive file viewer chooser.
1382
1567
  *
1383
- * Presents a list of files to view with descriptions, plus a Skip option.
1384
- * Loops until the user selects Skip.
1568
+ * Presents a list of files to view:
1569
+ * - "Show report:" for the report output (.report.md) at the top
1570
+ * - "Show source:" for other files (.form.md, .raw.md, .yml)
1571
+ * - "Quit" at the bottom
1572
+ *
1573
+ * Loops until the user selects Quit.
1385
1574
  *
1386
1575
  * @param files Array of file options to display
1387
1576
  */
1388
1577
  async function showFileViewerChooser(files) {
1389
1578
  if (!isInteractive$2()) return;
1390
1579
  console.log("");
1580
+ const reportFile = files.find((f) => f.path.endsWith(".report.md"));
1581
+ const sourceFiles = files.filter((f) => !f.path.endsWith(".report.md"));
1391
1582
  while (true) {
1392
- const options = [...files.map((file) => ({
1583
+ const options = [];
1584
+ if (reportFile) options.push({
1585
+ value: reportFile.path,
1586
+ label: `Show report: ${pc.green(basename(reportFile.path))}`,
1587
+ hint: reportFile.hint ?? ""
1588
+ });
1589
+ for (const file of sourceFiles) options.push({
1393
1590
  value: file.path,
1394
- label: pc.green(basename(file.path)),
1591
+ label: `Show source: ${pc.green(basename(file.path))}`,
1395
1592
  hint: file.hint ?? ""
1396
- })), {
1397
- value: "skip",
1398
- label: "Done viewing",
1399
- hint: "exit file viewer"
1400
- }];
1593
+ });
1594
+ options.push({
1595
+ value: "quit",
1596
+ label: "Quit",
1597
+ hint: ""
1598
+ });
1401
1599
  const selection = await p.select({
1402
- message: "View an output file?",
1600
+ message: "View files:",
1403
1601
  options
1404
1602
  });
1405
- if (p.isCancel(selection) || selection === "skip") break;
1603
+ if (p.isCancel(selection) || selection === "quit") break;
1406
1604
  await viewFile(selection);
1407
1605
  console.log("");
1408
1606
  }
@@ -1534,54 +1732,75 @@ async function promptForWebSearchModel() {
1534
1732
  * Accepts optional harness config overrides - research uses different defaults.
1535
1733
  */
1536
1734
  async function runAgentFill(form, modelId, _outputPath, configOverrides) {
1537
- const spinner = p.spinner();
1735
+ const { provider: providerName, model: modelName } = parseModelIdForDisplay(modelId);
1736
+ const resolveSpinner = createSpinner({
1737
+ type: "compute",
1738
+ operation: `Resolving model: ${modelId}`
1739
+ });
1740
+ let model, provider;
1538
1741
  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
- };
1742
+ const result = await resolveModel(modelId);
1743
+ model = result.model;
1744
+ provider = result.provider;
1745
+ resolveSpinner.stop(`✓ Model resolved: ${modelId}`);
1581
1746
  } catch (error) {
1582
- spinner.stop(pc.red("Agent fill failed"));
1747
+ resolveSpinner.error("Model resolution failed");
1583
1748
  throw error;
1584
1749
  }
1750
+ const harnessConfig = {
1751
+ maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
1752
+ maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
1753
+ maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
1754
+ targetRoles: [AGENT_ROLE],
1755
+ fillMode: "continue"
1756
+ };
1757
+ console.log("");
1758
+ console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
1759
+ const harness = createHarness(form, harnessConfig);
1760
+ const agent = createLiveAgent({
1761
+ model,
1762
+ provider,
1763
+ targetRole: AGENT_ROLE,
1764
+ enableWebSearch: true
1765
+ });
1766
+ p.log.step(pc.bold("Agent fill in progress..."));
1767
+ let stepResult = harness.step();
1768
+ while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1769
+ console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1770
+ const llmSpinner = createSpinner({
1771
+ type: "api",
1772
+ provider: providerName,
1773
+ model: modelName,
1774
+ turnNumber: stepResult.turnNumber
1775
+ });
1776
+ let response;
1777
+ try {
1778
+ response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1779
+ llmSpinner.stop();
1780
+ } catch (error) {
1781
+ llmSpinner.error("LLM call failed");
1782
+ throw error;
1783
+ }
1784
+ const { patches, stats } = response;
1785
+ for (const patch of patches) {
1786
+ const typeName = formatPatchType(patch);
1787
+ const value = formatPatchValue(patch);
1788
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
1789
+ if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
1790
+ else console.log(` (${typeName}) = ${pc.green(value)}`);
1791
+ }
1792
+ stepResult = harness.apply(patches, stepResult.issues);
1793
+ const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
1794
+ console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
1795
+ if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1796
+ }
1797
+ if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
1798
+ else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
1799
+ Object.assign(form, harness.getForm());
1800
+ return {
1801
+ success: stepResult.isComplete,
1802
+ turnCount: harness.getTurnNumber()
1803
+ };
1585
1804
  }
1586
1805
  /**
1587
1806
  * Run the interactive example scaffolding and filling flow.
@@ -1682,9 +1901,9 @@ async function runInteractiveFlow(preselectedId, formsDirOverride) {
1682
1901
  showInteractiveOutro(patches.length, false);
1683
1902
  console.log("");
1684
1903
  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)")}`);
1904
+ console.log(` ${formatPath(userFillOutputs.reportPath)} ${pc.dim("(output report)")}`);
1905
+ console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(output values)")}`);
1906
+ console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(filled markform source)")}`);
1688
1907
  logTiming({
1689
1908
  verbose: false,
1690
1909
  format: "console",
@@ -1710,19 +1929,19 @@ async function runInteractiveFlow(preselectedId, formsDirOverride) {
1710
1929
  console.log(cliCommand);
1711
1930
  if (userFillOutputs) await showFileViewerChooser([
1712
1931
  {
1713
- path: userFillOutputs.formPath,
1714
- label: "Markform",
1715
- hint: "form with tags"
1932
+ path: userFillOutputs.reportPath,
1933
+ label: "Report",
1934
+ hint: "output report"
1716
1935
  },
1717
1936
  {
1718
- path: userFillOutputs.rawPath,
1719
- label: "Plain Markdown",
1720
- hint: "rendered output"
1937
+ path: userFillOutputs.yamlPath,
1938
+ label: "Values",
1939
+ hint: "output values"
1721
1940
  },
1722
1941
  {
1723
- path: userFillOutputs.yamlPath,
1724
- label: "YAML",
1725
- hint: "extracted values"
1942
+ path: userFillOutputs.formPath,
1943
+ label: "Form",
1944
+ hint: "filled markform source"
1726
1945
  }
1727
1946
  ]);
1728
1947
  p.outro("Happy form filling!");
@@ -1760,29 +1979,29 @@ async function runInteractiveFlow(preselectedId, formsDirOverride) {
1760
1979
  dryRun: false,
1761
1980
  quiet: false
1762
1981
  }, timingLabel, Date.now() - agentStartTime);
1763
- const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, agentOutputPath);
1982
+ const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, agentOutputPath);
1764
1983
  console.log("");
1765
1984
  const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
1766
1985
  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)`);
1986
+ console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
1987
+ console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
1988
+ console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
1770
1989
  if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
1771
1990
  await showFileViewerChooser([
1772
1991
  {
1773
- path: formPath,
1774
- label: "Markform",
1775
- hint: "form with tags"
1992
+ path: reportPath,
1993
+ label: "Report",
1994
+ hint: "output report"
1776
1995
  },
1777
1996
  {
1778
- path: rawPath,
1779
- label: "Plain Markdown",
1780
- hint: "rendered output"
1997
+ path: yamlPath,
1998
+ label: "Values",
1999
+ hint: "output values"
1781
2000
  },
1782
2001
  {
1783
- path: yamlPath,
1784
- label: "YAML",
1785
- hint: "extracted values"
2002
+ path: formPath,
2003
+ label: "Form",
2004
+ hint: "filled markform source"
1786
2005
  }
1787
2006
  ]);
1788
2007
  } catch (error) {
@@ -1864,7 +2083,9 @@ function registerExportCommand(program) {
1864
2083
  ...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
1865
2084
  id: opt.id,
1866
2085
  label: opt.label
1867
- })) } : {}
2086
+ })) } : {},
2087
+ ...field.placeholder ? { placeholder: field.placeholder } : {},
2088
+ ...field.examples && field.examples.length > 0 ? { examples: field.examples } : {}
1868
2089
  }))
1869
2090
  }))
1870
2091
  };
@@ -1987,13 +2208,13 @@ function registerFillCommand(program) {
1987
2208
  logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
1988
2209
  showInteractiveOutro(patches.length, false);
1989
2210
  } else {
1990
- const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, outputPath$1);
2211
+ const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, outputPath$1);
1991
2212
  showInteractiveOutro(patches.length, false);
1992
2213
  console.log("");
1993
2214
  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)")}`);
2215
+ console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
2216
+ console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
2217
+ console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
1997
2218
  }
1998
2219
  logTiming(ctx, "Fill time", durationMs$1);
1999
2220
  if (patches.length > 0) {
@@ -2026,14 +2247,18 @@ function registerFillCommand(program) {
2026
2247
  const harness = createHarness(form, harnessConfig);
2027
2248
  let agent;
2028
2249
  let mockPath;
2250
+ let agentProvider;
2251
+ let agentModelName;
2029
2252
  if (options.mock) {
2030
2253
  mockPath = resolve(options.mockSource);
2031
2254
  logVerbose(ctx, `Reading mock source: ${mockPath}`);
2032
2255
  agent = createMockAgent(parseForm(await readFile$1(mockPath)));
2033
2256
  } else {
2034
- const modelId = options.model;
2035
- logVerbose(ctx, `Resolving model: ${modelId}`);
2036
- const { model, provider } = await resolveModel(modelId);
2257
+ const modelIdString = options.model;
2258
+ logVerbose(ctx, `Resolving model: ${modelIdString}`);
2259
+ const { model, provider, modelId } = await resolveModel(modelIdString);
2260
+ agentProvider = provider;
2261
+ agentModelName = modelId;
2037
2262
  let systemPrompt;
2038
2263
  if (options.instructions) {
2039
2264
  systemPrompt = options.instructions;
@@ -2048,7 +2273,8 @@ function registerFillCommand(program) {
2048
2273
  model,
2049
2274
  provider,
2050
2275
  systemPromptAddition: systemPrompt,
2051
- targetRole: primaryRole
2276
+ targetRole: primaryRole,
2277
+ enableWebSearch: true
2052
2278
  });
2053
2279
  agent = liveAgent;
2054
2280
  logInfo(ctx, `Available tools: ${liveAgent.getAvailableToolNames().join(", ")}`);
@@ -2064,7 +2290,22 @@ function registerFillCommand(program) {
2064
2290
  let stepResult = harness.step();
2065
2291
  logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
2066
2292
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
2067
- const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
2293
+ let spinner = null;
2294
+ if (!options.mock && agentProvider && agentModelName && process.stdout.isTTY && !ctx.quiet) spinner = createSpinner({
2295
+ type: "api",
2296
+ provider: agentProvider,
2297
+ model: agentModelName,
2298
+ turnNumber: stepResult.turnNumber
2299
+ });
2300
+ let response;
2301
+ try {
2302
+ response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
2303
+ spinner?.stop();
2304
+ } catch (error) {
2305
+ spinner?.error("LLM call failed");
2306
+ throw error;
2307
+ }
2308
+ const { patches, stats } = response;
2068
2309
  const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
2069
2310
  logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
2070
2311
  for (const patch of patches) {
@@ -2487,13 +2728,13 @@ function registerReportCommand(program) {
2487
2728
  //#endregion
2488
2729
  //#region src/cli/commands/spec.ts
2489
2730
  /**
2490
- * Get the path to the SPEC.md file.
2731
+ * Get the path to the markform-spec.md file.
2491
2732
  * Works both during development and when installed as a package.
2492
2733
  */
2493
2734
  function getSpecPath() {
2494
2735
  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");
2736
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-spec.md");
2737
+ return join(dirname(dirname(dirname(thisDir))), "docs", "markform-spec.md");
2497
2738
  }
2498
2739
  /**
2499
2740
  * Load the spec content.
@@ -2884,6 +3125,34 @@ function formDataToPatches(formData, form) {
2884
3125
  });
2885
3126
  break;
2886
3127
  }
3128
+ case "date": {
3129
+ const value = formData[fieldId];
3130
+ if (typeof value === "string" && value.trim() !== "") patches.push({
3131
+ op: "set_date",
3132
+ fieldId,
3133
+ value: value.trim()
3134
+ });
3135
+ else patches.push({
3136
+ op: "clear_field",
3137
+ fieldId
3138
+ });
3139
+ break;
3140
+ }
3141
+ case "year": {
3142
+ const value = formData[fieldId];
3143
+ if (typeof value === "string" && value.trim() !== "") {
3144
+ const num = parseInt(value, 10);
3145
+ if (!isNaN(num)) patches.push({
3146
+ op: "set_year",
3147
+ fieldId,
3148
+ value: num
3149
+ });
3150
+ } else patches.push({
3151
+ op: "clear_field",
3152
+ fieldId
3153
+ });
3154
+ break;
3155
+ }
2887
3156
  }
2888
3157
  }
2889
3158
  return patches;
@@ -3228,7 +3497,13 @@ function renderFieldHtml(field, value, isSkipped) {
3228
3497
  case "url_list":
3229
3498
  inputHtml = renderUrlListInput(field, value, disabledAttr);
3230
3499
  break;
3231
- default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
3500
+ case "date":
3501
+ inputHtml = renderDateInput(field, value, disabledAttr);
3502
+ break;
3503
+ case "year":
3504
+ inputHtml = renderYearInput(field, value, disabledAttr);
3505
+ break;
3506
+ default: inputHtml = "<div class=\"field-help\">(unknown field kind)</div>";
3232
3507
  }
3233
3508
  const skipButton = !field.required && !skipped ? `<div class="field-actions">
3234
3509
  <button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
@@ -3250,7 +3525,8 @@ function renderStringInput(field, value, disabledAttr) {
3250
3525
  const requiredAttr = field.required ? " required" : "";
3251
3526
  const minLengthAttr = field.minLength !== void 0 ? ` minlength="${field.minLength}"` : "";
3252
3527
  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}>`;
3528
+ const placeholderAttr = field.placeholder ? ` placeholder="${escapeHtml(field.placeholder)}"` : "";
3529
+ return `<input type="text" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minLengthAttr}${maxLengthAttr}${placeholderAttr}${disabledAttr}>`;
3254
3530
  }
3255
3531
  /**
3256
3532
  * Render a number field as number input.
@@ -3261,7 +3537,8 @@ function renderNumberInput(field, value, disabledAttr) {
3261
3537
  const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
3262
3538
  const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
3263
3539
  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}>`;
3540
+ const placeholderAttr = field.placeholder ? ` placeholder="${escapeHtml(field.placeholder)}"` : "";
3541
+ return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${stepAttr}${placeholderAttr}${disabledAttr}>`;
3265
3542
  }
3266
3543
  /**
3267
3544
  * Render a string list field as textarea.
@@ -3269,7 +3546,8 @@ function renderNumberInput(field, value, disabledAttr) {
3269
3546
  function renderStringListInput(field, value, disabledAttr) {
3270
3547
  const currentValue = (value?.kind === "string_list" ? value.items : []).join("\n");
3271
3548
  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>`;
3549
+ const placeholderText = field.placeholder ? `${escapeHtml(field.placeholder)} (one item per line)` : "Enter one item per line";
3550
+ return `<textarea id="field-${field.id}" name="${field.id}" placeholder="${placeholderText}"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3273
3551
  }
3274
3552
  /**
3275
3553
  * Render a URL field as url input.
@@ -3277,7 +3555,8 @@ function renderStringListInput(field, value, disabledAttr) {
3277
3555
  function renderUrlInput(field, value, disabledAttr) {
3278
3556
  const currentValue = value?.kind === "url" && value.value !== null ? value.value : "";
3279
3557
  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}>`;
3558
+ const placeholderText = field.placeholder ?? "https://example.com";
3559
+ return `<input type="url" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}" placeholder="${escapeHtml(placeholderText)}"${requiredAttr}${disabledAttr}>`;
3281
3560
  }
3282
3561
  /**
3283
3562
  * Render a URL list field as textarea.
@@ -3285,7 +3564,28 @@ function renderUrlInput(field, value, disabledAttr) {
3285
3564
  function renderUrlListInput(field, value, disabledAttr) {
3286
3565
  const currentValue = (value?.kind === "url_list" ? value.items : []).join("\n");
3287
3566
  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>`;
3567
+ const placeholderText = field.placeholder ? `${escapeHtml(field.placeholder)} (one URL per line)` : "Enter one URL per line";
3568
+ return `<textarea id="field-${field.id}" name="${field.id}" placeholder="${placeholderText}"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3569
+ }
3570
+ /**
3571
+ * Render a date field as date input.
3572
+ */
3573
+ function renderDateInput(field, value, disabledAttr) {
3574
+ const currentValue = value?.kind === "date" && value.value !== null ? value.value : "";
3575
+ const requiredAttr = field.required ? " required" : "";
3576
+ const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
3577
+ const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
3578
+ return `<input type="date" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${disabledAttr}>`;
3579
+ }
3580
+ /**
3581
+ * Render a year field as number input.
3582
+ */
3583
+ function renderYearInput(field, value, disabledAttr) {
3584
+ const currentValue = value?.kind === "year" && value.value !== null ? String(value.value) : "";
3585
+ const requiredAttr = field.required ? " required" : "";
3586
+ const minAttr = field.min !== void 0 ? ` min="${field.min}"` : " min=\"1000\"";
3587
+ const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : " max=\"2500\"";
3588
+ return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}" step="1" placeholder="YYYY"${requiredAttr}${minAttr}${maxAttr}${disabledAttr}>`;
3289
3589
  }
3290
3590
  /**
3291
3591
  * Render a single-select field as select element.
@@ -3671,7 +3971,8 @@ function registerResearchCommand(program) {
3671
3971
  process.exit(1);
3672
3972
  }
3673
3973
  const modelId = options.model;
3674
- if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
3974
+ const { provider, model: modelName } = parseModelIdForDisplay(modelId);
3975
+ if (!hasWebSearchSupport(provider)) {
3675
3976
  const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
3676
3977
  logError(`Model "${modelId}" does not support web search.`);
3677
3978
  console.log("");
@@ -3709,27 +4010,40 @@ function registerResearchCommand(program) {
3709
4010
  logVerbose(ctx, `Max turns: ${maxTurns}`);
3710
4011
  logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
3711
4012
  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
- });
4013
+ const spinner = process.stdout.isTTY && !ctx.quiet ? createSpinner({
4014
+ type: "api",
4015
+ provider,
4016
+ model: modelName
4017
+ }) : null;
4018
+ let result;
4019
+ try {
4020
+ result = await runResearch(form, {
4021
+ model: modelId,
4022
+ enableWebSearch: true,
4023
+ maxTurns,
4024
+ maxPatchesPerTurn,
4025
+ maxIssuesPerTurn,
4026
+ targetRoles: [AGENT_ROLE],
4027
+ fillMode: "continue"
4028
+ });
4029
+ spinner?.stop();
4030
+ } catch (error) {
4031
+ spinner?.error("Research failed");
4032
+ throw error;
4033
+ }
3720
4034
  if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
3721
4035
  logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
3722
4036
  logInfo(ctx, `Turns: ${result.totalTurns}`);
3723
4037
  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);
4038
+ const { reportPath, yamlPath, formPath } = await exportMultiFormat(result.form, outputPath);
3725
4039
  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)")}`);
4040
+ console.log(` ${reportPath} ${pc.dim("(output report)")}`);
4041
+ console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
4042
+ console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
3729
4043
  if (options.transcript && result.transcript) {
3730
- const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
4044
+ const { serializeSession: serializeSession$1 } = await import("./session-B_stoXQn.mjs");
3731
4045
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
3732
- const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
4046
+ const { writeFile: writeFile$1 } = await import("./shared-CZsyShck.mjs");
3733
4047
  await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
3734
4048
  logInfo(ctx, `Transcript: ${transcriptPath}`);
3735
4049
  }
@@ -3875,10 +4189,11 @@ function withColoredHelp(cmd) {
3875
4189
  */
3876
4190
  function createProgram() {
3877
4191
  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})`);
4192
+ program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION, "--version", "output the version number").showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`);
3879
4193
  registerReadmeCommand(program);
3880
4194
  registerDocsCommand(program);
3881
4195
  registerSpecCommand(program);
4196
+ registerApisCommand(program);
3882
4197
  registerApplyCommand(program);
3883
4198
  registerDumpCommand(program);
3884
4199
  registerExamplesCommand(program);