markform 0.1.3 → 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 +81 -28
  2. package/dist/ai-sdk.d.mts +2 -2
  3. package/dist/ai-sdk.mjs +5 -5
  4. package/dist/{apply-00UmzDKL.mjs → apply-C54EMAJ1.mjs} +371 -26
  5. package/dist/bin.mjs +6 -6
  6. package/dist/{cli-D--Lel-e.mjs → cli-BhWhn6L9.mjs} +383 -87
  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 +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-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 +25 -11
  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 -19
  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 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-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
  /**
@@ -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
  }
@@ -850,9 +946,10 @@ async function promptSkipOrFill(ctx) {
850
946
  async function promptForString(ctx) {
851
947
  const field = ctx.field;
852
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);
853
950
  const result = await p.text({
854
951
  message: formatFieldLabel(ctx),
855
- placeholder: currentVal ?? (ctx.description ? ctx.description.slice(0, 60) : void 0),
952
+ placeholder: placeholderText,
856
953
  initialValue: currentVal ?? "",
857
954
  validate: (value) => {
858
955
  if (field.required && !value.trim()) return "This field is required";
@@ -875,9 +972,10 @@ async function promptForString(ctx) {
875
972
  async function promptForNumber(ctx) {
876
973
  const field = ctx.field;
877
974
  const currentVal = ctx.currentValue?.kind === "number" ? ctx.currentValue.value : null;
975
+ const placeholderText = field.placeholder ?? (currentVal !== null ? String(currentVal) : void 0);
878
976
  const result = await p.text({
879
977
  message: formatFieldLabel(ctx),
880
- placeholder: currentVal !== null ? String(currentVal) : void 0,
978
+ placeholder: placeholderText,
881
979
  initialValue: currentVal !== null ? String(currentVal) : "",
882
980
  validate: (value) => {
883
981
  if (field.required && !value.trim()) return "This field is required";
@@ -903,7 +1001,10 @@ async function promptForNumber(ctx) {
903
1001
  async function promptForStringList(ctx) {
904
1002
  const field = ctx.field;
905
1003
  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.";
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.";
907
1008
  const result = await p.text({
908
1009
  message: formatFieldLabel(ctx),
909
1010
  placeholder: hint,
@@ -1077,9 +1178,10 @@ async function promptForCheckboxes(ctx) {
1077
1178
  async function promptForUrl(ctx) {
1078
1179
  const field = ctx.field;
1079
1180
  const currentVal = ctx.currentValue?.kind === "url" ? ctx.currentValue.value : null;
1181
+ const placeholderText = field.placeholder ?? currentVal ?? "https://example.com";
1080
1182
  const result = await p.text({
1081
1183
  message: formatFieldLabel(ctx),
1082
- placeholder: currentVal ?? "https://example.com",
1184
+ placeholder: placeholderText,
1083
1185
  initialValue: currentVal ?? "",
1084
1186
  validate: (value) => {
1085
1187
  if (field.required && !value.trim()) return "This field is required";
@@ -1100,12 +1202,87 @@ async function promptForUrl(ctx) {
1100
1202
  };
1101
1203
  }
1102
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
+ /**
1103
1277
  * Prompt for a URL list field value.
1104
1278
  */
1105
1279
  async function promptForUrlList(ctx) {
1106
1280
  const field = ctx.field;
1107
1281
  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.";
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.";
1109
1286
  const result = await p.text({
1110
1287
  message: formatFieldLabel(ctx),
1111
1288
  placeholder: hint,
@@ -1132,7 +1309,7 @@ async function promptForUrlList(ctx) {
1132
1309
  };
1133
1310
  }
1134
1311
  /**
1135
- * Prompt user for a single field value based on field type.
1312
+ * Prompt user for a single field value based on field kind.
1136
1313
  * Returns a Patch to set the value, or null if skipped/cancelled.
1137
1314
  *
1138
1315
  * For optional fields, first offers a choice to skip or fill.
@@ -1151,6 +1328,8 @@ async function promptForField(ctx) {
1151
1328
  case "checkboxes": return promptForCheckboxes(ctx);
1152
1329
  case "url": return promptForUrl(ctx);
1153
1330
  case "url_list": return promptForUrlList(ctx);
1331
+ case "date": return promptForDate(ctx);
1332
+ case "year": return promptForYear(ctx);
1154
1333
  default: return null;
1155
1334
  }
1156
1335
  }
@@ -1553,54 +1732,75 @@ async function promptForWebSearchModel() {
1553
1732
  * Accepts optional harness config overrides - research uses different defaults.
1554
1733
  */
1555
1734
  async function runAgentFill(form, modelId, _outputPath, configOverrides) {
1556
- 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;
1557
1741
  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
- };
1742
+ const result = await resolveModel(modelId);
1743
+ model = result.model;
1744
+ provider = result.provider;
1745
+ resolveSpinner.stop(`✓ Model resolved: ${modelId}`);
1600
1746
  } catch (error) {
1601
- spinner.stop(pc.red("Agent fill failed"));
1747
+ resolveSpinner.error("Model resolution failed");
1602
1748
  throw error;
1603
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
+ };
1604
1804
  }
1605
1805
  /**
1606
1806
  * Run the interactive example scaffolding and filling flow.
@@ -1883,7 +2083,9 @@ function registerExportCommand(program) {
1883
2083
  ...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
1884
2084
  id: opt.id,
1885
2085
  label: opt.label
1886
- })) } : {}
2086
+ })) } : {},
2087
+ ...field.placeholder ? { placeholder: field.placeholder } : {},
2088
+ ...field.examples && field.examples.length > 0 ? { examples: field.examples } : {}
1887
2089
  }))
1888
2090
  }))
1889
2091
  };
@@ -2045,14 +2247,18 @@ function registerFillCommand(program) {
2045
2247
  const harness = createHarness(form, harnessConfig);
2046
2248
  let agent;
2047
2249
  let mockPath;
2250
+ let agentProvider;
2251
+ let agentModelName;
2048
2252
  if (options.mock) {
2049
2253
  mockPath = resolve(options.mockSource);
2050
2254
  logVerbose(ctx, `Reading mock source: ${mockPath}`);
2051
2255
  agent = createMockAgent(parseForm(await readFile$1(mockPath)));
2052
2256
  } else {
2053
- const modelId = options.model;
2054
- logVerbose(ctx, `Resolving model: ${modelId}`);
2055
- 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;
2056
2262
  let systemPrompt;
2057
2263
  if (options.instructions) {
2058
2264
  systemPrompt = options.instructions;
@@ -2067,7 +2273,8 @@ function registerFillCommand(program) {
2067
2273
  model,
2068
2274
  provider,
2069
2275
  systemPromptAddition: systemPrompt,
2070
- targetRole: primaryRole
2276
+ targetRole: primaryRole,
2277
+ enableWebSearch: true
2071
2278
  });
2072
2279
  agent = liveAgent;
2073
2280
  logInfo(ctx, `Available tools: ${liveAgent.getAvailableToolNames().join(", ")}`);
@@ -2083,7 +2290,22 @@ function registerFillCommand(program) {
2083
2290
  let stepResult = harness.step();
2084
2291
  logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
2085
2292
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
2086
- 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;
2087
2309
  const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
2088
2310
  logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
2089
2311
  for (const patch of patches) {
@@ -2506,13 +2728,13 @@ function registerReportCommand(program) {
2506
2728
  //#endregion
2507
2729
  //#region src/cli/commands/spec.ts
2508
2730
  /**
2509
- * Get the path to the SPEC.md file.
2731
+ * Get the path to the markform-spec.md file.
2510
2732
  * Works both during development and when installed as a package.
2511
2733
  */
2512
2734
  function getSpecPath() {
2513
2735
  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");
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");
2516
2738
  }
2517
2739
  /**
2518
2740
  * Load the spec content.
@@ -2903,6 +3125,34 @@ function formDataToPatches(formData, form) {
2903
3125
  });
2904
3126
  break;
2905
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
+ }
2906
3156
  }
2907
3157
  }
2908
3158
  return patches;
@@ -3247,7 +3497,13 @@ function renderFieldHtml(field, value, isSkipped) {
3247
3497
  case "url_list":
3248
3498
  inputHtml = renderUrlListInput(field, value, disabledAttr);
3249
3499
  break;
3250
- 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>";
3251
3507
  }
3252
3508
  const skipButton = !field.required && !skipped ? `<div class="field-actions">
3253
3509
  <button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
@@ -3269,7 +3525,8 @@ function renderStringInput(field, value, disabledAttr) {
3269
3525
  const requiredAttr = field.required ? " required" : "";
3270
3526
  const minLengthAttr = field.minLength !== void 0 ? ` minlength="${field.minLength}"` : "";
3271
3527
  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}>`;
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}>`;
3273
3530
  }
3274
3531
  /**
3275
3532
  * Render a number field as number input.
@@ -3280,7 +3537,8 @@ function renderNumberInput(field, value, disabledAttr) {
3280
3537
  const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
3281
3538
  const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
3282
3539
  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}>`;
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}>`;
3284
3542
  }
3285
3543
  /**
3286
3544
  * Render a string list field as textarea.
@@ -3288,7 +3546,8 @@ function renderNumberInput(field, value, disabledAttr) {
3288
3546
  function renderStringListInput(field, value, disabledAttr) {
3289
3547
  const currentValue = (value?.kind === "string_list" ? value.items : []).join("\n");
3290
3548
  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>`;
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>`;
3292
3551
  }
3293
3552
  /**
3294
3553
  * Render a URL field as url input.
@@ -3296,7 +3555,8 @@ function renderStringListInput(field, value, disabledAttr) {
3296
3555
  function renderUrlInput(field, value, disabledAttr) {
3297
3556
  const currentValue = value?.kind === "url" && value.value !== null ? value.value : "";
3298
3557
  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}>`;
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}>`;
3300
3560
  }
3301
3561
  /**
3302
3562
  * Render a URL list field as textarea.
@@ -3304,7 +3564,28 @@ function renderUrlInput(field, value, disabledAttr) {
3304
3564
  function renderUrlListInput(field, value, disabledAttr) {
3305
3565
  const currentValue = (value?.kind === "url_list" ? value.items : []).join("\n");
3306
3566
  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>`;
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}>`;
3308
3589
  }
3309
3590
  /**
3310
3591
  * Render a single-select field as select element.
@@ -3690,7 +3971,8 @@ function registerResearchCommand(program) {
3690
3971
  process.exit(1);
3691
3972
  }
3692
3973
  const modelId = options.model;
3693
- if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
3974
+ const { provider, model: modelName } = parseModelIdForDisplay(modelId);
3975
+ if (!hasWebSearchSupport(provider)) {
3694
3976
  const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
3695
3977
  logError(`Model "${modelId}" does not support web search.`);
3696
3978
  console.log("");
@@ -3728,14 +4010,27 @@ function registerResearchCommand(program) {
3728
4010
  logVerbose(ctx, `Max turns: ${maxTurns}`);
3729
4011
  logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
3730
4012
  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
- });
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
+ }
3739
4034
  if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
3740
4035
  logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
3741
4036
  logInfo(ctx, `Turns: ${result.totalTurns}`);
@@ -3746,9 +4041,9 @@ function registerResearchCommand(program) {
3746
4041
  console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
3747
4042
  console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
3748
4043
  if (options.transcript && result.transcript) {
3749
- const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
4044
+ const { serializeSession: serializeSession$1 } = await import("./session-B_stoXQn.mjs");
3750
4045
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
3751
- const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
4046
+ const { writeFile: writeFile$1 } = await import("./shared-CZsyShck.mjs");
3752
4047
  await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
3753
4048
  logInfo(ctx, `Transcript: ${transcriptPath}`);
3754
4049
  }
@@ -3894,10 +4189,11 @@ function withColoredHelp(cmd) {
3894
4189
  */
3895
4190
  function createProgram() {
3896
4191
  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})`);
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})`);
3898
4193
  registerReadmeCommand(program);
3899
4194
  registerDocsCommand(program);
3900
4195
  registerSpecCommand(program);
4196
+ registerApisCommand(program);
3901
4197
  registerApplyCommand(program);
3902
4198
  registerDumpCommand(program);
3903
4199
  registerExamplesCommand(program);