markform 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +546 -0
- package/README.md +338 -71
- package/SPEC.md +2779 -0
- package/dist/ai-sdk.d.mts +2 -2
- package/dist/ai-sdk.mjs +5 -3
- package/dist/{apply-BQdd-fdx.mjs → apply-BfAGTHMh.mjs} +837 -730
- package/dist/bin.mjs +6 -3
- package/dist/{cli-pjOiHgCW.mjs → cli-B3NVm6zL.mjs} +1349 -422
- package/dist/cli.mjs +6 -3
- package/dist/{coreTypes--6etkcwb.d.mts → coreTypes-BXhhz9Iq.d.mts} +1946 -794
- package/dist/coreTypes-Dful87E0.mjs +537 -0
- package/dist/index.d.mts +116 -19
- package/dist/index.mjs +5 -3
- package/dist/session-Bqnwi9wp.mjs +110 -0
- package/dist/session-DdAtY2Ni.mjs +4 -0
- package/dist/shared-D7gf27Tr.mjs +3 -0
- package/dist/shared-N_s1M-_K.mjs +176 -0
- package/dist/src-BXRkGFpG.mjs +7587 -0
- package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
- package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
- package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
- package/examples/movie-research/movie-research-basic.form.md +164 -0
- package/examples/movie-research/movie-research-deep.form.md +486 -0
- package/examples/movie-research/movie-research-minimal.form.md +73 -0
- package/examples/simple/simple-mock-filled.form.md +17 -13
- package/examples/simple/simple-skipped-filled.form.md +32 -9
- package/examples/simple/simple-with-skips.session.yaml +102 -143
- package/examples/simple/simple.form.md +13 -13
- package/examples/simple/simple.session.yaml +80 -69
- package/examples/startup-deep-research/startup-deep-research.form.md +60 -8
- package/examples/startup-research/startup-research-mock-filled.form.md +1 -1
- package/examples/startup-research/startup-research.form.md +1 -1
- package/package.json +9 -5
- package/dist/src-Cs4_9lWP.mjs +0 -2151
- package/examples/political-research/political-research.form.md +0 -233
- package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
|
@@ -1,172 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as
|
|
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";
|
|
3
6
|
import YAML from "yaml";
|
|
7
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
8
|
import { Command } from "commander";
|
|
5
9
|
import pc from "picocolors";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
12
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import
|
|
13
|
+
import * as p from "@clack/prompts";
|
|
14
|
+
import { exec, spawn } from "node:child_process";
|
|
11
15
|
import { createServer } from "node:http";
|
|
12
16
|
|
|
13
|
-
//#region src/cli/lib/naming.ts
|
|
14
|
-
/**
|
|
15
|
-
* Naming convention utilities for JSON/YAML output.
|
|
16
|
-
*
|
|
17
|
-
* Converts between camelCase (TypeScript internal) and snake_case (JSON/YAML output).
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Convert a camelCase string to snake_case.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* toSnakeCase("fieldCount") // "field_count"
|
|
24
|
-
* toSnakeCase("parentFieldId") // "parent_field_id"
|
|
25
|
-
* toSnakeCase("already_snake") // "already_snake"
|
|
26
|
-
*/
|
|
27
|
-
function toSnakeCase(str) {
|
|
28
|
-
return str.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Recursively convert all object keys from camelCase to snake_case.
|
|
32
|
-
*
|
|
33
|
-
* Handles nested objects and arrays. Primitives are returned unchanged.
|
|
34
|
-
*/
|
|
35
|
-
function convertKeysToSnakeCase(obj) {
|
|
36
|
-
if (obj === null || obj === void 0) return obj;
|
|
37
|
-
if (Array.isArray(obj)) return obj.map(convertKeysToSnakeCase);
|
|
38
|
-
if (typeof obj === "object") {
|
|
39
|
-
const result = {};
|
|
40
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
41
|
-
const snakeKey = toSnakeCase(key);
|
|
42
|
-
result[snakeKey] = convertKeysToSnakeCase(value);
|
|
43
|
-
}
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
return obj;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region src/cli/lib/shared.ts
|
|
51
|
-
/**
|
|
52
|
-
* Valid format options for Commander choice validation.
|
|
53
|
-
*/
|
|
54
|
-
const OUTPUT_FORMATS = [
|
|
55
|
-
"console",
|
|
56
|
-
"plaintext",
|
|
57
|
-
"yaml",
|
|
58
|
-
"json",
|
|
59
|
-
"markform",
|
|
60
|
-
"markdown"
|
|
61
|
-
];
|
|
62
|
-
/**
|
|
63
|
-
* Extract command context from Commander options.
|
|
64
|
-
*/
|
|
65
|
-
function getCommandContext(command) {
|
|
66
|
-
const opts = command.optsWithGlobals();
|
|
67
|
-
return {
|
|
68
|
-
dryRun: opts.dryRun ?? false,
|
|
69
|
-
verbose: opts.verbose ?? false,
|
|
70
|
-
quiet: opts.quiet ?? false,
|
|
71
|
-
format: opts.format ?? "console"
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Check if output should use colors.
|
|
76
|
-
* Returns true for console format when stdout is a TTY.
|
|
77
|
-
*/
|
|
78
|
-
function shouldUseColors(ctx) {
|
|
79
|
-
if (ctx.format === "plaintext" || ctx.format === "yaml" || ctx.format === "json") return false;
|
|
80
|
-
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Format structured data according to output format.
|
|
84
|
-
*
|
|
85
|
-
* JSON and YAML outputs are converted to snake_case keys for consistency.
|
|
86
|
-
*/
|
|
87
|
-
function formatOutput(ctx, data, consoleFormatter) {
|
|
88
|
-
switch (ctx.format) {
|
|
89
|
-
case "json": return JSON.stringify(convertKeysToSnakeCase(data), null, 2);
|
|
90
|
-
case "yaml": return YAML.stringify(convertKeysToSnakeCase(data));
|
|
91
|
-
case "plaintext":
|
|
92
|
-
case "console":
|
|
93
|
-
default:
|
|
94
|
-
if (consoleFormatter) return consoleFormatter(data, shouldUseColors(ctx));
|
|
95
|
-
return YAML.stringify(convertKeysToSnakeCase(data));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Log a dry-run message.
|
|
100
|
-
*/
|
|
101
|
-
function logDryRun(message, details) {
|
|
102
|
-
console.log(pc.yellow(`[DRY RUN] ${message}`));
|
|
103
|
-
if (details) console.log(pc.dim(JSON.stringify(details, null, 2)));
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Log a verbose message (only shown if --verbose is set).
|
|
107
|
-
*/
|
|
108
|
-
function logVerbose(ctx, message) {
|
|
109
|
-
if (ctx.verbose) console.log(pc.dim(message));
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Log an info message (hidden if --quiet is set).
|
|
113
|
-
*/
|
|
114
|
-
function logInfo(ctx, message) {
|
|
115
|
-
if (!ctx.quiet) console.log(message);
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Log an error message (always shown).
|
|
119
|
-
*/
|
|
120
|
-
function logError(message) {
|
|
121
|
-
console.error(pc.red(`Error: ${message}`));
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Log a success message (hidden if --quiet is set).
|
|
125
|
-
*/
|
|
126
|
-
function logSuccess(ctx, message) {
|
|
127
|
-
if (!ctx.quiet) console.log(pc.green(message));
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Log a timing message (hidden if --quiet is set).
|
|
131
|
-
*/
|
|
132
|
-
function logTiming(ctx, label, durationMs) {
|
|
133
|
-
if (!ctx.quiet) {
|
|
134
|
-
const seconds = (durationMs / 1e3).toFixed(1);
|
|
135
|
-
console.log(pc.cyan(`⏰ ${label}: ${seconds}s`));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Log a warning message (hidden if --quiet is set).
|
|
140
|
-
*/
|
|
141
|
-
function logWarn(ctx, message) {
|
|
142
|
-
if (!ctx.quiet) console.log(pc.yellow(`⚠️ ${message}`));
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Format a file path for display: relative to cwd, colored green.
|
|
146
|
-
* If the path is within the cwd, shows as relative (e.g., "./simple-filled1.form.md")
|
|
147
|
-
* If outside cwd, shows the absolute path.
|
|
148
|
-
*/
|
|
149
|
-
function formatPath(absolutePath, cwd = process.cwd()) {
|
|
150
|
-
const relativePath = relative(cwd, absolutePath);
|
|
151
|
-
const displayPath = relativePath.startsWith("..") ? absolutePath : `./${relativePath}`;
|
|
152
|
-
return pc.green(displayPath);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Read a file and return its contents.
|
|
156
|
-
*/
|
|
157
|
-
async function readFile(filePath) {
|
|
158
|
-
const { readFile: fsReadFile } = await import("node:fs/promises");
|
|
159
|
-
return fsReadFile(filePath, "utf-8");
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Write contents to a file.
|
|
163
|
-
*/
|
|
164
|
-
async function writeFile(filePath, contents) {
|
|
165
|
-
const { writeFile: fsWriteFile } = await import("node:fs/promises");
|
|
166
|
-
await fsWriteFile(filePath, contents, "utf-8");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
//#endregion
|
|
170
17
|
//#region src/cli/commands/apply.ts
|
|
171
18
|
/**
|
|
172
19
|
* Format state badge for console output.
|
|
@@ -200,9 +47,9 @@ function formatConsoleReport$2(report, useColors) {
|
|
|
200
47
|
const counts = report.progress.counts;
|
|
201
48
|
lines.push(bold("Progress:"));
|
|
202
49
|
lines.push(` Total fields: ${counts.totalFields}`);
|
|
203
|
-
lines.push(`
|
|
204
|
-
lines.push(`
|
|
205
|
-
lines.push(` Empty
|
|
50
|
+
lines.push(` Valid: ${counts.validFields}, Invalid: ${counts.invalidFields}`);
|
|
51
|
+
lines.push(` Filled: ${counts.filledFields}, Empty: ${counts.emptyFields}`);
|
|
52
|
+
lines.push(` Empty required: ${counts.emptyRequiredFields}`);
|
|
206
53
|
lines.push("");
|
|
207
54
|
if (report.issues.length > 0) {
|
|
208
55
|
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
@@ -225,7 +72,7 @@ function registerApplyCommand(program) {
|
|
|
225
72
|
process.exit(1);
|
|
226
73
|
}
|
|
227
74
|
logVerbose(ctx, `Reading file: ${file}`);
|
|
228
|
-
const content = await readFile(file);
|
|
75
|
+
const content = await readFile$1(file);
|
|
229
76
|
logVerbose(ctx, "Parsing form...");
|
|
230
77
|
const form = parseForm(content);
|
|
231
78
|
logVerbose(ctx, "Parsing patches...");
|
|
@@ -297,78 +144,92 @@ function registerApplyCommand(program) {
|
|
|
297
144
|
}
|
|
298
145
|
|
|
299
146
|
//#endregion
|
|
300
|
-
//#region src/cli/commands/
|
|
147
|
+
//#region src/cli/commands/docs.ts
|
|
301
148
|
/**
|
|
302
|
-
*
|
|
149
|
+
* Get the path to the DOCS.md file.
|
|
150
|
+
* Works both during development and when installed as a package.
|
|
303
151
|
*/
|
|
304
|
-
function
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
switch (value.kind) {
|
|
309
|
-
case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
310
|
-
case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
311
|
-
case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
312
|
-
case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
|
|
313
|
-
case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
|
|
314
|
-
case "checkboxes": {
|
|
315
|
-
const entries = Object.entries(value.values);
|
|
316
|
-
if (entries.length === 0) return dim("(no entries)");
|
|
317
|
-
return entries.map(([k, v]) => `${k}:${v}`).join(", ");
|
|
318
|
-
}
|
|
319
|
-
default: return dim("(unknown)");
|
|
320
|
-
}
|
|
152
|
+
function getDocsPath() {
|
|
153
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
154
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "DOCS.md");
|
|
155
|
+
return join(dirname(dirname(dirname(thisDir))), "DOCS.md");
|
|
321
156
|
}
|
|
322
157
|
/**
|
|
323
|
-
*
|
|
158
|
+
* Load the docs content.
|
|
324
159
|
*/
|
|
325
|
-
function
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (lines.length === 0) {
|
|
333
|
-
const dim = useColors ? pc.dim : (s) => s;
|
|
334
|
-
lines.push(dim("(no values)"));
|
|
160
|
+
function loadDocs() {
|
|
161
|
+
const docsPath = getDocsPath();
|
|
162
|
+
try {
|
|
163
|
+
return readFileSync(docsPath, "utf-8");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
throw new Error(`Failed to load DOCS from ${docsPath}: ${message}`);
|
|
335
167
|
}
|
|
336
|
-
return lines.join("\n");
|
|
337
168
|
}
|
|
338
169
|
/**
|
|
339
|
-
*
|
|
170
|
+
* Apply basic terminal formatting to markdown content.
|
|
171
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
340
172
|
*/
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
173
|
+
function formatMarkdown$3(content, useColors) {
|
|
174
|
+
if (!useColors) return content;
|
|
175
|
+
const lines = content.split("\n");
|
|
176
|
+
const formatted = [];
|
|
177
|
+
let inCodeBlock = false;
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
if (line.startsWith("```")) {
|
|
180
|
+
inCodeBlock = !inCodeBlock;
|
|
181
|
+
formatted.push(pc.dim(line));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (inCodeBlock) {
|
|
185
|
+
formatted.push(pc.dim(line));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (line.startsWith("# ")) {
|
|
189
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (line.startsWith("## ")) {
|
|
193
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (line.startsWith("### ")) {
|
|
197
|
+
formatted.push(pc.bold(line));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
201
|
+
return pc.yellow(code);
|
|
202
|
+
});
|
|
203
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
204
|
+
return pc.bold(text);
|
|
205
|
+
});
|
|
206
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
207
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
208
|
+
});
|
|
209
|
+
formatted.push(formattedLine);
|
|
350
210
|
}
|
|
211
|
+
return formatted.join("\n");
|
|
351
212
|
}
|
|
352
213
|
/**
|
|
353
|
-
*
|
|
214
|
+
* Check if stdout is an interactive terminal.
|
|
354
215
|
*/
|
|
355
|
-
function
|
|
356
|
-
|
|
216
|
+
function isInteractive$3() {
|
|
217
|
+
return process.stdout.isTTY === true;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Display content.
|
|
221
|
+
*/
|
|
222
|
+
function displayContent$2(content) {
|
|
223
|
+
console.log(content);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Register the docs command.
|
|
227
|
+
*/
|
|
228
|
+
function registerDocsCommand(program) {
|
|
229
|
+
program.command("docs").description("Display concise Markform syntax reference (agent-friendly)").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
357
230
|
const ctx = getCommandContext(cmd);
|
|
358
231
|
try {
|
|
359
|
-
|
|
360
|
-
const content = await readFile(file);
|
|
361
|
-
logVerbose(ctx, "Parsing form...");
|
|
362
|
-
const form = parseForm(content);
|
|
363
|
-
if (ctx.format === "json" || ctx.format === "yaml") {
|
|
364
|
-
const plainValues = {};
|
|
365
|
-
for (const [fieldId, value] of Object.entries(form.valuesByFieldId)) plainValues[fieldId] = toPlainValue(value);
|
|
366
|
-
const output = formatOutput(ctx, plainValues, () => "");
|
|
367
|
-
console.log(output);
|
|
368
|
-
} else {
|
|
369
|
-
const output = formatOutput(ctx, form.valuesByFieldId, (data, useColors) => formatConsoleValues(data, useColors));
|
|
370
|
-
console.log(output);
|
|
371
|
-
}
|
|
232
|
+
displayContent$2(formatMarkdown$3(loadDocs(), !options.raw && isInteractive$3() && !ctx.quiet));
|
|
372
233
|
} catch (error) {
|
|
373
234
|
logError(error instanceof Error ? error.message : String(error));
|
|
374
235
|
process.exit(1);
|
|
@@ -387,46 +248,100 @@ function registerDumpCommand(program) {
|
|
|
387
248
|
* - YAML values (.yml) - extracted field values
|
|
388
249
|
*/
|
|
389
250
|
/**
|
|
390
|
-
* Convert field
|
|
251
|
+
* Convert field responses to structured format for export (markform-218).
|
|
391
252
|
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
253
|
+
* Includes state for all fields:
|
|
254
|
+
* - { state: 'unanswered' } for unfilled fields
|
|
255
|
+
* - { state: 'skipped' } for skipped fields
|
|
256
|
+
* - { state: 'aborted' } for aborted fields
|
|
257
|
+
* - { state: 'answered', value: ... } for answered fields
|
|
394
258
|
*/
|
|
395
|
-
function
|
|
259
|
+
function toStructuredValues(form) {
|
|
396
260
|
const result = {};
|
|
397
|
-
for (const [fieldId,
|
|
398
|
-
|
|
399
|
-
result[fieldId] =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
261
|
+
for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
|
|
262
|
+
if (!response || response.state === "unanswered") {
|
|
263
|
+
result[fieldId] = { state: "unanswered" };
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (response.state === "skipped") {
|
|
267
|
+
result[fieldId] = {
|
|
268
|
+
state: "skipped",
|
|
269
|
+
...response.reason && { reason: response.reason }
|
|
270
|
+
};
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (response.state === "aborted") {
|
|
274
|
+
result[fieldId] = {
|
|
275
|
+
state: "aborted",
|
|
276
|
+
...response.reason && { reason: response.reason }
|
|
277
|
+
};
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (!response.value) {
|
|
281
|
+
result[fieldId] = {
|
|
282
|
+
state: "answered",
|
|
283
|
+
value: null
|
|
284
|
+
};
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const value = response.value;
|
|
288
|
+
let exportValue;
|
|
289
|
+
switch (value.kind) {
|
|
290
|
+
case "string":
|
|
291
|
+
exportValue = value.value ?? null;
|
|
292
|
+
break;
|
|
293
|
+
case "number":
|
|
294
|
+
exportValue = value.value ?? null;
|
|
295
|
+
break;
|
|
296
|
+
case "string_list":
|
|
297
|
+
exportValue = value.items;
|
|
298
|
+
break;
|
|
299
|
+
case "single_select":
|
|
300
|
+
exportValue = value.selected ?? null;
|
|
301
|
+
break;
|
|
302
|
+
case "multi_select":
|
|
303
|
+
exportValue = value.selected;
|
|
304
|
+
break;
|
|
305
|
+
case "checkboxes":
|
|
306
|
+
exportValue = value.values;
|
|
307
|
+
break;
|
|
308
|
+
case "url":
|
|
309
|
+
exportValue = value.value ?? null;
|
|
310
|
+
break;
|
|
311
|
+
case "url_list":
|
|
312
|
+
exportValue = value.items;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
result[fieldId] = {
|
|
316
|
+
state: "answered",
|
|
317
|
+
value: exportValue
|
|
318
|
+
};
|
|
416
319
|
}
|
|
417
320
|
return result;
|
|
418
321
|
}
|
|
419
322
|
/**
|
|
323
|
+
* Convert notes to export format (markform-219).
|
|
324
|
+
*/
|
|
325
|
+
function toNotesArray(form) {
|
|
326
|
+
return form.notes.map((note) => ({
|
|
327
|
+
id: note.id,
|
|
328
|
+
ref: note.ref,
|
|
329
|
+
role: note.role,
|
|
330
|
+
text: note.text
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
420
334
|
* Derive export paths from a base form path.
|
|
335
|
+
* Uses centralized extension constants from settings.ts.
|
|
421
336
|
*
|
|
422
337
|
* @param basePath - Path to the .form.md file
|
|
423
338
|
* @returns Object with paths for all export formats
|
|
424
339
|
*/
|
|
425
340
|
function deriveExportPaths(basePath) {
|
|
426
341
|
return {
|
|
427
|
-
formPath: basePath,
|
|
428
|
-
rawPath: basePath
|
|
429
|
-
yamlPath: basePath
|
|
342
|
+
formPath: deriveExportPath(basePath, "form"),
|
|
343
|
+
rawPath: deriveExportPath(basePath, "raw"),
|
|
344
|
+
yamlPath: deriveExportPath(basePath, "yaml")
|
|
430
345
|
};
|
|
431
346
|
}
|
|
432
347
|
/**
|
|
@@ -435,24 +350,101 @@ function deriveExportPaths(basePath) {
|
|
|
435
350
|
* Writes:
|
|
436
351
|
* - Markform format (.form.md) - canonical form with directives
|
|
437
352
|
* - Raw markdown (.raw.md) - plain readable markdown (no directives)
|
|
438
|
-
* - YAML values (.yml) -
|
|
353
|
+
* - YAML values (.yml) - structured format with state and notes (markform-218, markform-219)
|
|
439
354
|
*
|
|
440
355
|
* @param form - The parsed form to export
|
|
441
356
|
* @param basePath - Base path for the .form.md file (other paths are derived)
|
|
442
357
|
* @returns Paths to all exported files
|
|
443
358
|
*/
|
|
444
|
-
function exportMultiFormat(form, basePath) {
|
|
359
|
+
async function exportMultiFormat(form, basePath) {
|
|
445
360
|
const paths = deriveExportPaths(basePath);
|
|
446
361
|
const formContent = serialize(form);
|
|
447
|
-
|
|
362
|
+
await writeFile(paths.formPath, formContent);
|
|
448
363
|
const rawContent = serializeRawMarkdown(form);
|
|
449
|
-
|
|
450
|
-
const values =
|
|
451
|
-
const
|
|
452
|
-
|
|
364
|
+
await writeFile(paths.rawPath, rawContent);
|
|
365
|
+
const values = toStructuredValues(form);
|
|
366
|
+
const notes = toNotesArray(form);
|
|
367
|
+
const exportData = {
|
|
368
|
+
values,
|
|
369
|
+
...notes.length > 0 && { notes }
|
|
370
|
+
};
|
|
371
|
+
const yamlContent = YAML.stringify(exportData);
|
|
372
|
+
await writeFile(paths.yamlPath, yamlContent);
|
|
453
373
|
return paths;
|
|
454
374
|
}
|
|
455
375
|
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/cli/commands/dump.ts
|
|
378
|
+
/**
|
|
379
|
+
* Format a field response for console display, including state information.
|
|
380
|
+
*/
|
|
381
|
+
function formatFieldResponse(response, useColors) {
|
|
382
|
+
const dim = useColors ? pc.dim : (s) => s;
|
|
383
|
+
const green = useColors ? pc.green : (s) => s;
|
|
384
|
+
const yellow = useColors ? pc.yellow : (s) => s;
|
|
385
|
+
if (response.state === "unanswered") return dim("(unanswered)");
|
|
386
|
+
if (response.state === "skipped") return yellow(`[skipped]${response.reason ? ` ${response.reason}` : ""}`);
|
|
387
|
+
if (response.state === "aborted") return yellow(`[aborted]${response.reason ? ` ${response.reason}` : ""}`);
|
|
388
|
+
const value = response.value;
|
|
389
|
+
if (!value) return dim("(empty)");
|
|
390
|
+
switch (value.kind) {
|
|
391
|
+
case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
392
|
+
case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
393
|
+
case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
394
|
+
case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
|
|
395
|
+
case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
|
|
396
|
+
case "checkboxes": {
|
|
397
|
+
const entries = Object.entries(value.values);
|
|
398
|
+
if (entries.length === 0) return dim("(no entries)");
|
|
399
|
+
return entries.map(([k, v]) => `${k}:${v}`).join(", ");
|
|
400
|
+
}
|
|
401
|
+
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
402
|
+
case "url_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
403
|
+
default: return dim("(unknown)");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Format form responses for console output, showing all fields with their states.
|
|
408
|
+
*/
|
|
409
|
+
function formatConsoleResponses(form, useColors) {
|
|
410
|
+
const lines = [];
|
|
411
|
+
const bold = useColors ? pc.bold : (s) => s;
|
|
412
|
+
const dim = useColors ? pc.dim : (s) => s;
|
|
413
|
+
for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
|
|
414
|
+
const valueStr = formatFieldResponse(response, useColors);
|
|
415
|
+
lines.push(`${bold(fieldId)}: ${valueStr}`);
|
|
416
|
+
}
|
|
417
|
+
if (lines.length === 0) lines.push(dim("(no fields)"));
|
|
418
|
+
return lines.join("\n");
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Register the dump command.
|
|
422
|
+
*/
|
|
423
|
+
function registerDumpCommand(program) {
|
|
424
|
+
program.command("dump <file>").description("Extract and display form values with state (lightweight inspect)").action(async (file, _options, cmd) => {
|
|
425
|
+
const ctx = getCommandContext(cmd);
|
|
426
|
+
try {
|
|
427
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
428
|
+
const content = await readFile$1(file);
|
|
429
|
+
logVerbose(ctx, "Parsing form...");
|
|
430
|
+
const form = parseForm(content);
|
|
431
|
+
if (ctx.format === "json" || ctx.format === "yaml") {
|
|
432
|
+
const output = formatOutput(ctx, {
|
|
433
|
+
values: toStructuredValues(form),
|
|
434
|
+
...form.notes.length > 0 && { notes: toNotesArray(form) }
|
|
435
|
+
}, () => "");
|
|
436
|
+
console.log(output);
|
|
437
|
+
} else {
|
|
438
|
+
const output = formatOutput(ctx, form, (data, useColors) => formatConsoleResponses(data, useColors));
|
|
439
|
+
console.log(output);
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
456
448
|
//#endregion
|
|
457
449
|
//#region src/cli/lib/patchFormat.ts
|
|
458
450
|
/** Maximum characters for a patch value display before truncation */
|
|
@@ -477,8 +469,13 @@ function formatPatchValue(patch) {
|
|
|
477
469
|
case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
|
|
478
470
|
case "clear_field": return "(cleared)";
|
|
479
471
|
case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
|
|
472
|
+
case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
|
|
480
473
|
case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
481
474
|
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
475
|
+
case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
476
|
+
case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
477
|
+
case "add_note": return truncate(`note: ${patch.text}`);
|
|
478
|
+
case "remove_note": return `(remove note ${patch.noteId})`;
|
|
482
479
|
}
|
|
483
480
|
}
|
|
484
481
|
/**
|
|
@@ -494,46 +491,103 @@ function formatPatchType(patch) {
|
|
|
494
491
|
case "set_checkboxes": return "checkboxes";
|
|
495
492
|
case "clear_field": return "clear";
|
|
496
493
|
case "skip_field": return "skip";
|
|
494
|
+
case "abort_field": return "abort";
|
|
497
495
|
case "set_url": return "url";
|
|
498
496
|
case "set_url_list": return "url_list";
|
|
497
|
+
case "set_date": return "date";
|
|
498
|
+
case "set_year": return "year";
|
|
499
|
+
case "add_note": return "note";
|
|
500
|
+
case "remove_note": return "remove_note";
|
|
499
501
|
}
|
|
500
502
|
}
|
|
501
503
|
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/cli/lib/formatting.ts
|
|
506
|
+
/**
|
|
507
|
+
* Get a short status word from an issue reason.
|
|
508
|
+
*/
|
|
509
|
+
function issueReasonToStatus(reason) {
|
|
510
|
+
switch (reason) {
|
|
511
|
+
case "required_missing": return "missing";
|
|
512
|
+
case "validation_error": return "invalid";
|
|
513
|
+
case "checkbox_incomplete": return "incomplete";
|
|
514
|
+
case "min_items_not_met": return "too-few";
|
|
515
|
+
case "optional_empty": return "empty";
|
|
516
|
+
default: return "issue";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Format a single issue as "fieldId (status)".
|
|
521
|
+
*/
|
|
522
|
+
function formatIssueBrief(issue) {
|
|
523
|
+
const status = issueReasonToStatus(issue.reason);
|
|
524
|
+
return `${issue.ref} (${status})`;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Format issues for turn logging - shows count and brief field list.
|
|
528
|
+
* Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
|
|
529
|
+
*/
|
|
530
|
+
function formatTurnIssues(issues, maxShow = 5) {
|
|
531
|
+
const count = issues.length;
|
|
532
|
+
if (count === 0) return "0 issues";
|
|
533
|
+
return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
502
536
|
//#endregion
|
|
503
537
|
//#region src/cli/examples/exampleRegistry.ts
|
|
504
538
|
/**
|
|
505
539
|
* Example form registry.
|
|
506
540
|
* Provides form content from the examples directory for the examples CLI command.
|
|
541
|
+
*
|
|
542
|
+
* Metadata (title, description) is loaded dynamically from the form's YAML frontmatter
|
|
543
|
+
* rather than being duplicated here, following the single source of truth principle.
|
|
544
|
+
*/
|
|
545
|
+
/**
|
|
546
|
+
* Example definitions without content or metadata.
|
|
547
|
+
* Title and description are loaded dynamically from frontmatter.
|
|
507
548
|
*/
|
|
508
|
-
/** Example definitions without content - content is loaded lazily. */
|
|
509
549
|
const EXAMPLE_DEFINITIONS = [
|
|
510
550
|
{
|
|
511
551
|
id: "simple",
|
|
512
|
-
title: "Simple Test Form",
|
|
513
|
-
description: "User and agent roles for testing full workflow. User fills required fields, agent fills optional.",
|
|
514
552
|
filename: "simple.form.md",
|
|
515
|
-
path: "simple/simple.form.md"
|
|
553
|
+
path: "simple/simple.form.md",
|
|
554
|
+
type: "fill"
|
|
516
555
|
},
|
|
517
556
|
{
|
|
518
|
-
id: "
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
557
|
+
id: "movie-research-minimal",
|
|
558
|
+
filename: "movie-research-minimal.form.md",
|
|
559
|
+
path: "movie-research/movie-research-minimal.form.md",
|
|
560
|
+
type: "research"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
id: "movie-research-basic",
|
|
564
|
+
filename: "movie-research-basic.form.md",
|
|
565
|
+
path: "movie-research/movie-research-basic.form.md",
|
|
566
|
+
type: "research"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "movie-research-deep",
|
|
570
|
+
filename: "movie-research-deep.form.md",
|
|
571
|
+
path: "movie-research/movie-research-deep.form.md",
|
|
572
|
+
type: "research"
|
|
523
573
|
},
|
|
524
574
|
{
|
|
525
575
|
id: "earnings-analysis",
|
|
526
|
-
title: "Company Quarterly Analysis",
|
|
527
|
-
description: "Financial analysis with one user field (company) and agent-filled quarterly analysis sections.",
|
|
528
576
|
filename: "earnings-analysis.form.md",
|
|
529
|
-
path: "earnings-analysis/earnings-analysis.form.md"
|
|
577
|
+
path: "earnings-analysis/earnings-analysis.form.md",
|
|
578
|
+
type: "research"
|
|
530
579
|
},
|
|
531
580
|
{
|
|
532
581
|
id: "startup-deep-research",
|
|
533
|
-
title: "Startup Deep Research",
|
|
534
|
-
description: "Comprehensive startup intelligence gathering with company info, founders, funding, competitors, social media, and community presence.",
|
|
535
582
|
filename: "startup-deep-research.form.md",
|
|
536
|
-
path: "startup-deep-research/startup-deep-research.form.md"
|
|
583
|
+
path: "startup-deep-research/startup-deep-research.form.md",
|
|
584
|
+
type: "research"
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
id: "celebrity-deep-research",
|
|
588
|
+
filename: "celebrity-deep-research.form.md",
|
|
589
|
+
path: "celebrity-deep-research/celebrity-deep-research.form.md",
|
|
590
|
+
type: "research"
|
|
537
591
|
}
|
|
538
592
|
];
|
|
539
593
|
/**
|
|
@@ -547,7 +601,7 @@ function getExamplesDir() {
|
|
|
547
601
|
}
|
|
548
602
|
/**
|
|
549
603
|
* Load the content of an example form.
|
|
550
|
-
* @param exampleId - The example ID (e.g., 'simple', '
|
|
604
|
+
* @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
|
|
551
605
|
* @returns The form content as a string
|
|
552
606
|
* @throws Error if the example is not found
|
|
553
607
|
*/
|
|
@@ -569,7 +623,7 @@ function getExampleById(id) {
|
|
|
569
623
|
}
|
|
570
624
|
/**
|
|
571
625
|
* Get the absolute path to an example's source file.
|
|
572
|
-
* @param exampleId - The example ID (e.g., 'simple', '
|
|
626
|
+
* @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
|
|
573
627
|
* @returns The absolute path to the example form file
|
|
574
628
|
* @throws Error if the example is not found
|
|
575
629
|
*/
|
|
@@ -578,6 +632,48 @@ function getExamplePath(exampleId) {
|
|
|
578
632
|
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
579
633
|
return join(getExamplesDir(), example.path);
|
|
580
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Extract YAML frontmatter from a markdown file content.
|
|
637
|
+
* @param content - The markdown file content
|
|
638
|
+
* @returns The parsed frontmatter object or null if no frontmatter found
|
|
639
|
+
*/
|
|
640
|
+
function extractFrontmatter(content) {
|
|
641
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
642
|
+
if (!frontmatterMatch || !frontmatterMatch[1]) return null;
|
|
643
|
+
try {
|
|
644
|
+
return YAML.parse(frontmatterMatch[1]);
|
|
645
|
+
} catch {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Load metadata (title, description) from an example's YAML frontmatter.
|
|
651
|
+
* @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
|
|
652
|
+
* @returns Object with title and description from frontmatter
|
|
653
|
+
*/
|
|
654
|
+
function loadExampleMetadata(exampleId) {
|
|
655
|
+
const frontmatter = extractFrontmatter(loadExampleContent(exampleId));
|
|
656
|
+
if (!frontmatter || !frontmatter.markform) return {};
|
|
657
|
+
const markform = frontmatter.markform;
|
|
658
|
+
return {
|
|
659
|
+
title: typeof markform.title === "string" ? markform.title : void 0,
|
|
660
|
+
description: typeof markform.description === "string" ? markform.description : void 0
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get all example definitions with metadata loaded from frontmatter.
|
|
665
|
+
* @returns Array of ExampleDefinition with title and description populated
|
|
666
|
+
*/
|
|
667
|
+
function getAllExamplesWithMetadata() {
|
|
668
|
+
return EXAMPLE_DEFINITIONS.map((example) => {
|
|
669
|
+
const metadata = loadExampleMetadata(example.id);
|
|
670
|
+
return {
|
|
671
|
+
...example,
|
|
672
|
+
title: metadata.title,
|
|
673
|
+
description: metadata.description
|
|
674
|
+
};
|
|
675
|
+
});
|
|
676
|
+
}
|
|
581
677
|
|
|
582
678
|
//#endregion
|
|
583
679
|
//#region src/cli/lib/versioning.ts
|
|
@@ -654,6 +750,29 @@ function generateVersionedPath(filePath) {
|
|
|
654
750
|
}
|
|
655
751
|
return candidate;
|
|
656
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Generate a versioned filename within the forms directory.
|
|
755
|
+
*
|
|
756
|
+
* Derives the base name from the input path and creates a versioned
|
|
757
|
+
* output path within the specified forms directory.
|
|
758
|
+
*
|
|
759
|
+
* @param inputPath - Original input file path (used to derive basename)
|
|
760
|
+
* @param formsDir - Absolute path to the forms directory
|
|
761
|
+
* @returns Absolute path to a non-existent versioned file in formsDir
|
|
762
|
+
*/
|
|
763
|
+
function generateVersionedPathInFormsDir(inputPath, formsDir) {
|
|
764
|
+
const inputFilename = basename(inputPath);
|
|
765
|
+
const parsed = parseVersionedPath(inputFilename);
|
|
766
|
+
const baseName = parsed?.base ?? inputFilename.replace(/\.form\.md$/i, "");
|
|
767
|
+
const extension = parsed?.extension ?? ".form.md";
|
|
768
|
+
let version = 1;
|
|
769
|
+
let candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
|
|
770
|
+
while (existsSync(candidate)) {
|
|
771
|
+
version++;
|
|
772
|
+
candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
|
|
773
|
+
}
|
|
774
|
+
return candidate;
|
|
775
|
+
}
|
|
657
776
|
|
|
658
777
|
//#endregion
|
|
659
778
|
//#region src/cli/lib/interactivePrompts.ts
|
|
@@ -683,7 +802,7 @@ function getFieldById(form, fieldId) {
|
|
|
683
802
|
*/
|
|
684
803
|
function formatFieldLabel(ctx) {
|
|
685
804
|
const required = ctx.field.required ? pc.red("*") : "";
|
|
686
|
-
const progress =
|
|
805
|
+
const progress = `(${ctx.index} of ${ctx.total})`;
|
|
687
806
|
return `${ctx.field.label}${required} ${progress}`;
|
|
688
807
|
}
|
|
689
808
|
/**
|
|
@@ -693,6 +812,7 @@ function createSkipPatch(field) {
|
|
|
693
812
|
return {
|
|
694
813
|
op: "skip_field",
|
|
695
814
|
fieldId: field.id,
|
|
815
|
+
role: "user",
|
|
696
816
|
reason: "User skipped in console"
|
|
697
817
|
};
|
|
698
818
|
}
|
|
@@ -1057,9 +1177,10 @@ async function runInteractiveFill(form, issues) {
|
|
|
1057
1177
|
const field = getFieldById(form, issue.ref);
|
|
1058
1178
|
if (!field) continue;
|
|
1059
1179
|
index++;
|
|
1180
|
+
const response = form.responsesByFieldId[field.id];
|
|
1060
1181
|
const patch = await promptForField({
|
|
1061
1182
|
field,
|
|
1062
|
-
currentValue:
|
|
1183
|
+
currentValue: response?.state === "answered" ? response.value : void 0,
|
|
1063
1184
|
description: getFieldDescription(form, field.id),
|
|
1064
1185
|
index,
|
|
1065
1186
|
total: uniqueFieldIssues.length
|
|
@@ -1110,6 +1231,183 @@ function showInteractiveOutro(patchCount, cancelled) {
|
|
|
1110
1231
|
p.outro(`✓ ${patchCount} field(s) updated.`);
|
|
1111
1232
|
}
|
|
1112
1233
|
|
|
1234
|
+
//#endregion
|
|
1235
|
+
//#region src/cli/lib/fileViewer.ts
|
|
1236
|
+
/**
|
|
1237
|
+
* File viewer utility for displaying files with colorization and pagination.
|
|
1238
|
+
*
|
|
1239
|
+
* Provides a modern console experience:
|
|
1240
|
+
* - Syntax highlighting for markdown and YAML
|
|
1241
|
+
* - Pagination using system pager (less) when available
|
|
1242
|
+
* - Fallback to console output when not interactive
|
|
1243
|
+
*/
|
|
1244
|
+
/**
|
|
1245
|
+
* Check if stdout is an interactive terminal.
|
|
1246
|
+
*/
|
|
1247
|
+
function isInteractive$2() {
|
|
1248
|
+
return process.stdout.isTTY === true;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Apply terminal formatting to markdown content.
|
|
1252
|
+
* Colorizes headers, code blocks, and other elements.
|
|
1253
|
+
*/
|
|
1254
|
+
function formatMarkdown$2(content) {
|
|
1255
|
+
const lines = content.split("\n");
|
|
1256
|
+
const formatted = [];
|
|
1257
|
+
let inCodeBlock = false;
|
|
1258
|
+
for (const line of lines) {
|
|
1259
|
+
if (line.startsWith("```")) {
|
|
1260
|
+
inCodeBlock = !inCodeBlock;
|
|
1261
|
+
formatted.push(pc.dim(line));
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
if (inCodeBlock) {
|
|
1265
|
+
formatted.push(pc.cyan(line));
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
if (line.startsWith("# ")) {
|
|
1269
|
+
formatted.push(pc.bold(pc.magenta(line)));
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
if (line.startsWith("## ")) {
|
|
1273
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (line.startsWith("### ")) {
|
|
1277
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (line.startsWith("#### ")) {
|
|
1281
|
+
formatted.push(pc.bold(line));
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1285
|
+
return pc.yellow(code);
|
|
1286
|
+
});
|
|
1287
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
1288
|
+
return pc.bold(text);
|
|
1289
|
+
});
|
|
1290
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
1291
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
1292
|
+
});
|
|
1293
|
+
formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
|
|
1294
|
+
return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
|
|
1295
|
+
});
|
|
1296
|
+
formatted.push(formattedLine);
|
|
1297
|
+
}
|
|
1298
|
+
return formatted.join("\n");
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Apply terminal formatting to YAML content.
|
|
1302
|
+
*/
|
|
1303
|
+
function formatYaml(content) {
|
|
1304
|
+
const lines = content.split("\n");
|
|
1305
|
+
const formatted = [];
|
|
1306
|
+
for (const line of lines) {
|
|
1307
|
+
if (line.trim().startsWith("#")) {
|
|
1308
|
+
formatted.push(pc.dim(line));
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
|
|
1312
|
+
if (match) {
|
|
1313
|
+
const [, indent, key, colon, value] = match;
|
|
1314
|
+
formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (line.trim().startsWith("-")) {
|
|
1318
|
+
formatted.push(pc.green(line));
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
formatted.push(line);
|
|
1322
|
+
}
|
|
1323
|
+
return formatted.join("\n");
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Format file content based on extension.
|
|
1327
|
+
*/
|
|
1328
|
+
function formatContent(content, filename) {
|
|
1329
|
+
if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
|
|
1330
|
+
if (filename.endsWith(".md")) return formatMarkdown$2(content);
|
|
1331
|
+
return content;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Display content using system pager (less) if available.
|
|
1335
|
+
* Falls back to console.log if not interactive or pager unavailable.
|
|
1336
|
+
*
|
|
1337
|
+
* @returns Promise that resolves when viewing is complete
|
|
1338
|
+
*/
|
|
1339
|
+
async function displayWithPager(content, title) {
|
|
1340
|
+
if (!isInteractive$2()) {
|
|
1341
|
+
console.log(content);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
|
|
1345
|
+
return new Promise((resolve$1) => {
|
|
1346
|
+
const pager = spawn("less", [
|
|
1347
|
+
"-R",
|
|
1348
|
+
"-S",
|
|
1349
|
+
"-X",
|
|
1350
|
+
"-F",
|
|
1351
|
+
"-K"
|
|
1352
|
+
], { stdio: [
|
|
1353
|
+
"pipe",
|
|
1354
|
+
"inherit",
|
|
1355
|
+
"inherit"
|
|
1356
|
+
] });
|
|
1357
|
+
pager.on("error", () => {
|
|
1358
|
+
console.log(header);
|
|
1359
|
+
console.log("");
|
|
1360
|
+
console.log(content);
|
|
1361
|
+
console.log("");
|
|
1362
|
+
resolve$1();
|
|
1363
|
+
});
|
|
1364
|
+
pager.on("close", () => {
|
|
1365
|
+
resolve$1();
|
|
1366
|
+
});
|
|
1367
|
+
pager.stdin.write(header + "\n\n");
|
|
1368
|
+
pager.stdin.write(content);
|
|
1369
|
+
pager.stdin.end();
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Load and display a file with formatting and pagination.
|
|
1374
|
+
*/
|
|
1375
|
+
async function viewFile(filePath) {
|
|
1376
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1377
|
+
const filename = basename(filePath);
|
|
1378
|
+
await displayWithPager(formatContent(content, filename), filename);
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Show an interactive file viewer chooser.
|
|
1382
|
+
*
|
|
1383
|
+
* Presents a list of files to view with descriptions, plus a Skip option.
|
|
1384
|
+
* Loops until the user selects Skip.
|
|
1385
|
+
*
|
|
1386
|
+
* @param files Array of file options to display
|
|
1387
|
+
*/
|
|
1388
|
+
async function showFileViewerChooser(files) {
|
|
1389
|
+
if (!isInteractive$2()) return;
|
|
1390
|
+
console.log("");
|
|
1391
|
+
while (true) {
|
|
1392
|
+
const options = [...files.map((file) => ({
|
|
1393
|
+
value: file.path,
|
|
1394
|
+
label: pc.green(basename(file.path)),
|
|
1395
|
+
hint: file.hint ?? ""
|
|
1396
|
+
})), {
|
|
1397
|
+
value: "skip",
|
|
1398
|
+
label: "Done viewing",
|
|
1399
|
+
hint: "exit file viewer"
|
|
1400
|
+
}];
|
|
1401
|
+
const selection = await p.select({
|
|
1402
|
+
message: "View an output file?",
|
|
1403
|
+
options
|
|
1404
|
+
});
|
|
1405
|
+
if (p.isCancel(selection) || selection === "skip") break;
|
|
1406
|
+
await viewFile(selection);
|
|
1407
|
+
console.log("");
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1113
1411
|
//#endregion
|
|
1114
1412
|
//#region src/cli/commands/examples.ts
|
|
1115
1413
|
/**
|
|
@@ -1117,10 +1415,12 @@ function showInteractiveOutro(patchCount, cancelled) {
|
|
|
1117
1415
|
*/
|
|
1118
1416
|
function printExamplesList() {
|
|
1119
1417
|
console.log(pc.bold("Available examples:\n"));
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
console.log(`
|
|
1418
|
+
const examples = getAllExamplesWithMetadata();
|
|
1419
|
+
for (const example of examples) {
|
|
1420
|
+
const typeLabel = example.type === "research" ? pc.magenta("[research]") : pc.blue("[fill]");
|
|
1421
|
+
console.log(` ${pc.cyan(example.id)} ${typeLabel}`);
|
|
1422
|
+
console.log(` ${pc.bold(example.title ?? example.id)}`);
|
|
1423
|
+
console.log(` ${example.description ?? "No description"}`);
|
|
1124
1424
|
console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
|
|
1125
1425
|
console.log("");
|
|
1126
1426
|
}
|
|
@@ -1129,12 +1429,12 @@ function printExamplesList() {
|
|
|
1129
1429
|
* Display API availability status at startup.
|
|
1130
1430
|
*/
|
|
1131
1431
|
function showApiStatus() {
|
|
1132
|
-
console.log(
|
|
1432
|
+
console.log("API Status:");
|
|
1133
1433
|
for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1134
1434
|
const info = getProviderInfo(provider);
|
|
1135
1435
|
const hasKey = !!process.env[info.envVar];
|
|
1136
|
-
const status = hasKey ? pc.green("✓") :
|
|
1137
|
-
const envVar = hasKey ?
|
|
1436
|
+
const status = hasKey ? pc.green("✓") : "○";
|
|
1437
|
+
const envVar = hasKey ? info.envVar : pc.yellow(info.envVar);
|
|
1138
1438
|
console.log(` ${status} ${provider} (${envVar})`);
|
|
1139
1439
|
}
|
|
1140
1440
|
console.log("");
|
|
@@ -1146,7 +1446,7 @@ function buildModelOptions() {
|
|
|
1146
1446
|
const options = [];
|
|
1147
1447
|
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1148
1448
|
const info = getProviderInfo(provider);
|
|
1149
|
-
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") :
|
|
1449
|
+
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
|
|
1150
1450
|
for (const model of models) options.push({
|
|
1151
1451
|
value: `${provider}/${model}`,
|
|
1152
1452
|
label: `${provider}/${model}`,
|
|
@@ -1184,39 +1484,91 @@ async function promptForModel() {
|
|
|
1184
1484
|
return selection;
|
|
1185
1485
|
}
|
|
1186
1486
|
/**
|
|
1487
|
+
* Build model options filtered to providers with web search support.
|
|
1488
|
+
*/
|
|
1489
|
+
function buildWebSearchModelOptions() {
|
|
1490
|
+
const options = [];
|
|
1491
|
+
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1492
|
+
if (!hasWebSearchSupport(provider)) continue;
|
|
1493
|
+
const info = getProviderInfo(provider);
|
|
1494
|
+
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
|
|
1495
|
+
for (const model of models) options.push({
|
|
1496
|
+
value: `${provider}/${model}`,
|
|
1497
|
+
label: `${provider}/${model}`,
|
|
1498
|
+
hint: `${keyStatus} ${info.envVar}`
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
options.push({
|
|
1502
|
+
value: "custom",
|
|
1503
|
+
label: "Enter custom model ID...",
|
|
1504
|
+
hint: "provider/model-id format"
|
|
1505
|
+
});
|
|
1506
|
+
return options;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Prompt user to select a model with web search capability for research workflow.
|
|
1510
|
+
*/
|
|
1511
|
+
async function promptForWebSearchModel() {
|
|
1512
|
+
const modelOptions = buildWebSearchModelOptions();
|
|
1513
|
+
if (modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
|
|
1514
|
+
const selection = await p.select({
|
|
1515
|
+
message: "Select LLM model (web search required):",
|
|
1516
|
+
options: modelOptions
|
|
1517
|
+
});
|
|
1518
|
+
if (p.isCancel(selection)) return null;
|
|
1519
|
+
if (selection === "custom") {
|
|
1520
|
+
const customModel = await p.text({
|
|
1521
|
+
message: "Model ID (provider/model-id):",
|
|
1522
|
+
placeholder: "openai/gpt-5-mini",
|
|
1523
|
+
validate: (value) => {
|
|
1524
|
+
if (!value.includes("/")) return "Format: provider/model-id (e.g., openai/gpt-5-mini)";
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
if (p.isCancel(customModel)) return null;
|
|
1528
|
+
return customModel;
|
|
1529
|
+
}
|
|
1530
|
+
return selection;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1187
1533
|
* Run the agent fill workflow.
|
|
1534
|
+
* Accepts optional harness config overrides - research uses different defaults.
|
|
1188
1535
|
*/
|
|
1189
|
-
async function runAgentFill(form, modelId, _outputPath) {
|
|
1536
|
+
async function runAgentFill(form, modelId, _outputPath, configOverrides) {
|
|
1190
1537
|
const spinner = p.spinner();
|
|
1191
1538
|
try {
|
|
1192
1539
|
spinner.start(`Resolving model: ${modelId}`);
|
|
1193
|
-
const { model } = await resolveModel(modelId);
|
|
1540
|
+
const { model, provider } = await resolveModel(modelId);
|
|
1194
1541
|
spinner.stop(`Model resolved: ${modelId}`);
|
|
1195
1542
|
const harnessConfig = {
|
|
1196
|
-
maxTurns: DEFAULT_MAX_TURNS,
|
|
1197
|
-
maxPatchesPerTurn: DEFAULT_MAX_PATCHES_PER_TURN,
|
|
1198
|
-
|
|
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,
|
|
1199
1546
|
targetRoles: [AGENT_ROLE],
|
|
1200
1547
|
fillMode: "continue"
|
|
1201
1548
|
};
|
|
1549
|
+
console.log("");
|
|
1550
|
+
console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
|
|
1202
1551
|
const harness = createHarness(form, harnessConfig);
|
|
1203
1552
|
const agent = createLiveAgent({
|
|
1204
1553
|
model,
|
|
1554
|
+
provider,
|
|
1205
1555
|
targetRole: AGENT_ROLE
|
|
1206
1556
|
});
|
|
1207
|
-
console.log("");
|
|
1208
1557
|
p.log.step(pc.bold("Agent fill in progress..."));
|
|
1209
1558
|
let stepResult = harness.step();
|
|
1210
1559
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1211
|
-
console.log(pc.
|
|
1212
|
-
const { patches } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1560
|
+
console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1561
|
+
const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1213
1562
|
for (const patch of patches) {
|
|
1214
1563
|
const typeName = formatPatchType(patch);
|
|
1215
1564
|
const value = formatPatchValue(patch);
|
|
1216
|
-
|
|
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)}`);
|
|
1217
1568
|
}
|
|
1218
1569
|
stepResult = harness.apply(patches, stepResult.issues);
|
|
1219
|
-
|
|
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}`);
|
|
1220
1572
|
if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
|
|
1221
1573
|
}
|
|
1222
1574
|
if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
|
|
@@ -1233,18 +1585,24 @@ async function runAgentFill(form, modelId, _outputPath) {
|
|
|
1233
1585
|
}
|
|
1234
1586
|
/**
|
|
1235
1587
|
* Run the interactive example scaffolding and filling flow.
|
|
1588
|
+
*
|
|
1589
|
+
* @param preselectedId Optional example ID to pre-select
|
|
1590
|
+
* @param formsDirOverride Optional forms directory override from CLI option
|
|
1236
1591
|
*/
|
|
1237
|
-
async function runInteractiveFlow(preselectedId) {
|
|
1592
|
+
async function runInteractiveFlow(preselectedId, formsDirOverride) {
|
|
1238
1593
|
const startTime = Date.now();
|
|
1239
1594
|
p.intro(pc.bgCyan(pc.black(" markform examples ")));
|
|
1595
|
+
const formsDir = getFormsDir(formsDirOverride);
|
|
1596
|
+
await ensureFormsDir(formsDir);
|
|
1240
1597
|
showApiStatus();
|
|
1241
1598
|
let selectedId = preselectedId;
|
|
1242
1599
|
if (!selectedId) {
|
|
1600
|
+
const examples = getAllExamplesWithMetadata();
|
|
1243
1601
|
const selection = await p.select({
|
|
1244
1602
|
message: "Select an example form to scaffold:",
|
|
1245
|
-
options:
|
|
1603
|
+
options: examples.map((example$1) => ({
|
|
1246
1604
|
value: example$1.id,
|
|
1247
|
-
label: example$1.title,
|
|
1605
|
+
label: example$1.title ?? example$1.id,
|
|
1248
1606
|
hint: example$1.description
|
|
1249
1607
|
}))
|
|
1250
1608
|
});
|
|
@@ -1259,9 +1617,9 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1259
1617
|
p.cancel(`Unknown example: ${selectedId}`);
|
|
1260
1618
|
process.exit(1);
|
|
1261
1619
|
}
|
|
1262
|
-
const defaultFilename =
|
|
1620
|
+
const defaultFilename = basename(generateVersionedPathInFormsDir(example.filename, formsDir));
|
|
1263
1621
|
const filenameResult = await p.text({
|
|
1264
|
-
message:
|
|
1622
|
+
message: `Output filename (in ${formatPath(formsDir)}):`,
|
|
1265
1623
|
initialValue: defaultFilename,
|
|
1266
1624
|
validate: (value) => {
|
|
1267
1625
|
if (!value.trim()) return "Filename is required";
|
|
@@ -1273,7 +1631,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1273
1631
|
process.exit(0);
|
|
1274
1632
|
}
|
|
1275
1633
|
const filename = filenameResult;
|
|
1276
|
-
const outputPath = join(
|
|
1634
|
+
const outputPath = join(formsDir, filename);
|
|
1277
1635
|
if (existsSync(outputPath)) {
|
|
1278
1636
|
const overwrite = await p.confirm({
|
|
1279
1637
|
message: `${filename} already exists. Overwrite?`,
|
|
@@ -1287,7 +1645,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1287
1645
|
let content;
|
|
1288
1646
|
try {
|
|
1289
1647
|
content = loadExampleContent(selectedId);
|
|
1290
|
-
|
|
1648
|
+
await writeFile(outputPath, content);
|
|
1291
1649
|
} catch (error) {
|
|
1292
1650
|
const message = error instanceof Error ? error.message : String(error);
|
|
1293
1651
|
p.cancel(`Failed to write file: ${message}`);
|
|
@@ -1296,6 +1654,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1296
1654
|
p.log.success(`Created ${formatPath(outputPath)}`);
|
|
1297
1655
|
const form = parseForm(content);
|
|
1298
1656
|
const targetRoles = [USER_ROLE];
|
|
1657
|
+
let userFillOutputs = null;
|
|
1299
1658
|
const inspectResult = inspect(form, { targetRoles });
|
|
1300
1659
|
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
1301
1660
|
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
@@ -1308,7 +1667,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1308
1667
|
dryRun: false,
|
|
1309
1668
|
quiet: false
|
|
1310
1669
|
}, "Total time", Date.now() - startTime);
|
|
1311
|
-
p.outro(
|
|
1670
|
+
p.outro("Form scaffolded with no fields to fill.");
|
|
1312
1671
|
return;
|
|
1313
1672
|
}
|
|
1314
1673
|
} else {
|
|
@@ -1319,13 +1678,13 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1319
1678
|
process.exit(1);
|
|
1320
1679
|
}
|
|
1321
1680
|
if (patches.length > 0) applyPatches(form, patches);
|
|
1322
|
-
|
|
1681
|
+
userFillOutputs = await exportMultiFormat(form, outputPath);
|
|
1323
1682
|
showInteractiveOutro(patches.length, false);
|
|
1324
1683
|
console.log("");
|
|
1325
1684
|
p.log.success("Outputs:");
|
|
1326
|
-
console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
|
|
1327
|
-
console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
|
|
1328
|
-
console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
|
|
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)")}`);
|
|
1329
1688
|
logTiming({
|
|
1330
1689
|
verbose: false,
|
|
1331
1690
|
format: "console",
|
|
@@ -1334,29 +1693,50 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1334
1693
|
}, "Fill time", Date.now() - startTime);
|
|
1335
1694
|
}
|
|
1336
1695
|
const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
|
|
1696
|
+
const isResearchExample = example.type === "research";
|
|
1337
1697
|
if (agentFieldIssues.length > 0) {
|
|
1338
1698
|
console.log("");
|
|
1699
|
+
const workflowLabel = isResearchExample ? "research" : "agent fill";
|
|
1339
1700
|
p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
|
|
1701
|
+
const confirmMessage = isResearchExample ? "Run research now? (requires web search)" : "Run agent fill now?";
|
|
1340
1702
|
const runAgent = await p.confirm({
|
|
1341
|
-
message:
|
|
1703
|
+
message: confirmMessage,
|
|
1342
1704
|
initialValue: true
|
|
1343
1705
|
});
|
|
1344
1706
|
if (p.isCancel(runAgent) || !runAgent) {
|
|
1345
1707
|
console.log("");
|
|
1346
|
-
|
|
1347
|
-
console.log(
|
|
1348
|
-
|
|
1708
|
+
const cliCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=<provider/model>` : ` markform fill ${formatPath(outputPath)} --model=<provider/model>`;
|
|
1709
|
+
console.log(`You can run ${workflowLabel} later with:`);
|
|
1710
|
+
console.log(cliCommand);
|
|
1711
|
+
if (userFillOutputs) await showFileViewerChooser([
|
|
1712
|
+
{
|
|
1713
|
+
path: userFillOutputs.formPath,
|
|
1714
|
+
label: "Markform",
|
|
1715
|
+
hint: "form with tags"
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
path: userFillOutputs.rawPath,
|
|
1719
|
+
label: "Plain Markdown",
|
|
1720
|
+
hint: "rendered output"
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
path: userFillOutputs.yamlPath,
|
|
1724
|
+
label: "YAML",
|
|
1725
|
+
hint: "extracted values"
|
|
1726
|
+
}
|
|
1727
|
+
]);
|
|
1728
|
+
p.outro("Happy form filling!");
|
|
1349
1729
|
return;
|
|
1350
1730
|
}
|
|
1351
|
-
const modelId = await promptForModel();
|
|
1731
|
+
const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
|
|
1352
1732
|
if (!modelId) {
|
|
1353
1733
|
p.cancel("Cancelled.");
|
|
1354
1734
|
process.exit(0);
|
|
1355
1735
|
}
|
|
1356
|
-
const agentDefaultFilename =
|
|
1736
|
+
const agentDefaultFilename = basename(generateVersionedPathInFormsDir(outputPath, formsDir));
|
|
1357
1737
|
const agentFilenameResult = await p.text({
|
|
1358
|
-
message:
|
|
1359
|
-
initialValue:
|
|
1738
|
+
message: `Agent output filename (in ${formatPath(formsDir)}):`,
|
|
1739
|
+
initialValue: agentDefaultFilename,
|
|
1360
1740
|
validate: (value) => {
|
|
1361
1741
|
if (!value.trim()) return "Filename is required";
|
|
1362
1742
|
}
|
|
@@ -1365,39 +1745,64 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1365
1745
|
p.cancel("Cancelled.");
|
|
1366
1746
|
process.exit(0);
|
|
1367
1747
|
}
|
|
1368
|
-
const agentOutputPath = join(
|
|
1748
|
+
const agentOutputPath = join(formsDir, agentFilenameResult);
|
|
1369
1749
|
const agentStartTime = Date.now();
|
|
1750
|
+
const timingLabel = isResearchExample ? "Research time" : "Agent fill time";
|
|
1751
|
+
const configOverrides = isResearchExample ? {
|
|
1752
|
+
maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
|
|
1753
|
+
maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
|
|
1754
|
+
} : void 0;
|
|
1370
1755
|
try {
|
|
1371
|
-
const { success
|
|
1756
|
+
const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
|
|
1372
1757
|
logTiming({
|
|
1373
1758
|
verbose: false,
|
|
1374
1759
|
format: "console",
|
|
1375
1760
|
dryRun: false,
|
|
1376
1761
|
quiet: false
|
|
1377
|
-
},
|
|
1378
|
-
const { formPath, rawPath, yamlPath } = exportMultiFormat(form, agentOutputPath);
|
|
1762
|
+
}, timingLabel, Date.now() - agentStartTime);
|
|
1763
|
+
const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, agentOutputPath);
|
|
1379
1764
|
console.log("");
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
console.log(` ${formatPath(
|
|
1383
|
-
console.log(` ${formatPath(
|
|
1765
|
+
const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
|
|
1766
|
+
p.log.success(successMessage);
|
|
1767
|
+
console.log(` ${formatPath(formPath)} (markform)`);
|
|
1768
|
+
console.log(` ${formatPath(rawPath)} (plain markdown)`);
|
|
1769
|
+
console.log(` ${formatPath(yamlPath)} (values as YAML)`);
|
|
1384
1770
|
if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
|
|
1771
|
+
await showFileViewerChooser([
|
|
1772
|
+
{
|
|
1773
|
+
path: formPath,
|
|
1774
|
+
label: "Markform",
|
|
1775
|
+
hint: "form with tags"
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
path: rawPath,
|
|
1779
|
+
label: "Plain Markdown",
|
|
1780
|
+
hint: "rendered output"
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
path: yamlPath,
|
|
1784
|
+
label: "YAML",
|
|
1785
|
+
hint: "extracted values"
|
|
1786
|
+
}
|
|
1787
|
+
]);
|
|
1385
1788
|
} catch (error) {
|
|
1386
1789
|
const message = error instanceof Error ? error.message : String(error);
|
|
1387
|
-
|
|
1790
|
+
const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
|
|
1791
|
+
p.log.error(`${failMessage}: ${message}`);
|
|
1388
1792
|
console.log("");
|
|
1389
|
-
console.log(
|
|
1390
|
-
|
|
1793
|
+
console.log("You can try again with:");
|
|
1794
|
+
const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
|
|
1795
|
+
console.log(retryCommand);
|
|
1391
1796
|
}
|
|
1392
1797
|
}
|
|
1393
|
-
p.outro(
|
|
1798
|
+
p.outro("Happy form filling!");
|
|
1394
1799
|
}
|
|
1395
1800
|
/**
|
|
1396
1801
|
* Register the examples command.
|
|
1397
1802
|
*/
|
|
1398
1803
|
function registerExamplesCommand(program) {
|
|
1399
|
-
program.command("examples").description("
|
|
1400
|
-
getCommandContext(cmd);
|
|
1804
|
+
program.command("examples").description("Try out some example forms interactively using the console").option("--list", "List available examples without interactive selection").option("--name <example>", "Select example by ID (still prompts for filename)").action(async (options, cmd) => {
|
|
1805
|
+
const ctx = getCommandContext(cmd);
|
|
1401
1806
|
try {
|
|
1402
1807
|
if (options.list) {
|
|
1403
1808
|
printExamplesList();
|
|
@@ -1411,7 +1816,7 @@ function registerExamplesCommand(program) {
|
|
|
1411
1816
|
process.exit(1);
|
|
1412
1817
|
}
|
|
1413
1818
|
}
|
|
1414
|
-
await runInteractiveFlow(options.name);
|
|
1819
|
+
await runInteractiveFlow(options.name, ctx.formsDir);
|
|
1415
1820
|
} catch (error) {
|
|
1416
1821
|
logError(error instanceof Error ? error.message : String(error));
|
|
1417
1822
|
process.exit(1);
|
|
@@ -1434,7 +1839,7 @@ function registerExportCommand(program) {
|
|
|
1434
1839
|
else if (ctx.format === "markform") format = "markform";
|
|
1435
1840
|
try {
|
|
1436
1841
|
logVerbose(ctx, `Reading file: ${file}`);
|
|
1437
|
-
const content = await readFile(file);
|
|
1842
|
+
const content = await readFile$1(file);
|
|
1438
1843
|
logVerbose(ctx, "Parsing form...");
|
|
1439
1844
|
const form = parseForm(content);
|
|
1440
1845
|
if (format === "markform") {
|
|
@@ -1445,26 +1850,30 @@ function registerExportCommand(program) {
|
|
|
1445
1850
|
console.log(serializeRawMarkdown(form));
|
|
1446
1851
|
return;
|
|
1447
1852
|
}
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
})) } : {}
|
|
1464
|
-
}))
|
|
1853
|
+
const schema = {
|
|
1854
|
+
id: form.schema.id,
|
|
1855
|
+
title: form.schema.title,
|
|
1856
|
+
groups: form.schema.groups.map((group) => ({
|
|
1857
|
+
id: group.id,
|
|
1858
|
+
title: group.title,
|
|
1859
|
+
children: group.children.map((field) => ({
|
|
1860
|
+
id: field.id,
|
|
1861
|
+
kind: field.kind,
|
|
1862
|
+
label: field.label,
|
|
1863
|
+
required: field.required,
|
|
1864
|
+
...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
|
|
1865
|
+
id: opt.id,
|
|
1866
|
+
label: opt.label
|
|
1867
|
+
})) } : {}
|
|
1465
1868
|
}))
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1869
|
+
}))
|
|
1870
|
+
};
|
|
1871
|
+
const values = {};
|
|
1872
|
+
for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
|
|
1873
|
+
const output = {
|
|
1874
|
+
schema,
|
|
1875
|
+
values,
|
|
1876
|
+
notes: form.notes,
|
|
1468
1877
|
markdown: serialize(form)
|
|
1469
1878
|
};
|
|
1470
1879
|
if (format === "json") if (options.compact) console.log(JSON.stringify(output));
|
|
@@ -1498,7 +1907,7 @@ function formatConsoleSession(transcript, useColors) {
|
|
|
1498
1907
|
lines.push(bold("Harness Config:"));
|
|
1499
1908
|
lines.push(` Max turns: ${transcript.harness.maxTurns}`);
|
|
1500
1909
|
lines.push(` Max patches/turn: ${transcript.harness.maxPatchesPerTurn}`);
|
|
1501
|
-
lines.push(` Max issues: ${transcript.harness.
|
|
1910
|
+
lines.push(` Max issues/turn: ${transcript.harness.maxIssuesPerTurn}`);
|
|
1502
1911
|
lines.push("");
|
|
1503
1912
|
lines.push(bold(`Turns (${transcript.turns.length}):`));
|
|
1504
1913
|
for (const turn of transcript.turns) {
|
|
@@ -1517,7 +1926,7 @@ function formatConsoleSession(transcript, useColors) {
|
|
|
1517
1926
|
* Register the fill command.
|
|
1518
1927
|
*/
|
|
1519
1928
|
function registerFillCommand(program) {
|
|
1520
|
-
program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-
|
|
1929
|
+
program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-5-mini)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES_PER_TURN})`, String(DEFAULT_MAX_ISSUES_PER_TURN)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").action(async (file, options, cmd) => {
|
|
1521
1930
|
const ctx = getCommandContext(cmd);
|
|
1522
1931
|
const filePath = resolve(file);
|
|
1523
1932
|
try {
|
|
@@ -1539,7 +1948,7 @@ function registerFillCommand(program) {
|
|
|
1539
1948
|
fillMode = options.mode;
|
|
1540
1949
|
}
|
|
1541
1950
|
logVerbose(ctx, `Reading form: ${filePath}`);
|
|
1542
|
-
const formContent = await readFile(filePath);
|
|
1951
|
+
const formContent = await readFile$1(filePath);
|
|
1543
1952
|
logVerbose(ctx, "Parsing form...");
|
|
1544
1953
|
const form = parseForm(formContent);
|
|
1545
1954
|
if (options.interactive) {
|
|
@@ -1567,12 +1976,18 @@ function registerFillCommand(program) {
|
|
|
1567
1976
|
}
|
|
1568
1977
|
if (patches.length > 0) applyPatches(form, patches);
|
|
1569
1978
|
const durationMs$1 = Date.now() - startTime;
|
|
1570
|
-
|
|
1979
|
+
let outputPath$1;
|
|
1980
|
+
if (options.output) outputPath$1 = resolve(options.output);
|
|
1981
|
+
else {
|
|
1982
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
1983
|
+
await ensureFormsDir(formsDir);
|
|
1984
|
+
outputPath$1 = generateVersionedPathInFormsDir(filePath, formsDir);
|
|
1985
|
+
}
|
|
1571
1986
|
if (ctx.dryRun) {
|
|
1572
1987
|
logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
|
|
1573
1988
|
showInteractiveOutro(patches.length, false);
|
|
1574
1989
|
} else {
|
|
1575
|
-
const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath$1);
|
|
1990
|
+
const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, outputPath$1);
|
|
1576
1991
|
showInteractiveOutro(patches.length, false);
|
|
1577
1992
|
console.log("");
|
|
1578
1993
|
p.log.success("Outputs:");
|
|
@@ -1583,8 +1998,8 @@ function registerFillCommand(program) {
|
|
|
1583
1998
|
logTiming(ctx, "Fill time", durationMs$1);
|
|
1584
1999
|
if (patches.length > 0) {
|
|
1585
2000
|
console.log("");
|
|
1586
|
-
console.log(
|
|
1587
|
-
console.log(
|
|
2001
|
+
console.log("Next step: fill remaining fields with agent");
|
|
2002
|
+
console.log(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`);
|
|
1588
2003
|
}
|
|
1589
2004
|
process.exit(0);
|
|
1590
2005
|
}
|
|
@@ -1599,22 +2014,22 @@ function registerFillCommand(program) {
|
|
|
1599
2014
|
process.exit(1);
|
|
1600
2015
|
}
|
|
1601
2016
|
if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
|
|
1602
|
-
const harnessConfig = {
|
|
1603
|
-
maxTurns:
|
|
1604
|
-
maxPatchesPerTurn:
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2017
|
+
const harnessConfig = resolveHarnessConfig(form, {
|
|
2018
|
+
maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
|
|
2019
|
+
maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
|
|
2020
|
+
maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
|
|
2021
|
+
maxFieldsPerTurn: options.maxFields ? parseInt(options.maxFields, 10) : void 0,
|
|
2022
|
+
maxGroupsPerTurn: options.maxGroups ? parseInt(options.maxGroups, 10) : void 0,
|
|
1608
2023
|
targetRoles,
|
|
1609
2024
|
fillMode
|
|
1610
|
-
};
|
|
2025
|
+
});
|
|
1611
2026
|
const harness = createHarness(form, harnessConfig);
|
|
1612
2027
|
let agent;
|
|
1613
2028
|
let mockPath;
|
|
1614
2029
|
if (options.mock) {
|
|
1615
2030
|
mockPath = resolve(options.mockSource);
|
|
1616
2031
|
logVerbose(ctx, `Reading mock source: ${mockPath}`);
|
|
1617
|
-
agent = createMockAgent(parseForm(await readFile(mockPath)));
|
|
2032
|
+
agent = createMockAgent(parseForm(await readFile$1(mockPath)));
|
|
1618
2033
|
} else {
|
|
1619
2034
|
const modelId = options.model;
|
|
1620
2035
|
logVerbose(ctx, `Resolving model: ${modelId}`);
|
|
@@ -1626,7 +2041,7 @@ function registerFillCommand(program) {
|
|
|
1626
2041
|
} else if (options.prompt) {
|
|
1627
2042
|
const promptPath = resolve(options.prompt);
|
|
1628
2043
|
logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
|
|
1629
|
-
systemPrompt = await readFile(promptPath);
|
|
2044
|
+
systemPrompt = await readFile$1(promptPath);
|
|
1630
2045
|
}
|
|
1631
2046
|
const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
|
|
1632
2047
|
const liveAgent = createLiveAgent({
|
|
@@ -1643,21 +2058,24 @@ function registerFillCommand(program) {
|
|
|
1643
2058
|
logInfo(ctx, `Agent: ${options.mock ? "mock" : "live"}${options.model ? ` (${options.model})` : ""}`);
|
|
1644
2059
|
logVerbose(ctx, `Max turns: ${harnessConfig.maxTurns}`);
|
|
1645
2060
|
logVerbose(ctx, `Max patches per turn: ${harnessConfig.maxPatchesPerTurn}`);
|
|
1646
|
-
logVerbose(ctx, `Max issues per
|
|
2061
|
+
logVerbose(ctx, `Max issues per turn: ${harnessConfig.maxIssuesPerTurn}`);
|
|
1647
2062
|
logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
|
|
1648
2063
|
logVerbose(ctx, `Fill mode: ${fillMode}`);
|
|
1649
2064
|
let stepResult = harness.step();
|
|
1650
|
-
logInfo(ctx, `Turn ${
|
|
2065
|
+
logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1651
2066
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1652
2067
|
const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1653
|
-
|
|
2068
|
+
const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
|
|
2069
|
+
logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
|
|
1654
2070
|
for (const patch of patches) {
|
|
1655
2071
|
const typeName = formatPatchType(patch);
|
|
1656
2072
|
const value = formatPatchValue(patch);
|
|
1657
|
-
|
|
2073
|
+
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
2074
|
+
if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
2075
|
+
else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
1658
2076
|
}
|
|
1659
2077
|
if (stats) {
|
|
1660
|
-
logVerbose(ctx, ` Stats:
|
|
2078
|
+
logVerbose(ctx, ` Stats: tokens ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0}`);
|
|
1661
2079
|
if (stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
|
|
1662
2080
|
if (stats.prompts) {
|
|
1663
2081
|
logVerbose(ctx, ``);
|
|
@@ -1679,14 +2097,20 @@ function registerFillCommand(program) {
|
|
|
1679
2097
|
if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
1680
2098
|
else if (!harness.hasReachedMaxTurns()) {
|
|
1681
2099
|
stepResult = harness.step();
|
|
1682
|
-
logInfo(ctx, `Turn ${
|
|
2100
|
+
logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1683
2101
|
}
|
|
1684
2102
|
}
|
|
1685
2103
|
const durationMs = Date.now() - startTime;
|
|
1686
2104
|
if (stepResult.isComplete) logSuccess(ctx, `Form completed in ${harness.getTurnNumber()} turn(s)`);
|
|
1687
2105
|
else if (harness.hasReachedMaxTurns()) logWarn(ctx, `Max turns reached (${harnessConfig.maxTurns})`);
|
|
1688
2106
|
logTiming(ctx, "Fill time", durationMs);
|
|
1689
|
-
|
|
2107
|
+
let outputPath;
|
|
2108
|
+
if (options.output) outputPath = resolve(options.output);
|
|
2109
|
+
else {
|
|
2110
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2111
|
+
await ensureFormsDir(formsDir);
|
|
2112
|
+
outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
|
|
2113
|
+
}
|
|
1690
2114
|
const formMarkdown = serialize(harness.getForm());
|
|
1691
2115
|
if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
|
|
1692
2116
|
else {
|
|
@@ -1750,6 +2174,18 @@ function formatState$1(state, useColors) {
|
|
|
1750
2174
|
return useColors ? colorFn(text) : text;
|
|
1751
2175
|
}
|
|
1752
2176
|
/**
|
|
2177
|
+
* Format answer state badge for console output.
|
|
2178
|
+
*/
|
|
2179
|
+
function formatAnswerState(state, useColors) {
|
|
2180
|
+
const [text, colorFn] = {
|
|
2181
|
+
answered: ["answered", pc.green],
|
|
2182
|
+
skipped: ["skipped", pc.yellow],
|
|
2183
|
+
aborted: ["aborted", pc.red],
|
|
2184
|
+
unanswered: ["unanswered", pc.dim]
|
|
2185
|
+
}[state] ?? [state, (s) => s];
|
|
2186
|
+
return useColors ? colorFn(text) : text;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
1753
2189
|
* Format priority badge for console output.
|
|
1754
2190
|
*
|
|
1755
2191
|
* Priority tiers and colors:
|
|
@@ -1796,6 +2232,8 @@ function formatFieldValue(value, useColors) {
|
|
|
1796
2232
|
if (entries.length === 0) return dim("(no entries)");
|
|
1797
2233
|
return entries.map(([k, v]) => `${k}:${v}`).join(", ");
|
|
1798
2234
|
}
|
|
2235
|
+
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
2236
|
+
case "url_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
|
|
1799
2237
|
default: return dim("(unknown)");
|
|
1800
2238
|
}
|
|
1801
2239
|
}
|
|
@@ -1823,13 +2261,11 @@ function formatConsoleReport$1(report, useColors) {
|
|
|
1823
2261
|
lines.push(bold("Progress:"));
|
|
1824
2262
|
lines.push(` Total fields: ${progress.counts.totalFields}`);
|
|
1825
2263
|
lines.push(` Required: ${progress.counts.requiredFields}`);
|
|
1826
|
-
lines.push(`
|
|
1827
|
-
lines.push(`
|
|
1828
|
-
lines.push(`
|
|
1829
|
-
lines.push(`
|
|
1830
|
-
lines.push(`
|
|
1831
|
-
lines.push(` Empty (required): ${progress.counts.emptyRequiredFields}`);
|
|
1832
|
-
lines.push(` Empty (optional): ${progress.counts.emptyOptionalFields}`);
|
|
2264
|
+
lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
|
|
2265
|
+
lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
|
|
2266
|
+
lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
|
|
2267
|
+
lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
|
|
2268
|
+
lines.push(` Total notes: ${progress.counts.totalNotes}`);
|
|
1833
2269
|
lines.push("");
|
|
1834
2270
|
lines.push(bold("Form Content:"));
|
|
1835
2271
|
for (const group of report.groups) {
|
|
@@ -1837,13 +2273,25 @@ function formatConsoleReport$1(report, useColors) {
|
|
|
1837
2273
|
for (const field of group.children) {
|
|
1838
2274
|
const reqBadge = field.required ? yellow("[required]") : dim("[optional]");
|
|
1839
2275
|
const roleBadge = field.role !== "agent" ? cyan(`[${field.role}]`) : "";
|
|
2276
|
+
const fieldProgress = progress.fields[field.id];
|
|
2277
|
+
const responseStateBadge = fieldProgress ? `[${formatAnswerState(fieldProgress.answerState, useColors)}]` : "";
|
|
2278
|
+
const notesBadge = fieldProgress?.hasNotes ? cyan(`[${fieldProgress.noteCount} note${fieldProgress.noteCount > 1 ? "s" : ""}]`) : "";
|
|
1840
2279
|
const value = report.values[field.id];
|
|
1841
2280
|
const valueStr = formatFieldValue(value, useColors);
|
|
1842
|
-
lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge}`.trim());
|
|
2281
|
+
lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge} ${responseStateBadge} ${notesBadge}`.trim());
|
|
1843
2282
|
lines.push(` ${dim("→")} ${valueStr}`);
|
|
1844
2283
|
}
|
|
1845
2284
|
}
|
|
1846
2285
|
lines.push("");
|
|
2286
|
+
if (report.notes.length > 0) {
|
|
2287
|
+
lines.push(bold(`Notes (${report.notes.length}):`));
|
|
2288
|
+
for (const note of report.notes) {
|
|
2289
|
+
const roleBadge = cyan(`[${note.role}]`);
|
|
2290
|
+
const refLabel = dim(`${note.ref}:`);
|
|
2291
|
+
lines.push(` ${note.id} ${roleBadge} ${refLabel} ${note.text}`.trim());
|
|
2292
|
+
}
|
|
2293
|
+
lines.push("");
|
|
2294
|
+
}
|
|
1847
2295
|
if (report.issues.length > 0) {
|
|
1848
2296
|
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
1849
2297
|
for (const issue of report.issues) {
|
|
@@ -1871,11 +2319,13 @@ function registerInspectCommand(program) {
|
|
|
1871
2319
|
process.exit(1);
|
|
1872
2320
|
}
|
|
1873
2321
|
logVerbose(ctx, `Reading file: ${file}`);
|
|
1874
|
-
const content = await readFile(file);
|
|
2322
|
+
const content = await readFile$1(file);
|
|
1875
2323
|
logVerbose(ctx, "Parsing form...");
|
|
1876
2324
|
const form = parseForm(content);
|
|
1877
2325
|
logVerbose(ctx, "Running inspection...");
|
|
1878
2326
|
const result = inspect(form, { targetRoles });
|
|
2327
|
+
const values = {};
|
|
2328
|
+
for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
|
|
1879
2329
|
const output = formatOutput(ctx, {
|
|
1880
2330
|
title: form.schema.title,
|
|
1881
2331
|
structure: result.structureSummary,
|
|
@@ -1892,7 +2342,8 @@ function registerInspectCommand(program) {
|
|
|
1892
2342
|
role: field.role
|
|
1893
2343
|
}))
|
|
1894
2344
|
})),
|
|
1895
|
-
values
|
|
2345
|
+
values,
|
|
2346
|
+
notes: form.notes,
|
|
1896
2347
|
issues: result.issues.map((issue) => ({
|
|
1897
2348
|
ref: issue.ref,
|
|
1898
2349
|
scope: issue.scope,
|
|
@@ -1912,7 +2363,7 @@ function registerInspectCommand(program) {
|
|
|
1912
2363
|
}
|
|
1913
2364
|
|
|
1914
2365
|
//#endregion
|
|
1915
|
-
//#region src/cli/commands/
|
|
2366
|
+
//#region src/cli/commands/readme.ts
|
|
1916
2367
|
/**
|
|
1917
2368
|
* Get the path to the README.md file.
|
|
1918
2369
|
* Works both during development and when installed as a package.
|
|
@@ -1938,6 +2389,128 @@ function loadReadme() {
|
|
|
1938
2389
|
* Apply basic terminal formatting to markdown content.
|
|
1939
2390
|
* Colorizes headers, code blocks, and other elements for better readability.
|
|
1940
2391
|
*/
|
|
2392
|
+
function formatMarkdown$1(content, useColors) {
|
|
2393
|
+
if (!useColors) return content;
|
|
2394
|
+
const lines = content.split("\n");
|
|
2395
|
+
const formatted = [];
|
|
2396
|
+
let inCodeBlock = false;
|
|
2397
|
+
for (const line of lines) {
|
|
2398
|
+
if (line.startsWith("```")) {
|
|
2399
|
+
inCodeBlock = !inCodeBlock;
|
|
2400
|
+
formatted.push(pc.dim(line));
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
2403
|
+
if (inCodeBlock) {
|
|
2404
|
+
formatted.push(pc.dim(line));
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
if (line.startsWith("# ")) {
|
|
2408
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
if (line.startsWith("## ")) {
|
|
2412
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
if (line.startsWith("### ")) {
|
|
2416
|
+
formatted.push(pc.bold(line));
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
2420
|
+
return pc.yellow(code);
|
|
2421
|
+
});
|
|
2422
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
2423
|
+
return pc.bold(text);
|
|
2424
|
+
});
|
|
2425
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
2426
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
2427
|
+
});
|
|
2428
|
+
formatted.push(formattedLine);
|
|
2429
|
+
}
|
|
2430
|
+
return formatted.join("\n");
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Check if stdout is an interactive terminal.
|
|
2434
|
+
*/
|
|
2435
|
+
function isInteractive$1() {
|
|
2436
|
+
return process.stdout.isTTY === true;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Display content. In a future enhancement, could pipe to a pager for long output.
|
|
2440
|
+
*/
|
|
2441
|
+
function displayContent$1(content) {
|
|
2442
|
+
console.log(content);
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Register the readme command.
|
|
2446
|
+
*/
|
|
2447
|
+
function registerReadmeCommand(program) {
|
|
2448
|
+
program.command("readme").description("✨Display README documentation ← START HERE!").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
2449
|
+
const ctx = getCommandContext(cmd);
|
|
2450
|
+
try {
|
|
2451
|
+
displayContent$1(formatMarkdown$1(loadReadme(), !options.raw && isInteractive$1() && !ctx.quiet));
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2454
|
+
process.exit(1);
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
//#endregion
|
|
2460
|
+
//#region src/cli/commands/report.ts
|
|
2461
|
+
/**
|
|
2462
|
+
* Register the report command.
|
|
2463
|
+
*/
|
|
2464
|
+
function registerReportCommand(program) {
|
|
2465
|
+
program.command("report <file>").description("Generate filtered markdown report (excludes instructions, report=false elements)").option("-o, --output <file>", "Output file path (default: stdout)").action(async (file, options, cmd) => {
|
|
2466
|
+
const ctx = getCommandContext(cmd);
|
|
2467
|
+
try {
|
|
2468
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
2469
|
+
const content = await readFile$1(file);
|
|
2470
|
+
logVerbose(ctx, "Parsing form...");
|
|
2471
|
+
const form = parseForm(content);
|
|
2472
|
+
logVerbose(ctx, "Generating report...");
|
|
2473
|
+
const reportContent = serializeReportMarkdown(form);
|
|
2474
|
+
if (options.output) {
|
|
2475
|
+
let outputPath = options.output;
|
|
2476
|
+
if (!outputPath.endsWith(REPORT_EXTENSION) && !outputPath.endsWith(".md")) outputPath = outputPath + REPORT_EXTENSION;
|
|
2477
|
+
await writeFile(outputPath, reportContent);
|
|
2478
|
+
logVerbose(ctx, `Report written to: ${outputPath}`);
|
|
2479
|
+
} else console.log(reportContent);
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
//#endregion
|
|
2488
|
+
//#region src/cli/commands/spec.ts
|
|
2489
|
+
/**
|
|
2490
|
+
* Get the path to the SPEC.md file.
|
|
2491
|
+
* Works both during development and when installed as a package.
|
|
2492
|
+
*/
|
|
2493
|
+
function getSpecPath() {
|
|
2494
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
2495
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "SPEC.md");
|
|
2496
|
+
return join(dirname(dirname(dirname(thisDir))), "SPEC.md");
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Load the spec content.
|
|
2500
|
+
*/
|
|
2501
|
+
function loadSpec() {
|
|
2502
|
+
const specPath = getSpecPath();
|
|
2503
|
+
try {
|
|
2504
|
+
return readFileSync(specPath, "utf-8");
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2507
|
+
throw new Error(`Failed to load SPEC from ${specPath}: ${message}`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Apply basic terminal formatting to markdown content.
|
|
2512
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
2513
|
+
*/
|
|
1941
2514
|
function formatMarkdown(content, useColors) {
|
|
1942
2515
|
if (!useColors) return content;
|
|
1943
2516
|
const lines = content.split("\n");
|
|
@@ -1991,13 +2564,13 @@ function displayContent(content) {
|
|
|
1991
2564
|
console.log(content);
|
|
1992
2565
|
}
|
|
1993
2566
|
/**
|
|
1994
|
-
* Register the
|
|
2567
|
+
* Register the spec command.
|
|
1995
2568
|
*/
|
|
1996
|
-
function
|
|
1997
|
-
program.command("
|
|
2569
|
+
function registerSpecCommand(program) {
|
|
2570
|
+
program.command("spec").description("Display the Markform specification").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
1998
2571
|
const ctx = getCommandContext(cmd);
|
|
1999
2572
|
try {
|
|
2000
|
-
displayContent(formatMarkdown(
|
|
2573
|
+
displayContent(formatMarkdown(loadSpec(), !options.raw && isInteractive() && !ctx.quiet));
|
|
2001
2574
|
} catch (error) {
|
|
2002
2575
|
logError(error instanceof Error ? error.message : String(error));
|
|
2003
2576
|
process.exit(1);
|
|
@@ -2077,15 +2650,18 @@ function openBrowser(url) {
|
|
|
2077
2650
|
* Register the serve command.
|
|
2078
2651
|
*/
|
|
2079
2652
|
function registerServeCommand(program) {
|
|
2080
|
-
program.command("serve <file>").description("Serve a
|
|
2653
|
+
program.command("serve <file>").description("Serve a file as a web page (forms are interactive, others are read-only)").option("-p, --port <port>", "Port to serve on", String(DEFAULT_PORT)).option("--no-open", "Don't open browser automatically").action(async (file, options, cmd) => {
|
|
2081
2654
|
const ctx = getCommandContext(cmd);
|
|
2082
2655
|
const port = parseInt(options.port ?? String(DEFAULT_PORT), 10);
|
|
2083
2656
|
const filePath = resolve(file);
|
|
2657
|
+
const fileType = detectFileType(filePath);
|
|
2084
2658
|
try {
|
|
2085
2659
|
logVerbose(ctx, `Reading file: ${filePath}`);
|
|
2086
|
-
|
|
2660
|
+
const content = await readFile$1(filePath);
|
|
2661
|
+
let form = null;
|
|
2662
|
+
if (fileType === "form") form = parseForm(content);
|
|
2087
2663
|
const server = createServer((req, res) => {
|
|
2088
|
-
handleRequest(req, res,
|
|
2664
|
+
handleRequest(req, res, filePath, fileType, form, ctx, (updatedForm) => {
|
|
2089
2665
|
form = updatedForm;
|
|
2090
2666
|
}).catch((err) => {
|
|
2091
2667
|
console.error("Request error:", err);
|
|
@@ -2095,7 +2671,8 @@ function registerServeCommand(program) {
|
|
|
2095
2671
|
});
|
|
2096
2672
|
server.listen(port, () => {
|
|
2097
2673
|
const url = `http://localhost:${port}`;
|
|
2098
|
-
|
|
2674
|
+
const typeLabel = fileType === "form" ? "Form" : fileType === "unknown" ? "File" : fileType.toUpperCase();
|
|
2675
|
+
logInfo(ctx, pc.green(`\n✓ ${typeLabel} server running at ${pc.bold(url)}\n`));
|
|
2099
2676
|
logInfo(ctx, pc.dim("Press Ctrl+C to stop\n"));
|
|
2100
2677
|
if (options.open !== false) openBrowser(url);
|
|
2101
2678
|
});
|
|
@@ -2112,15 +2689,33 @@ function registerServeCommand(program) {
|
|
|
2112
2689
|
}
|
|
2113
2690
|
/**
|
|
2114
2691
|
* Handle HTTP requests.
|
|
2692
|
+
* Dispatches to appropriate renderer based on file type.
|
|
2115
2693
|
*/
|
|
2116
|
-
async function handleRequest(req, res,
|
|
2694
|
+
async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm) {
|
|
2117
2695
|
const url = req.url ?? "/";
|
|
2118
|
-
if (req.method === "GET" && url === "/") {
|
|
2696
|
+
if (req.method === "GET" && url === "/") if (fileType === "form" && form) {
|
|
2119
2697
|
const html = renderFormHtml(form);
|
|
2120
2698
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2121
2699
|
res.end(html);
|
|
2122
|
-
} else if (
|
|
2123
|
-
|
|
2700
|
+
} else if (fileType === "raw" || fileType === "report") {
|
|
2701
|
+
const html = renderMarkdownHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2702
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2703
|
+
res.end(html);
|
|
2704
|
+
} else if (fileType === "yaml") {
|
|
2705
|
+
const html = renderYamlHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2706
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2707
|
+
res.end(html);
|
|
2708
|
+
} else if (fileType === "json") {
|
|
2709
|
+
const html = renderJsonHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2710
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2711
|
+
res.end(html);
|
|
2712
|
+
} else {
|
|
2713
|
+
const html = renderPlainTextHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2714
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2715
|
+
res.end(html);
|
|
2716
|
+
}
|
|
2717
|
+
else if (req.method === "POST" && url === "/save" && fileType === "form" && form) await handleSave(req, res, form, filePath, ctx, updateForm);
|
|
2718
|
+
else if (url === "/api/form" && form) {
|
|
2124
2719
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2125
2720
|
res.end(JSON.stringify({ schema: form.schema }));
|
|
2126
2721
|
} else {
|
|
@@ -2153,7 +2748,8 @@ function formDataToPatches(formData, form) {
|
|
|
2153
2748
|
if (formData[`__skip__${fieldId}`] === "1" && !field.required) {
|
|
2154
2749
|
patches.push({
|
|
2155
2750
|
op: "skip_field",
|
|
2156
|
-
fieldId
|
|
2751
|
+
fieldId,
|
|
2752
|
+
role: "user"
|
|
2157
2753
|
});
|
|
2158
2754
|
continue;
|
|
2159
2755
|
}
|
|
@@ -2334,9 +2930,9 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
|
|
|
2334
2930
|
* @public Exported for testing.
|
|
2335
2931
|
*/
|
|
2336
2932
|
function renderFormHtml(form) {
|
|
2337
|
-
const { schema,
|
|
2933
|
+
const { schema, responsesByFieldId } = form;
|
|
2338
2934
|
const formTitle = schema.title ?? schema.id;
|
|
2339
|
-
const groupsHtml = schema.groups.map((group) => renderGroup(group,
|
|
2935
|
+
const groupsHtml = schema.groups.map((group) => renderGroup(group, responsesByFieldId)).join("\n");
|
|
2340
2936
|
return `<!DOCTYPE html>
|
|
2341
2937
|
<html lang="en">
|
|
2342
2938
|
<head>
|
|
@@ -2583,9 +3179,12 @@ function renderFormHtml(form) {
|
|
|
2583
3179
|
/**
|
|
2584
3180
|
* Render a field group as HTML.
|
|
2585
3181
|
*/
|
|
2586
|
-
function renderGroup(group,
|
|
3182
|
+
function renderGroup(group, responses) {
|
|
2587
3183
|
const groupTitle = group.title ?? group.id;
|
|
2588
|
-
const fieldsHtml = group.children.map((field) =>
|
|
3184
|
+
const fieldsHtml = group.children.map((field) => {
|
|
3185
|
+
const response = responses[field.id];
|
|
3186
|
+
return renderFieldHtml(field, response?.state === "answered" ? response.value : void 0, response?.state === "skipped");
|
|
3187
|
+
}).join("\n");
|
|
2589
3188
|
return `
|
|
2590
3189
|
<div class="group">
|
|
2591
3190
|
<h2>${escapeHtml(groupTitle)}</h2>
|
|
@@ -2596,13 +3195,13 @@ function renderGroup(group, values, skips) {
|
|
|
2596
3195
|
* Render a field as HTML.
|
|
2597
3196
|
* @public Exported for testing.
|
|
2598
3197
|
*/
|
|
2599
|
-
function renderFieldHtml(field, value,
|
|
2600
|
-
const
|
|
3198
|
+
function renderFieldHtml(field, value, isSkipped) {
|
|
3199
|
+
const skipped = isSkipped === true;
|
|
2601
3200
|
const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
|
|
2602
3201
|
const typeLabel = `<span class="type-badge">${field.kind}</span>`;
|
|
2603
|
-
const skippedBadge =
|
|
2604
|
-
const fieldClass =
|
|
2605
|
-
const disabledAttr =
|
|
3202
|
+
const skippedBadge = skipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
|
|
3203
|
+
const fieldClass = skipped ? "field field-skipped" : "field";
|
|
3204
|
+
const disabledAttr = skipped ? " disabled" : "";
|
|
2606
3205
|
let inputHtml;
|
|
2607
3206
|
switch (field.kind) {
|
|
2608
3207
|
case "string":
|
|
@@ -2631,7 +3230,7 @@ function renderFieldHtml(field, value, skipInfo) {
|
|
|
2631
3230
|
break;
|
|
2632
3231
|
default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
|
|
2633
3232
|
}
|
|
2634
|
-
const skipButton = !field.required && !
|
|
3233
|
+
const skipButton = !field.required && !skipped ? `<div class="field-actions">
|
|
2635
3234
|
<button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
|
|
2636
3235
|
</div>` : "";
|
|
2637
3236
|
return `
|
|
@@ -2775,6 +3374,182 @@ function renderCheckboxesInput(field, value, disabledAttr) {
|
|
|
2775
3374
|
function escapeHtml(str) {
|
|
2776
3375
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2777
3376
|
}
|
|
3377
|
+
/** Common styles for read-only viewers */
|
|
3378
|
+
const READ_ONLY_STYLES = `
|
|
3379
|
+
* { box-sizing: border-box; }
|
|
3380
|
+
body {
|
|
3381
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
3382
|
+
line-height: 1.6;
|
|
3383
|
+
max-width: 900px;
|
|
3384
|
+
margin: 0 auto;
|
|
3385
|
+
padding: 2rem;
|
|
3386
|
+
background: #f8f9fa;
|
|
3387
|
+
color: #212529;
|
|
3388
|
+
}
|
|
3389
|
+
h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; font-size: 1.5rem; }
|
|
3390
|
+
.content {
|
|
3391
|
+
background: white;
|
|
3392
|
+
border-radius: 8px;
|
|
3393
|
+
padding: 1.5rem;
|
|
3394
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
3395
|
+
}
|
|
3396
|
+
pre {
|
|
3397
|
+
background: #1e1e1e;
|
|
3398
|
+
color: #d4d4d4;
|
|
3399
|
+
padding: 1rem;
|
|
3400
|
+
border-radius: 6px;
|
|
3401
|
+
overflow-x: auto;
|
|
3402
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
3403
|
+
font-size: 0.9rem;
|
|
3404
|
+
line-height: 1.5;
|
|
3405
|
+
}
|
|
3406
|
+
.badge {
|
|
3407
|
+
font-size: 0.75rem;
|
|
3408
|
+
padding: 0.2rem 0.5rem;
|
|
3409
|
+
background: #e9ecef;
|
|
3410
|
+
border-radius: 4px;
|
|
3411
|
+
color: #6c757d;
|
|
3412
|
+
margin-left: 0.75rem;
|
|
3413
|
+
font-weight: normal;
|
|
3414
|
+
}
|
|
3415
|
+
`;
|
|
3416
|
+
/**
|
|
3417
|
+
* Render markdown content as read-only HTML.
|
|
3418
|
+
* Simple rendering without full markdown parsing.
|
|
3419
|
+
*/
|
|
3420
|
+
function renderMarkdownHtml(content, filename) {
|
|
3421
|
+
const lines = content.split("\n");
|
|
3422
|
+
let html = "";
|
|
3423
|
+
let inParagraph = false;
|
|
3424
|
+
for (const line of lines) {
|
|
3425
|
+
const trimmed = line.trim();
|
|
3426
|
+
if (trimmed.startsWith("# ")) {
|
|
3427
|
+
if (inParagraph) {
|
|
3428
|
+
html += "</p>";
|
|
3429
|
+
inParagraph = false;
|
|
3430
|
+
}
|
|
3431
|
+
html += `<h2>${escapeHtml(trimmed.slice(2))}</h2>`;
|
|
3432
|
+
} else if (trimmed.startsWith("## ")) {
|
|
3433
|
+
if (inParagraph) {
|
|
3434
|
+
html += "</p>";
|
|
3435
|
+
inParagraph = false;
|
|
3436
|
+
}
|
|
3437
|
+
html += `<h3>${escapeHtml(trimmed.slice(3))}</h3>`;
|
|
3438
|
+
} else if (trimmed.startsWith("### ")) {
|
|
3439
|
+
if (inParagraph) {
|
|
3440
|
+
html += "</p>";
|
|
3441
|
+
inParagraph = false;
|
|
3442
|
+
}
|
|
3443
|
+
html += `<h4>${escapeHtml(trimmed.slice(4))}</h4>`;
|
|
3444
|
+
} else if (trimmed === "") {
|
|
3445
|
+
if (inParagraph) {
|
|
3446
|
+
html += "</p>";
|
|
3447
|
+
inParagraph = false;
|
|
3448
|
+
}
|
|
3449
|
+
} else {
|
|
3450
|
+
if (!inParagraph) {
|
|
3451
|
+
html += "<p>";
|
|
3452
|
+
inParagraph = true;
|
|
3453
|
+
} else html += "<br>";
|
|
3454
|
+
html += escapeHtml(trimmed);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
if (inParagraph) html += "</p>";
|
|
3458
|
+
return `<!DOCTYPE html>
|
|
3459
|
+
<html lang="en">
|
|
3460
|
+
<head>
|
|
3461
|
+
<meta charset="UTF-8">
|
|
3462
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3463
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3464
|
+
<style>${READ_ONLY_STYLES}
|
|
3465
|
+
h2 { color: #495057; font-size: 1.3rem; margin-top: 1.5rem; }
|
|
3466
|
+
h3 { color: #6c757d; font-size: 1.1rem; margin-top: 1.25rem; }
|
|
3467
|
+
h4 { color: #6c757d; font-size: 1rem; margin-top: 1rem; }
|
|
3468
|
+
p { margin: 0.75rem 0; }
|
|
3469
|
+
</style>
|
|
3470
|
+
</head>
|
|
3471
|
+
<body>
|
|
3472
|
+
<h1>${escapeHtml(filename)}<span class="badge">Markdown</span></h1>
|
|
3473
|
+
<div class="content">
|
|
3474
|
+
${html}
|
|
3475
|
+
</div>
|
|
3476
|
+
</body>
|
|
3477
|
+
</html>`;
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Render YAML content with syntax highlighting.
|
|
3481
|
+
*/
|
|
3482
|
+
function renderYamlHtml(content, filename) {
|
|
3483
|
+
const highlighted = content.split("\n").map((line) => {
|
|
3484
|
+
const colonIndex = line.indexOf(":");
|
|
3485
|
+
if (colonIndex > 0 && !line.trim().startsWith("#") && !line.trim().startsWith("-")) return `<span style="color:#9cdcfe">${escapeHtml(line.slice(0, colonIndex))}</span>${escapeHtml(line.slice(colonIndex))}`;
|
|
3486
|
+
if (line.trim().startsWith("#")) return `<span style="color:#6a9955">${escapeHtml(line)}</span>`;
|
|
3487
|
+
return escapeHtml(line);
|
|
3488
|
+
}).join("\n");
|
|
3489
|
+
return `<!DOCTYPE html>
|
|
3490
|
+
<html lang="en">
|
|
3491
|
+
<head>
|
|
3492
|
+
<meta charset="UTF-8">
|
|
3493
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3494
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3495
|
+
<style>${READ_ONLY_STYLES}</style>
|
|
3496
|
+
</head>
|
|
3497
|
+
<body>
|
|
3498
|
+
<h1>${escapeHtml(filename)}<span class="badge">YAML</span></h1>
|
|
3499
|
+
<div class="content">
|
|
3500
|
+
<pre>${highlighted}</pre>
|
|
3501
|
+
</div>
|
|
3502
|
+
</body>
|
|
3503
|
+
</html>`;
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Render JSON content with syntax highlighting and formatting.
|
|
3507
|
+
*/
|
|
3508
|
+
function renderJsonHtml(content, filename) {
|
|
3509
|
+
let formatted;
|
|
3510
|
+
try {
|
|
3511
|
+
const parsed = JSON.parse(content);
|
|
3512
|
+
formatted = JSON.stringify(parsed, null, 2);
|
|
3513
|
+
} catch {
|
|
3514
|
+
formatted = content;
|
|
3515
|
+
}
|
|
3516
|
+
const highlighted = formatted.replace(/"([^"]+)":/g, "<span style=\"color:#9cdcfe\">\"$1\"</span>:").replace(/: "([^"]+)"/g, ": <span style=\"color:#ce9178\">\"$1\"</span>").replace(/: (\d+\.?\d*)/g, ": <span style=\"color:#b5cea8\">$1</span>").replace(/: (true|false|null)/g, ": <span style=\"color:#569cd6\">$1</span>");
|
|
3517
|
+
return `<!DOCTYPE html>
|
|
3518
|
+
<html lang="en">
|
|
3519
|
+
<head>
|
|
3520
|
+
<meta charset="UTF-8">
|
|
3521
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3522
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3523
|
+
<style>${READ_ONLY_STYLES}</style>
|
|
3524
|
+
</head>
|
|
3525
|
+
<body>
|
|
3526
|
+
<h1>${escapeHtml(filename)}<span class="badge">JSON</span></h1>
|
|
3527
|
+
<div class="content">
|
|
3528
|
+
<pre>${highlighted}</pre>
|
|
3529
|
+
</div>
|
|
3530
|
+
</body>
|
|
3531
|
+
</html>`;
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Render plain text content.
|
|
3535
|
+
*/
|
|
3536
|
+
function renderPlainTextHtml(content, filename) {
|
|
3537
|
+
return `<!DOCTYPE html>
|
|
3538
|
+
<html lang="en">
|
|
3539
|
+
<head>
|
|
3540
|
+
<meta charset="UTF-8">
|
|
3541
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3542
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3543
|
+
<style>${READ_ONLY_STYLES}</style>
|
|
3544
|
+
</head>
|
|
3545
|
+
<body>
|
|
3546
|
+
<h1>${escapeHtml(filename)}<span class="badge">Text</span></h1>
|
|
3547
|
+
<div class="content">
|
|
3548
|
+
<pre>${escapeHtml(content)}</pre>
|
|
3549
|
+
</div>
|
|
3550
|
+
</body>
|
|
3551
|
+
</html>`;
|
|
3552
|
+
}
|
|
2778
3553
|
|
|
2779
3554
|
//#endregion
|
|
2780
3555
|
//#region src/cli/commands/render.ts
|
|
@@ -2798,7 +3573,7 @@ function registerRenderCommand(program) {
|
|
|
2798
3573
|
const outputPath = options.output ? resolve(options.output) : getDefaultOutputPath(filePath);
|
|
2799
3574
|
try {
|
|
2800
3575
|
logVerbose(ctx, `Reading file: ${filePath}`);
|
|
2801
|
-
const content = await readFile(filePath);
|
|
3576
|
+
const content = await readFile$1(filePath);
|
|
2802
3577
|
logVerbose(ctx, "Parsing form...");
|
|
2803
3578
|
const form = parseForm(content);
|
|
2804
3579
|
logVerbose(ctx, "Rendering HTML...");
|
|
@@ -2816,6 +3591,156 @@ function registerRenderCommand(program) {
|
|
|
2816
3591
|
});
|
|
2817
3592
|
}
|
|
2818
3593
|
|
|
3594
|
+
//#endregion
|
|
3595
|
+
//#region src/cli/lib/initialValues.ts
|
|
3596
|
+
/**
|
|
3597
|
+
* Parse initial value inputs from CLI flags.
|
|
3598
|
+
*
|
|
3599
|
+
* Supports formats:
|
|
3600
|
+
* - fieldId=value (string values)
|
|
3601
|
+
* - fieldId:number=123 (explicit number)
|
|
3602
|
+
* - fieldId:list=a,b,c (comma-separated list)
|
|
3603
|
+
*
|
|
3604
|
+
* @param inputs Array of input strings in "fieldId=value" format
|
|
3605
|
+
* @returns Array of patches to apply as initial values
|
|
3606
|
+
*/
|
|
3607
|
+
function parseInitialValues(inputs) {
|
|
3608
|
+
const patches = [];
|
|
3609
|
+
for (const input of inputs) {
|
|
3610
|
+
const equalsIndex = input.indexOf("=");
|
|
3611
|
+
if (equalsIndex === -1) throw new Error(`Invalid input format: "${input}" (expected "fieldId=value")`);
|
|
3612
|
+
const fieldSpec = input.slice(0, equalsIndex);
|
|
3613
|
+
const value = input.slice(equalsIndex + 1);
|
|
3614
|
+
const colonIndex = fieldSpec.indexOf(":");
|
|
3615
|
+
let fieldId;
|
|
3616
|
+
let type = null;
|
|
3617
|
+
if (colonIndex !== -1) {
|
|
3618
|
+
fieldId = fieldSpec.slice(0, colonIndex);
|
|
3619
|
+
type = fieldSpec.slice(colonIndex + 1).toLowerCase();
|
|
3620
|
+
} else fieldId = fieldSpec;
|
|
3621
|
+
if (type === "number") {
|
|
3622
|
+
const numValue = parseFloat(value);
|
|
3623
|
+
if (isNaN(numValue)) throw new Error(`Invalid number value for "${fieldId}": "${value}"`);
|
|
3624
|
+
patches.push({
|
|
3625
|
+
op: "set_number",
|
|
3626
|
+
fieldId,
|
|
3627
|
+
value: numValue
|
|
3628
|
+
});
|
|
3629
|
+
} else if (type === "list") {
|
|
3630
|
+
const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3631
|
+
patches.push({
|
|
3632
|
+
op: "set_string_list",
|
|
3633
|
+
fieldId,
|
|
3634
|
+
items
|
|
3635
|
+
});
|
|
3636
|
+
} else patches.push({
|
|
3637
|
+
op: "set_string",
|
|
3638
|
+
fieldId,
|
|
3639
|
+
value
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
return patches;
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Validate that all initial values reference valid field IDs.
|
|
3646
|
+
*
|
|
3647
|
+
* @param patches Patches to validate
|
|
3648
|
+
* @param validFieldIds Set of valid field IDs from the form
|
|
3649
|
+
* @returns Array of invalid field IDs (empty if all valid)
|
|
3650
|
+
*/
|
|
3651
|
+
function validateInitialValueFields(patches, validFieldIds) {
|
|
3652
|
+
const invalid = [];
|
|
3653
|
+
for (const patch of patches) if ("fieldId" in patch && patch.fieldId && !validFieldIds.has(patch.fieldId)) invalid.push(patch.fieldId);
|
|
3654
|
+
return invalid;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
//#endregion
|
|
3658
|
+
//#region src/cli/commands/research.ts
|
|
3659
|
+
/**
|
|
3660
|
+
* Register the research command.
|
|
3661
|
+
*/
|
|
3662
|
+
function registerResearchCommand(program) {
|
|
3663
|
+
program.command("research <input>").description("Fill a form using a web-search-enabled model").option("--model <provider/model>", "LLM model to use (e.g., google/gemini-2.5-flash). Required.").option("--output <path>", "Output path for filled form (default: auto-generated in forms directory)").option("--input <fieldId=value>", "Set initial field value (can be used multiple times)", (value, previous) => previous.concat([value]), []).option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN})`, String(DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues per turn (default: ${DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN})`, String(DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN)).option("--transcript", "Save session transcript").action(async (input, options, cmd) => {
|
|
3664
|
+
const ctx = getCommandContext(cmd);
|
|
3665
|
+
const startTime = Date.now();
|
|
3666
|
+
try {
|
|
3667
|
+
if (!options.model) {
|
|
3668
|
+
logError("--model is required");
|
|
3669
|
+
console.log("");
|
|
3670
|
+
console.log(formatSuggestedLlms());
|
|
3671
|
+
process.exit(1);
|
|
3672
|
+
}
|
|
3673
|
+
const modelId = options.model;
|
|
3674
|
+
if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
|
|
3675
|
+
const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
|
|
3676
|
+
logError(`Model "${modelId}" does not support web search.`);
|
|
3677
|
+
console.log("");
|
|
3678
|
+
console.log(pc.yellow("Research forms require web search capabilities."));
|
|
3679
|
+
console.log(`Use a model from: ${webSearchProviders.join(", ")}`);
|
|
3680
|
+
console.log("");
|
|
3681
|
+
console.log("Examples:");
|
|
3682
|
+
console.log(" --model openai/gpt-5-mini");
|
|
3683
|
+
console.log(" --model anthropic/claude-sonnet-4-5");
|
|
3684
|
+
console.log(" --model google/gemini-2.5-flash");
|
|
3685
|
+
console.log(" --model xai/grok-4");
|
|
3686
|
+
process.exit(1);
|
|
3687
|
+
}
|
|
3688
|
+
const inputPath = resolve(input);
|
|
3689
|
+
logVerbose(ctx, `Input: ${inputPath}`);
|
|
3690
|
+
const form = parseForm(await readFile$1(inputPath));
|
|
3691
|
+
logVerbose(ctx, `Parsed form: ${form.schema.id}`);
|
|
3692
|
+
const initialInputs = options.input ?? [];
|
|
3693
|
+
if (initialInputs.length > 0) {
|
|
3694
|
+
const patches = parseInitialValues(initialInputs);
|
|
3695
|
+
const invalidFields = validateInitialValueFields(patches, new Set(form.orderIndex));
|
|
3696
|
+
if (invalidFields.length > 0) logWarn(ctx, `Unknown field IDs: ${invalidFields.join(", ")}`);
|
|
3697
|
+
applyPatches(form, patches);
|
|
3698
|
+
logInfo(ctx, `Applied ${patches.length} initial value(s)`);
|
|
3699
|
+
}
|
|
3700
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
3701
|
+
let outputPath;
|
|
3702
|
+
if (options.output) outputPath = resolve(options.output);
|
|
3703
|
+
else outputPath = generateVersionedPathInFormsDir(inputPath, formsDir);
|
|
3704
|
+
logVerbose(ctx, `Output: ${outputPath}`);
|
|
3705
|
+
const maxTurns = parseInt(options.maxTurns, 10);
|
|
3706
|
+
const maxPatchesPerTurn = parseInt(options.maxPatches, 10);
|
|
3707
|
+
const maxIssuesPerTurn = parseInt(options.maxIssues, 10);
|
|
3708
|
+
logInfo(ctx, `Research fill with model: ${modelId}`);
|
|
3709
|
+
logVerbose(ctx, `Max turns: ${maxTurns}`);
|
|
3710
|
+
logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
|
|
3711
|
+
logVerbose(ctx, `Max issues/turn: ${maxIssuesPerTurn}`);
|
|
3712
|
+
const result = await runResearch(form, {
|
|
3713
|
+
model: modelId,
|
|
3714
|
+
maxTurns,
|
|
3715
|
+
maxPatchesPerTurn,
|
|
3716
|
+
maxIssuesPerTurn,
|
|
3717
|
+
targetRoles: [AGENT_ROLE],
|
|
3718
|
+
fillMode: "continue"
|
|
3719
|
+
});
|
|
3720
|
+
if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
|
|
3721
|
+
logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
|
|
3722
|
+
logInfo(ctx, `Turns: ${result.totalTurns}`);
|
|
3723
|
+
if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
|
|
3724
|
+
const { formPath, rawPath, yamlPath } = await exportMultiFormat(result.form, outputPath);
|
|
3725
|
+
logSuccess(ctx, "Outputs:");
|
|
3726
|
+
console.log(` ${formPath} ${pc.dim("(markform)")}`);
|
|
3727
|
+
console.log(` ${rawPath} ${pc.dim("(plain markdown)")}`);
|
|
3728
|
+
console.log(` ${yamlPath} ${pc.dim("(values as YAML)")}`);
|
|
3729
|
+
if (options.transcript && result.transcript) {
|
|
3730
|
+
const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
|
|
3731
|
+
const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
|
|
3732
|
+
const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
|
|
3733
|
+
await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
|
|
3734
|
+
logInfo(ctx, `Transcript: ${transcriptPath}`);
|
|
3735
|
+
}
|
|
3736
|
+
logTiming(ctx, "Research fill", Date.now() - startTime);
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
3739
|
+
process.exit(1);
|
|
3740
|
+
}
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
|
|
2819
3744
|
//#endregion
|
|
2820
3745
|
//#region src/cli/commands/validate.ts
|
|
2821
3746
|
/**
|
|
@@ -2875,12 +3800,10 @@ function formatConsoleReport(report, useColors) {
|
|
|
2875
3800
|
lines.push(bold("Progress:"));
|
|
2876
3801
|
lines.push(` Total fields: ${progress.counts.totalFields}`);
|
|
2877
3802
|
lines.push(` Required: ${progress.counts.requiredFields}`);
|
|
2878
|
-
lines.push(`
|
|
2879
|
-
lines.push(`
|
|
2880
|
-
lines.push(`
|
|
2881
|
-
lines.push(`
|
|
2882
|
-
lines.push(` Empty (required): ${progress.counts.emptyRequiredFields}`);
|
|
2883
|
-
lines.push(` Empty (optional): ${progress.counts.emptyOptionalFields}`);
|
|
3803
|
+
lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
|
|
3804
|
+
lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
|
|
3805
|
+
lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
|
|
3806
|
+
lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
|
|
2884
3807
|
lines.push("");
|
|
2885
3808
|
if (report.issues.length > 0) {
|
|
2886
3809
|
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
@@ -2900,7 +3823,7 @@ function registerValidateCommand(program) {
|
|
|
2900
3823
|
const ctx = getCommandContext(cmd);
|
|
2901
3824
|
try {
|
|
2902
3825
|
logVerbose(ctx, `Reading file: ${file}`);
|
|
2903
|
-
const content = await readFile(file);
|
|
3826
|
+
const content = await readFile$1(file);
|
|
2904
3827
|
logVerbose(ctx, "Parsing form...");
|
|
2905
3828
|
const form = parseForm(content);
|
|
2906
3829
|
logVerbose(ctx, "Running validation...");
|
|
@@ -2952,18 +3875,22 @@ function withColoredHelp(cmd) {
|
|
|
2952
3875
|
*/
|
|
2953
3876
|
function createProgram() {
|
|
2954
3877
|
const program = withColoredHelp(new Command());
|
|
2955
|
-
program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION).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");
|
|
2956
|
-
|
|
2957
|
-
|
|
3878
|
+
program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION).showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`);
|
|
3879
|
+
registerReadmeCommand(program);
|
|
3880
|
+
registerDocsCommand(program);
|
|
3881
|
+
registerSpecCommand(program);
|
|
2958
3882
|
registerApplyCommand(program);
|
|
2959
|
-
registerExportCommand(program);
|
|
2960
3883
|
registerDumpCommand(program);
|
|
2961
|
-
|
|
2962
|
-
|
|
3884
|
+
registerExamplesCommand(program);
|
|
3885
|
+
registerExportCommand(program);
|
|
2963
3886
|
registerFillCommand(program);
|
|
3887
|
+
registerInspectCommand(program);
|
|
2964
3888
|
registerModelsCommand(program);
|
|
2965
|
-
|
|
2966
|
-
|
|
3889
|
+
registerRenderCommand(program);
|
|
3890
|
+
registerReportCommand(program);
|
|
3891
|
+
registerResearchCommand(program);
|
|
3892
|
+
registerServeCommand(program);
|
|
3893
|
+
registerValidateCommand(program);
|
|
2967
3894
|
return program;
|
|
2968
3895
|
}
|
|
2969
3896
|
/**
|