markform 0.1.3 → 0.1.5

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 (36) hide show
  1. package/README.md +110 -70
  2. package/dist/ai-sdk.d.mts +2 -2
  3. package/dist/ai-sdk.mjs +5 -5
  4. package/dist/{apply-00UmzDKL.mjs → apply-BCCiJzQr.mjs} +371 -26
  5. package/dist/bin.mjs +6 -6
  6. package/dist/{cli-D--Lel-e.mjs → cli-D469amuk.mjs} +386 -96
  7. package/dist/cli.mjs +6 -6
  8. package/dist/{coreTypes-BXhhz9Iq.d.mts → coreTypes-9XZSNOv6.d.mts} +1878 -325
  9. package/dist/{coreTypes-Dful87E0.mjs → coreTypes-pyctKRgc.mjs} +79 -5
  10. package/dist/index.d.mts +142 -5
  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-Dm8jZ5dl.mjs → src-Df0XX7UB.mjs} +818 -125
  17. package/docs/markform-apis.md +194 -0
  18. package/{DOCS.md → docs/markform-reference.md} +130 -69
  19. package/{SPEC.md → docs/markform-spec.md} +359 -108
  20. package/examples/earnings-analysis/earnings-analysis.form.md +88 -800
  21. package/examples/earnings-analysis/earnings-analysis.valid.ts +16 -148
  22. package/examples/movie-research/movie-research-basic.form.md +41 -37
  23. package/examples/movie-research/movie-research-deep.form.md +110 -98
  24. package/examples/movie-research/movie-research-minimal.form.md +29 -15
  25. package/examples/simple/simple-mock-filled.form.md +105 -41
  26. package/examples/simple/simple-skipped-filled.form.md +103 -41
  27. package/examples/simple/simple-with-skips.session.yaml +93 -25
  28. package/examples/simple/simple.form.md +86 -32
  29. package/examples/simple/simple.session.yaml +98 -25
  30. package/examples/startup-deep-research/startup-deep-research.form.md +130 -103
  31. package/examples/startup-research/startup-research-mock-filled.form.md +55 -55
  32. package/examples/startup-research/startup-research.form.md +36 -36
  33. package/package.json +18 -19
  34. package/dist/session-DdAtY2Ni.mjs +0 -4
  35. package/dist/shared-D7gf27Tr.mjs +0 -3
  36. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +0 -912
@@ -1,19 +1,113 @@
1
- import { N as PatchSchema } from "./coreTypes-Dful87E0.mjs";
2
- import { A as parseRolesFlag, D as deriveReportPath, E as deriveExportPath, F as hasWebSearchSupport, 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-00UmzDKL.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-Dm8jZ5dl.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-BCCiJzQr.mjs";
3
+ import { a as resolveHarnessConfig, c as getProviderNames, f as createMockAgent, i as runResearch, l as resolveModel, m as createHarness, s as getProviderInfo, t as VERSION, u as createLiveAgent, w as parseForm } from "./src-Df0XX7UB.mjs";
4
+ import { n as serializeSession } from "./session-uF0e6m6k.mjs";
5
+ import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-BqPnYXrn.mjs";
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
  /**
@@ -480,6 +574,7 @@ function formatPatchValue(patch) {
480
574
  case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
481
575
  case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
482
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)";
483
578
  case "add_note": return truncate(`note: ${patch.text}`);
484
579
  case "remove_note": return `(remove note ${patch.noteId})`;
485
580
  }
@@ -502,6 +597,7 @@ function formatPatchType(patch) {
502
597
  case "set_url_list": return "url_list";
503
598
  case "set_date": return "date";
504
599
  case "set_year": return "year";
600
+ case "set_table": return "table";
505
601
  case "add_note": return "note";
506
602
  case "remove_note": return "remove_note";
507
603
  }
@@ -588,12 +684,6 @@ const EXAMPLE_DEFINITIONS = [
588
684
  filename: "startup-deep-research.form.md",
589
685
  path: "startup-deep-research/startup-deep-research.form.md",
590
686
  type: "research"
591
- },
592
- {
593
- id: "celebrity-deep-research",
594
- filename: "celebrity-deep-research.form.md",
595
- path: "celebrity-deep-research/celebrity-deep-research.form.md",
596
- type: "research"
597
687
  }
598
688
  ];
599
689
  /**
@@ -607,7 +697,7 @@ function getExamplesDir() {
607
697
  }
608
698
  /**
609
699
  * Load the content of an example form.
610
- * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
700
+ * @param exampleId - The example ID (e.g., 'simple', 'movie-research-deep')
611
701
  * @returns The form content as a string
612
702
  * @throws Error if the example is not found
613
703
  */
@@ -629,7 +719,7 @@ function getExampleById(id) {
629
719
  }
630
720
  /**
631
721
  * Get the absolute path to an example's source file.
632
- * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
722
+ * @param exampleId - The example ID (e.g., 'simple', 'movie-research-deep')
633
723
  * @returns The absolute path to the example form file
634
724
  * @throws Error if the example is not found
635
725
  */
@@ -654,7 +744,7 @@ function extractFrontmatter(content) {
654
744
  }
655
745
  /**
656
746
  * Load metadata (title, description) from an example's YAML frontmatter.
657
- * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
747
+ * @param exampleId - The example ID (e.g., 'simple', 'movie-research-deep')
658
748
  * @returns Object with title and description from frontmatter
659
749
  */
660
750
  function loadExampleMetadata(exampleId) {
@@ -850,9 +940,10 @@ async function promptSkipOrFill(ctx) {
850
940
  async function promptForString(ctx) {
851
941
  const field = ctx.field;
852
942
  const currentVal = ctx.currentValue?.kind === "string" ? ctx.currentValue.value : null;
943
+ const placeholderText = field.placeholder ?? currentVal ?? (ctx.description ? ctx.description.slice(0, 60) : void 0);
853
944
  const result = await p.text({
854
945
  message: formatFieldLabel(ctx),
855
- placeholder: currentVal ?? (ctx.description ? ctx.description.slice(0, 60) : void 0),
946
+ placeholder: placeholderText,
856
947
  initialValue: currentVal ?? "",
857
948
  validate: (value) => {
858
949
  if (field.required && !value.trim()) return "This field is required";
@@ -875,9 +966,10 @@ async function promptForString(ctx) {
875
966
  async function promptForNumber(ctx) {
876
967
  const field = ctx.field;
877
968
  const currentVal = ctx.currentValue?.kind === "number" ? ctx.currentValue.value : null;
969
+ const placeholderText = field.placeholder ?? (currentVal !== null ? String(currentVal) : void 0);
878
970
  const result = await p.text({
879
971
  message: formatFieldLabel(ctx),
880
- placeholder: currentVal !== null ? String(currentVal) : void 0,
972
+ placeholder: placeholderText,
881
973
  initialValue: currentVal !== null ? String(currentVal) : "",
882
974
  validate: (value) => {
883
975
  if (field.required && !value.trim()) return "This field is required";
@@ -903,7 +995,10 @@ async function promptForNumber(ctx) {
903
995
  async function promptForStringList(ctx) {
904
996
  const field = ctx.field;
905
997
  const currentItems = ctx.currentValue?.kind === "string_list" ? ctx.currentValue.items : [];
906
- const hint = ctx.description ? `${ctx.description.slice(0, 50)}... (one item per line)` : "Enter items, one per line. Press Enter twice when done.";
998
+ let hint;
999
+ if (field.placeholder) hint = `${field.placeholder} (one item per line)`;
1000
+ else if (ctx.description) hint = `${ctx.description.slice(0, 50)}... (one item per line)`;
1001
+ else hint = "Enter items, one per line. Press Enter twice when done.";
907
1002
  const result = await p.text({
908
1003
  message: formatFieldLabel(ctx),
909
1004
  placeholder: hint,
@@ -1077,9 +1172,10 @@ async function promptForCheckboxes(ctx) {
1077
1172
  async function promptForUrl(ctx) {
1078
1173
  const field = ctx.field;
1079
1174
  const currentVal = ctx.currentValue?.kind === "url" ? ctx.currentValue.value : null;
1175
+ const placeholderText = field.placeholder ?? currentVal ?? "https://example.com";
1080
1176
  const result = await p.text({
1081
1177
  message: formatFieldLabel(ctx),
1082
- placeholder: currentVal ?? "https://example.com",
1178
+ placeholder: placeholderText,
1083
1179
  initialValue: currentVal ?? "",
1084
1180
  validate: (value) => {
1085
1181
  if (field.required && !value.trim()) return "This field is required";
@@ -1100,12 +1196,87 @@ async function promptForUrl(ctx) {
1100
1196
  };
1101
1197
  }
1102
1198
  /**
1199
+ * Check if a string is a valid ISO 8601 date (YYYY-MM-DD).
1200
+ */
1201
+ function isValidDate(str) {
1202
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) return false;
1203
+ const date = new Date(str);
1204
+ if (isNaN(date.getTime())) return false;
1205
+ const [year, month, day] = str.split("-").map(Number);
1206
+ return date.getUTCFullYear() === year && date.getUTCMonth() + 1 === month && date.getUTCDate() === day;
1207
+ }
1208
+ /**
1209
+ * Prompt for a date field value.
1210
+ */
1211
+ async function promptForDate(ctx) {
1212
+ const field = ctx.field;
1213
+ const currentVal = ctx.currentValue?.kind === "date" ? ctx.currentValue.value : null;
1214
+ const constraints = [];
1215
+ if (field.min) constraints.push(`min: ${field.min}`);
1216
+ if (field.max) constraints.push(`max: ${field.max}`);
1217
+ const formatHint = constraints.length > 0 ? ` (${constraints.join(", ")})` : "";
1218
+ const result = await p.text({
1219
+ message: formatFieldLabel(ctx),
1220
+ placeholder: currentVal ?? `YYYY-MM-DD${formatHint}`,
1221
+ initialValue: currentVal ?? "",
1222
+ validate: (value) => {
1223
+ if (field.required && !value.trim()) return "This field is required";
1224
+ if (!value.trim()) return;
1225
+ if (!isValidDate(value)) return "Please enter a valid date in YYYY-MM-DD format";
1226
+ if (field.min && value < field.min) return `Date must be on or after ${field.min}`;
1227
+ if (field.max && value > field.max) return `Date must be on or before ${field.max}`;
1228
+ }
1229
+ });
1230
+ if (p.isCancel(result)) return null;
1231
+ if (!result && !field.required) return null;
1232
+ return {
1233
+ op: "set_date",
1234
+ fieldId: field.id,
1235
+ value: result || null
1236
+ };
1237
+ }
1238
+ /** Default year range for validation */
1239
+ const DEFAULT_MIN_YEAR = 1e3;
1240
+ const DEFAULT_MAX_YEAR = 2500;
1241
+ /**
1242
+ * Prompt for a year field value.
1243
+ */
1244
+ async function promptForYear(ctx) {
1245
+ const field = ctx.field;
1246
+ const currentVal = ctx.currentValue?.kind === "year" ? ctx.currentValue.value : null;
1247
+ const minYear = field.min ?? DEFAULT_MIN_YEAR;
1248
+ const maxYear = field.max ?? DEFAULT_MAX_YEAR;
1249
+ const result = await p.text({
1250
+ message: formatFieldLabel(ctx),
1251
+ placeholder: currentVal !== null ? String(currentVal) : `Year (${minYear}-${maxYear})`,
1252
+ initialValue: currentVal !== null ? String(currentVal) : "",
1253
+ validate: (value) => {
1254
+ if (field.required && !value.trim()) return "This field is required";
1255
+ if (!value.trim()) return;
1256
+ const num = Number(value);
1257
+ if (isNaN(num) || !Number.isInteger(num)) return "Please enter a valid year (e.g., 2025)";
1258
+ if (num < minYear) return `Year must be ${minYear} or later`;
1259
+ if (num > maxYear) return `Year must be ${maxYear} or earlier`;
1260
+ }
1261
+ });
1262
+ if (p.isCancel(result)) return null;
1263
+ if (!result && !field.required) return null;
1264
+ return {
1265
+ op: "set_year",
1266
+ fieldId: field.id,
1267
+ value: result ? Number(result) : null
1268
+ };
1269
+ }
1270
+ /**
1103
1271
  * Prompt for a URL list field value.
1104
1272
  */
1105
1273
  async function promptForUrlList(ctx) {
1106
1274
  const field = ctx.field;
1107
1275
  const currentItems = ctx.currentValue?.kind === "url_list" ? ctx.currentValue.items : [];
1108
- const hint = ctx.description ? `${ctx.description.slice(0, 50)}... (one URL per line)` : "Enter URLs, one per line. Press Enter twice when done.";
1276
+ let hint;
1277
+ if (field.placeholder) hint = `${field.placeholder} (one URL per line)`;
1278
+ else if (ctx.description) hint = `${ctx.description.slice(0, 50)}... (one URL per line)`;
1279
+ else hint = "Enter URLs, one per line. Press Enter twice when done.";
1109
1280
  const result = await p.text({
1110
1281
  message: formatFieldLabel(ctx),
1111
1282
  placeholder: hint,
@@ -1132,7 +1303,7 @@ async function promptForUrlList(ctx) {
1132
1303
  };
1133
1304
  }
1134
1305
  /**
1135
- * Prompt user for a single field value based on field type.
1306
+ * Prompt user for a single field value based on field kind.
1136
1307
  * Returns a Patch to set the value, or null if skipped/cancelled.
1137
1308
  *
1138
1309
  * For optional fields, first offers a choice to skip or fill.
@@ -1151,6 +1322,8 @@ async function promptForField(ctx) {
1151
1322
  case "checkboxes": return promptForCheckboxes(ctx);
1152
1323
  case "url": return promptForUrl(ctx);
1153
1324
  case "url_list": return promptForUrlList(ctx);
1325
+ case "date": return promptForDate(ctx);
1326
+ case "year": return promptForYear(ctx);
1154
1327
  default: return null;
1155
1328
  }
1156
1329
  }
@@ -1553,54 +1726,75 @@ async function promptForWebSearchModel() {
1553
1726
  * Accepts optional harness config overrides - research uses different defaults.
1554
1727
  */
1555
1728
  async function runAgentFill(form, modelId, _outputPath, configOverrides) {
1556
- const spinner = p.spinner();
1729
+ const { provider: providerName, model: modelName } = parseModelIdForDisplay(modelId);
1730
+ const resolveSpinner = createSpinner({
1731
+ type: "compute",
1732
+ operation: `Resolving model: ${modelId}`
1733
+ });
1734
+ let model, provider;
1557
1735
  try {
1558
- spinner.start(`Resolving model: ${modelId}`);
1559
- const { model, provider } = await resolveModel(modelId);
1560
- spinner.stop(`Model resolved: ${modelId}`);
1561
- const harnessConfig = {
1562
- maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
1563
- maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
1564
- maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
1565
- targetRoles: [AGENT_ROLE],
1566
- fillMode: "continue"
1567
- };
1568
- console.log("");
1569
- console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
1570
- const harness = createHarness(form, harnessConfig);
1571
- const agent = createLiveAgent({
1572
- model,
1573
- provider,
1574
- targetRole: AGENT_ROLE
1575
- });
1576
- p.log.step(pc.bold("Agent fill in progress..."));
1577
- let stepResult = harness.step();
1578
- while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1579
- console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1580
- const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1581
- for (const patch of patches) {
1582
- const typeName = formatPatchType(patch);
1583
- const value = formatPatchValue(patch);
1584
- const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
1585
- if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
1586
- else console.log(` (${typeName}) = ${pc.green(value)}`);
1587
- }
1588
- stepResult = harness.apply(patches, stepResult.issues);
1589
- const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
1590
- console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
1591
- if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1592
- }
1593
- if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
1594
- else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
1595
- Object.assign(form, harness.getForm());
1596
- return {
1597
- success: stepResult.isComplete,
1598
- turnCount: harness.getTurnNumber()
1599
- };
1736
+ const result = await resolveModel(modelId);
1737
+ model = result.model;
1738
+ provider = result.provider;
1739
+ resolveSpinner.stop(`✓ Model resolved: ${modelId}`);
1600
1740
  } catch (error) {
1601
- spinner.stop(pc.red("Agent fill failed"));
1741
+ resolveSpinner.error("Model resolution failed");
1602
1742
  throw error;
1603
1743
  }
1744
+ const harnessConfig = {
1745
+ maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
1746
+ maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
1747
+ maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
1748
+ targetRoles: [AGENT_ROLE],
1749
+ fillMode: "continue"
1750
+ };
1751
+ console.log("");
1752
+ console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
1753
+ const harness = createHarness(form, harnessConfig);
1754
+ const agent = createLiveAgent({
1755
+ model,
1756
+ provider,
1757
+ targetRole: AGENT_ROLE,
1758
+ enableWebSearch: true
1759
+ });
1760
+ p.log.step(pc.bold("Agent fill in progress..."));
1761
+ let stepResult = harness.step();
1762
+ while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1763
+ console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1764
+ const llmSpinner = createSpinner({
1765
+ type: "api",
1766
+ provider: providerName,
1767
+ model: modelName,
1768
+ turnNumber: stepResult.turnNumber
1769
+ });
1770
+ let response;
1771
+ try {
1772
+ response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1773
+ llmSpinner.stop();
1774
+ } catch (error) {
1775
+ llmSpinner.error("LLM call failed");
1776
+ throw error;
1777
+ }
1778
+ const { patches, stats } = response;
1779
+ for (const patch of patches) {
1780
+ const typeName = formatPatchType(patch);
1781
+ const value = formatPatchValue(patch);
1782
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
1783
+ if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
1784
+ else console.log(` (${typeName}) = ${pc.green(value)}`);
1785
+ }
1786
+ stepResult = harness.apply(patches, stepResult.issues);
1787
+ const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
1788
+ console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
1789
+ if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1790
+ }
1791
+ if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
1792
+ else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
1793
+ Object.assign(form, harness.getForm());
1794
+ return {
1795
+ success: stepResult.isComplete,
1796
+ turnCount: harness.getTurnNumber()
1797
+ };
1604
1798
  }
1605
1799
  /**
1606
1800
  * Run the interactive example scaffolding and filling flow.
@@ -1883,7 +2077,9 @@ function registerExportCommand(program) {
1883
2077
  ...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
1884
2078
  id: opt.id,
1885
2079
  label: opt.label
1886
- })) } : {}
2080
+ })) } : {},
2081
+ ...field.placeholder ? { placeholder: field.placeholder } : {},
2082
+ ...field.examples && field.examples.length > 0 ? { examples: field.examples } : {}
1887
2083
  }))
1888
2084
  }))
1889
2085
  };
@@ -2045,14 +2241,18 @@ function registerFillCommand(program) {
2045
2241
  const harness = createHarness(form, harnessConfig);
2046
2242
  let agent;
2047
2243
  let mockPath;
2244
+ let agentProvider;
2245
+ let agentModelName;
2048
2246
  if (options.mock) {
2049
2247
  mockPath = resolve(options.mockSource);
2050
2248
  logVerbose(ctx, `Reading mock source: ${mockPath}`);
2051
2249
  agent = createMockAgent(parseForm(await readFile$1(mockPath)));
2052
2250
  } else {
2053
- const modelId = options.model;
2054
- logVerbose(ctx, `Resolving model: ${modelId}`);
2055
- const { model, provider } = await resolveModel(modelId);
2251
+ const modelIdString = options.model;
2252
+ logVerbose(ctx, `Resolving model: ${modelIdString}`);
2253
+ const { model, provider, modelId } = await resolveModel(modelIdString);
2254
+ agentProvider = provider;
2255
+ agentModelName = modelId;
2056
2256
  let systemPrompt;
2057
2257
  if (options.instructions) {
2058
2258
  systemPrompt = options.instructions;
@@ -2067,7 +2267,8 @@ function registerFillCommand(program) {
2067
2267
  model,
2068
2268
  provider,
2069
2269
  systemPromptAddition: systemPrompt,
2070
- targetRole: primaryRole
2270
+ targetRole: primaryRole,
2271
+ enableWebSearch: true
2071
2272
  });
2072
2273
  agent = liveAgent;
2073
2274
  logInfo(ctx, `Available tools: ${liveAgent.getAvailableToolNames().join(", ")}`);
@@ -2083,7 +2284,22 @@ function registerFillCommand(program) {
2083
2284
  let stepResult = harness.step();
2084
2285
  logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
2085
2286
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
2086
- const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
2287
+ let spinner = null;
2288
+ if (!options.mock && agentProvider && agentModelName && process.stdout.isTTY && !ctx.quiet) spinner = createSpinner({
2289
+ type: "api",
2290
+ provider: agentProvider,
2291
+ model: agentModelName,
2292
+ turnNumber: stepResult.turnNumber
2293
+ });
2294
+ let response;
2295
+ try {
2296
+ response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
2297
+ spinner?.stop();
2298
+ } catch (error) {
2299
+ spinner?.error("LLM call failed");
2300
+ throw error;
2301
+ }
2302
+ const { patches, stats } = response;
2087
2303
  const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
2088
2304
  logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
2089
2305
  for (const patch of patches) {
@@ -2506,13 +2722,13 @@ function registerReportCommand(program) {
2506
2722
  //#endregion
2507
2723
  //#region src/cli/commands/spec.ts
2508
2724
  /**
2509
- * Get the path to the SPEC.md file.
2725
+ * Get the path to the markform-spec.md file.
2510
2726
  * Works both during development and when installed as a package.
2511
2727
  */
2512
2728
  function getSpecPath() {
2513
2729
  const thisDir = dirname(fileURLToPath(import.meta.url));
2514
- if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "SPEC.md");
2515
- return join(dirname(dirname(dirname(thisDir))), "SPEC.md");
2730
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-spec.md");
2731
+ return join(dirname(dirname(dirname(thisDir))), "docs", "markform-spec.md");
2516
2732
  }
2517
2733
  /**
2518
2734
  * Load the spec content.
@@ -2903,6 +3119,34 @@ function formDataToPatches(formData, form) {
2903
3119
  });
2904
3120
  break;
2905
3121
  }
3122
+ case "date": {
3123
+ const value = formData[fieldId];
3124
+ if (typeof value === "string" && value.trim() !== "") patches.push({
3125
+ op: "set_date",
3126
+ fieldId,
3127
+ value: value.trim()
3128
+ });
3129
+ else patches.push({
3130
+ op: "clear_field",
3131
+ fieldId
3132
+ });
3133
+ break;
3134
+ }
3135
+ case "year": {
3136
+ const value = formData[fieldId];
3137
+ if (typeof value === "string" && value.trim() !== "") {
3138
+ const num = parseInt(value, 10);
3139
+ if (!isNaN(num)) patches.push({
3140
+ op: "set_year",
3141
+ fieldId,
3142
+ value: num
3143
+ });
3144
+ } else patches.push({
3145
+ op: "clear_field",
3146
+ fieldId
3147
+ });
3148
+ break;
3149
+ }
2906
3150
  }
2907
3151
  }
2908
3152
  return patches;
@@ -3247,7 +3491,13 @@ function renderFieldHtml(field, value, isSkipped) {
3247
3491
  case "url_list":
3248
3492
  inputHtml = renderUrlListInput(field, value, disabledAttr);
3249
3493
  break;
3250
- default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
3494
+ case "date":
3495
+ inputHtml = renderDateInput(field, value, disabledAttr);
3496
+ break;
3497
+ case "year":
3498
+ inputHtml = renderYearInput(field, value, disabledAttr);
3499
+ break;
3500
+ default: inputHtml = "<div class=\"field-help\">(unknown field kind)</div>";
3251
3501
  }
3252
3502
  const skipButton = !field.required && !skipped ? `<div class="field-actions">
3253
3503
  <button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
@@ -3269,7 +3519,8 @@ function renderStringInput(field, value, disabledAttr) {
3269
3519
  const requiredAttr = field.required ? " required" : "";
3270
3520
  const minLengthAttr = field.minLength !== void 0 ? ` minlength="${field.minLength}"` : "";
3271
3521
  const maxLengthAttr = field.maxLength !== void 0 ? ` maxlength="${field.maxLength}"` : "";
3272
- return `<input type="text" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minLengthAttr}${maxLengthAttr}${disabledAttr}>`;
3522
+ const placeholderAttr = field.placeholder ? ` placeholder="${escapeHtml(field.placeholder)}"` : "";
3523
+ return `<input type="text" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minLengthAttr}${maxLengthAttr}${placeholderAttr}${disabledAttr}>`;
3273
3524
  }
3274
3525
  /**
3275
3526
  * Render a number field as number input.
@@ -3280,7 +3531,8 @@ function renderNumberInput(field, value, disabledAttr) {
3280
3531
  const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
3281
3532
  const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
3282
3533
  const stepAttr = field.integer ? " step=\"1\"" : "";
3283
- return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${stepAttr}${disabledAttr}>`;
3534
+ const placeholderAttr = field.placeholder ? ` placeholder="${escapeHtml(field.placeholder)}"` : "";
3535
+ return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${stepAttr}${placeholderAttr}${disabledAttr}>`;
3284
3536
  }
3285
3537
  /**
3286
3538
  * Render a string list field as textarea.
@@ -3288,7 +3540,8 @@ function renderNumberInput(field, value, disabledAttr) {
3288
3540
  function renderStringListInput(field, value, disabledAttr) {
3289
3541
  const currentValue = (value?.kind === "string_list" ? value.items : []).join("\n");
3290
3542
  const requiredAttr = field.required ? " required" : "";
3291
- return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one item per line"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3543
+ const placeholderText = field.placeholder ? `${escapeHtml(field.placeholder)} (one item per line)` : "Enter one item per line";
3544
+ return `<textarea id="field-${field.id}" name="${field.id}" placeholder="${placeholderText}"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3292
3545
  }
3293
3546
  /**
3294
3547
  * Render a URL field as url input.
@@ -3296,7 +3549,8 @@ function renderStringListInput(field, value, disabledAttr) {
3296
3549
  function renderUrlInput(field, value, disabledAttr) {
3297
3550
  const currentValue = value?.kind === "url" && value.value !== null ? value.value : "";
3298
3551
  const requiredAttr = field.required ? " required" : "";
3299
- return `<input type="url" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}" placeholder="https://example.com"${requiredAttr}${disabledAttr}>`;
3552
+ const placeholderText = field.placeholder ?? "https://example.com";
3553
+ return `<input type="url" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}" placeholder="${escapeHtml(placeholderText)}"${requiredAttr}${disabledAttr}>`;
3300
3554
  }
3301
3555
  /**
3302
3556
  * Render a URL list field as textarea.
@@ -3304,7 +3558,28 @@ function renderUrlInput(field, value, disabledAttr) {
3304
3558
  function renderUrlListInput(field, value, disabledAttr) {
3305
3559
  const currentValue = (value?.kind === "url_list" ? value.items : []).join("\n");
3306
3560
  const requiredAttr = field.required ? " required" : "";
3307
- return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one URL per line"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3561
+ const placeholderText = field.placeholder ? `${escapeHtml(field.placeholder)} (one URL per line)` : "Enter one URL per line";
3562
+ return `<textarea id="field-${field.id}" name="${field.id}" placeholder="${placeholderText}"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
3563
+ }
3564
+ /**
3565
+ * Render a date field as date input.
3566
+ */
3567
+ function renderDateInput(field, value, disabledAttr) {
3568
+ const currentValue = value?.kind === "date" && value.value !== null ? value.value : "";
3569
+ const requiredAttr = field.required ? " required" : "";
3570
+ const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
3571
+ const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
3572
+ return `<input type="date" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${disabledAttr}>`;
3573
+ }
3574
+ /**
3575
+ * Render a year field as number input.
3576
+ */
3577
+ function renderYearInput(field, value, disabledAttr) {
3578
+ const currentValue = value?.kind === "year" && value.value !== null ? String(value.value) : "";
3579
+ const requiredAttr = field.required ? " required" : "";
3580
+ const minAttr = field.min !== void 0 ? ` min="${field.min}"` : " min=\"1000\"";
3581
+ const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : " max=\"2500\"";
3582
+ return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}" step="1" placeholder="YYYY"${requiredAttr}${minAttr}${maxAttr}${disabledAttr}>`;
3308
3583
  }
3309
3584
  /**
3310
3585
  * Render a single-select field as select element.
@@ -3690,7 +3965,8 @@ function registerResearchCommand(program) {
3690
3965
  process.exit(1);
3691
3966
  }
3692
3967
  const modelId = options.model;
3693
- if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
3968
+ const { provider, model: modelName } = parseModelIdForDisplay(modelId);
3969
+ if (!hasWebSearchSupport(provider)) {
3694
3970
  const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
3695
3971
  logError(`Model "${modelId}" does not support web search.`);
3696
3972
  console.log("");
@@ -3728,14 +4004,27 @@ function registerResearchCommand(program) {
3728
4004
  logVerbose(ctx, `Max turns: ${maxTurns}`);
3729
4005
  logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
3730
4006
  logVerbose(ctx, `Max issues/turn: ${maxIssuesPerTurn}`);
3731
- const result = await runResearch(form, {
3732
- model: modelId,
3733
- maxTurns,
3734
- maxPatchesPerTurn,
3735
- maxIssuesPerTurn,
3736
- targetRoles: [AGENT_ROLE],
3737
- fillMode: "continue"
3738
- });
4007
+ const spinner = process.stdout.isTTY && !ctx.quiet ? createSpinner({
4008
+ type: "api",
4009
+ provider,
4010
+ model: modelName
4011
+ }) : null;
4012
+ let result;
4013
+ try {
4014
+ result = await runResearch(form, {
4015
+ model: modelId,
4016
+ enableWebSearch: true,
4017
+ maxTurns,
4018
+ maxPatchesPerTurn,
4019
+ maxIssuesPerTurn,
4020
+ targetRoles: [AGENT_ROLE],
4021
+ fillMode: "continue"
4022
+ });
4023
+ spinner?.stop();
4024
+ } catch (error) {
4025
+ spinner?.error("Research failed");
4026
+ throw error;
4027
+ }
3739
4028
  if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
3740
4029
  logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
3741
4030
  logInfo(ctx, `Turns: ${result.totalTurns}`);
@@ -3746,9 +4035,9 @@ function registerResearchCommand(program) {
3746
4035
  console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
3747
4036
  console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
3748
4037
  if (options.transcript && result.transcript) {
3749
- const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
4038
+ const { serializeSession: serializeSession$1 } = await import("./session-B_stoXQn.mjs");
3750
4039
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
3751
- const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
4040
+ const { writeFile: writeFile$1 } = await import("./shared-CZsyShck.mjs");
3752
4041
  await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
3753
4042
  logInfo(ctx, `Transcript: ${transcriptPath}`);
3754
4043
  }
@@ -3894,10 +4183,11 @@ function withColoredHelp(cmd) {
3894
4183
  */
3895
4184
  function createProgram() {
3896
4185
  const program = withColoredHelp(new Command());
3897
- 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})`);
4186
+ program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION, "--version", "output the version number").showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`);
3898
4187
  registerReadmeCommand(program);
3899
4188
  registerDocsCommand(program);
3900
4189
  registerSpecCommand(program);
4190
+ registerApisCommand(program);
3901
4191
  registerApplyCommand(program);
3902
4192
  registerDumpCommand(program);
3903
4193
  registerExamplesCommand(program);