markform 0.1.1 → 0.1.3
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 +340 -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-00UmzDKL.mjs} +849 -730
- package/dist/bin.mjs +6 -3
- package/dist/{cli-pjOiHgCW.mjs → cli-D--Lel-e.mjs} +1374 -428
- 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-Dm8jZ5dl.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 +54 -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 +10 -14
- 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 parseRolesFlag, D as deriveReportPath, E as deriveExportPath, F as hasWebSearchSupport, M as WEB_SEARCH_CONFIG, N as formatSuggestedLlms, O as detectFileType, T as USER_ROLE, _ as DEFAULT_MAX_TURNS, b as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, d as serializeRawMarkdown, f as serializeReportMarkdown, g as DEFAULT_MAX_PATCHES_PER_TURN, h as DEFAULT_MAX_ISSUES_PER_TURN, j as SUGGESTED_LLMS, k as getFormsDir, m as DEFAULT_FORMS_DIR, p as AGENT_ROLE, r as inspect, t as applyPatches, u as serialize, v as DEFAULT_PORT, w as REPORT_EXTENSION, x as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN } from "./apply-00UmzDKL.mjs";
|
|
3
|
+
import { a as resolveHarnessConfig, c as getProviderNames, f as createMockAgent, i as runResearch, l as resolveModel, m as createHarness, s as getProviderInfo, t as VERSION, u as createLiveAgent, v as parseForm } from "./src-Dm8jZ5dl.mjs";
|
|
4
|
+
import { n as serializeSession } from "./session-Bqnwi9wp.mjs";
|
|
5
|
+
import { a as getCommandContext, c as logInfo, d as logVerbose, f as logWarn, h as writeFile, i as formatPath, l as logSuccess, n as ensureFormsDir, o as logDryRun, p as readFile$1, r as formatOutput, s as logError, t as OUTPUT_FORMATS, u as logTiming } from "./shared-N_s1M-_K.mjs";
|
|
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,72 +248,209 @@ 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.
|
|
336
|
+
*
|
|
337
|
+
* Standard exports: report, values (yaml), form.
|
|
338
|
+
* Raw markdown is available via CLI but not in standard exports.
|
|
421
339
|
*
|
|
422
340
|
* @param basePath - Path to the .form.md file
|
|
423
341
|
* @returns Object with paths for all export formats
|
|
424
342
|
*/
|
|
425
343
|
function deriveExportPaths(basePath) {
|
|
426
344
|
return {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
345
|
+
reportPath: deriveReportPath(basePath),
|
|
346
|
+
yamlPath: deriveExportPath(basePath, "yaml"),
|
|
347
|
+
formPath: deriveExportPath(basePath, "form")
|
|
430
348
|
};
|
|
431
349
|
}
|
|
432
350
|
/**
|
|
433
351
|
* Export form to multiple formats.
|
|
434
352
|
*
|
|
435
|
-
*
|
|
353
|
+
* Standard exports:
|
|
354
|
+
* - Report format (.report.md) - filtered markdown (excludes instructions, report=false)
|
|
355
|
+
* - YAML values (.yml) - structured format with state and notes
|
|
436
356
|
* - Markform format (.form.md) - canonical form with directives
|
|
437
|
-
*
|
|
438
|
-
*
|
|
357
|
+
*
|
|
358
|
+
* Note: Raw markdown (.raw.md) is available via CLI `markform export --raw`
|
|
359
|
+
* but is not included in standard multi-format export.
|
|
439
360
|
*
|
|
440
361
|
* @param form - The parsed form to export
|
|
441
362
|
* @param basePath - Base path for the .form.md file (other paths are derived)
|
|
442
363
|
* @returns Paths to all exported files
|
|
443
364
|
*/
|
|
444
|
-
function exportMultiFormat(form, basePath) {
|
|
365
|
+
async function exportMultiFormat(form, basePath) {
|
|
445
366
|
const paths = deriveExportPaths(basePath);
|
|
367
|
+
const reportContent = serializeReportMarkdown(form);
|
|
368
|
+
await writeFile(paths.reportPath, reportContent);
|
|
369
|
+
const values = toStructuredValues(form);
|
|
370
|
+
const notes = toNotesArray(form);
|
|
371
|
+
const exportData = {
|
|
372
|
+
values,
|
|
373
|
+
...notes.length > 0 && { notes }
|
|
374
|
+
};
|
|
375
|
+
const yamlContent = YAML.stringify(exportData);
|
|
376
|
+
await writeFile(paths.yamlPath, yamlContent);
|
|
446
377
|
const formContent = serialize(form);
|
|
447
|
-
|
|
448
|
-
const rawContent = serializeRawMarkdown(form);
|
|
449
|
-
writeFileSync(paths.rawPath, rawContent, "utf-8");
|
|
450
|
-
const values = toPlainValues(form);
|
|
451
|
-
const yamlContent = YAML.stringify(values);
|
|
452
|
-
writeFileSync(paths.yamlPath, yamlContent, "utf-8");
|
|
378
|
+
await writeFile(paths.formPath, formContent);
|
|
453
379
|
return paths;
|
|
454
380
|
}
|
|
455
381
|
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/cli/commands/dump.ts
|
|
384
|
+
/**
|
|
385
|
+
* Format a field response for console display, including state information.
|
|
386
|
+
*/
|
|
387
|
+
function formatFieldResponse(response, useColors) {
|
|
388
|
+
const dim = useColors ? pc.dim : (s) => s;
|
|
389
|
+
const green = useColors ? pc.green : (s) => s;
|
|
390
|
+
const yellow = useColors ? pc.yellow : (s) => s;
|
|
391
|
+
if (response.state === "unanswered") return dim("(unanswered)");
|
|
392
|
+
if (response.state === "skipped") return yellow(`[skipped]${response.reason ? ` ${response.reason}` : ""}`);
|
|
393
|
+
if (response.state === "aborted") return yellow(`[aborted]${response.reason ? ` ${response.reason}` : ""}`);
|
|
394
|
+
const value = response.value;
|
|
395
|
+
if (!value) return dim("(empty)");
|
|
396
|
+
switch (value.kind) {
|
|
397
|
+
case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
398
|
+
case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
399
|
+
case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
400
|
+
case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
|
|
401
|
+
case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
|
|
402
|
+
case "checkboxes": {
|
|
403
|
+
const entries = Object.entries(value.values);
|
|
404
|
+
if (entries.length === 0) return dim("(no entries)");
|
|
405
|
+
return entries.map(([k, v]) => `${k}:${v}`).join(", ");
|
|
406
|
+
}
|
|
407
|
+
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
408
|
+
case "url_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
409
|
+
default: return dim("(unknown)");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Format form responses for console output, showing all fields with their states.
|
|
414
|
+
*/
|
|
415
|
+
function formatConsoleResponses(form, useColors) {
|
|
416
|
+
const lines = [];
|
|
417
|
+
const bold = useColors ? pc.bold : (s) => s;
|
|
418
|
+
const dim = useColors ? pc.dim : (s) => s;
|
|
419
|
+
for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
|
|
420
|
+
const valueStr = formatFieldResponse(response, useColors);
|
|
421
|
+
lines.push(`${bold(fieldId)}: ${valueStr}`);
|
|
422
|
+
}
|
|
423
|
+
if (lines.length === 0) lines.push(dim("(no fields)"));
|
|
424
|
+
return lines.join("\n");
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Register the dump command.
|
|
428
|
+
*/
|
|
429
|
+
function registerDumpCommand(program) {
|
|
430
|
+
program.command("dump <file>").description("Extract and display form values with state (lightweight inspect)").action(async (file, _options, cmd) => {
|
|
431
|
+
const ctx = getCommandContext(cmd);
|
|
432
|
+
try {
|
|
433
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
434
|
+
const content = await readFile$1(file);
|
|
435
|
+
logVerbose(ctx, "Parsing form...");
|
|
436
|
+
const form = parseForm(content);
|
|
437
|
+
if (ctx.format === "json" || ctx.format === "yaml") {
|
|
438
|
+
const output = formatOutput(ctx, {
|
|
439
|
+
values: toStructuredValues(form),
|
|
440
|
+
...form.notes.length > 0 && { notes: toNotesArray(form) }
|
|
441
|
+
}, () => "");
|
|
442
|
+
console.log(output);
|
|
443
|
+
} else {
|
|
444
|
+
const output = formatOutput(ctx, form, (data, useColors) => formatConsoleResponses(data, useColors));
|
|
445
|
+
console.log(output);
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
456
454
|
//#endregion
|
|
457
455
|
//#region src/cli/lib/patchFormat.ts
|
|
458
456
|
/** Maximum characters for a patch value display before truncation */
|
|
@@ -477,8 +475,13 @@ function formatPatchValue(patch) {
|
|
|
477
475
|
case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
|
|
478
476
|
case "clear_field": return "(cleared)";
|
|
479
477
|
case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
|
|
478
|
+
case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
|
|
480
479
|
case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
481
480
|
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
481
|
+
case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
482
|
+
case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
483
|
+
case "add_note": return truncate(`note: ${patch.text}`);
|
|
484
|
+
case "remove_note": return `(remove note ${patch.noteId})`;
|
|
482
485
|
}
|
|
483
486
|
}
|
|
484
487
|
/**
|
|
@@ -494,46 +497,103 @@ function formatPatchType(patch) {
|
|
|
494
497
|
case "set_checkboxes": return "checkboxes";
|
|
495
498
|
case "clear_field": return "clear";
|
|
496
499
|
case "skip_field": return "skip";
|
|
500
|
+
case "abort_field": return "abort";
|
|
497
501
|
case "set_url": return "url";
|
|
498
502
|
case "set_url_list": return "url_list";
|
|
503
|
+
case "set_date": return "date";
|
|
504
|
+
case "set_year": return "year";
|
|
505
|
+
case "add_note": return "note";
|
|
506
|
+
case "remove_note": return "remove_note";
|
|
499
507
|
}
|
|
500
508
|
}
|
|
501
509
|
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/cli/lib/formatting.ts
|
|
512
|
+
/**
|
|
513
|
+
* Get a short status word from an issue reason.
|
|
514
|
+
*/
|
|
515
|
+
function issueReasonToStatus(reason) {
|
|
516
|
+
switch (reason) {
|
|
517
|
+
case "required_missing": return "missing";
|
|
518
|
+
case "validation_error": return "invalid";
|
|
519
|
+
case "checkbox_incomplete": return "incomplete";
|
|
520
|
+
case "min_items_not_met": return "too-few";
|
|
521
|
+
case "optional_empty": return "empty";
|
|
522
|
+
default: return "issue";
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Format a single issue as "fieldId (status)".
|
|
527
|
+
*/
|
|
528
|
+
function formatIssueBrief(issue) {
|
|
529
|
+
const status = issueReasonToStatus(issue.reason);
|
|
530
|
+
return `${issue.ref} (${status})`;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Format issues for turn logging - shows count and brief field list.
|
|
534
|
+
* Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
|
|
535
|
+
*/
|
|
536
|
+
function formatTurnIssues(issues, maxShow = 5) {
|
|
537
|
+
const count = issues.length;
|
|
538
|
+
if (count === 0) return "0 issues";
|
|
539
|
+
return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
502
542
|
//#endregion
|
|
503
543
|
//#region src/cli/examples/exampleRegistry.ts
|
|
504
544
|
/**
|
|
505
545
|
* Example form registry.
|
|
506
546
|
* Provides form content from the examples directory for the examples CLI command.
|
|
547
|
+
*
|
|
548
|
+
* Metadata (title, description) is loaded dynamically from the form's YAML frontmatter
|
|
549
|
+
* rather than being duplicated here, following the single source of truth principle.
|
|
550
|
+
*/
|
|
551
|
+
/**
|
|
552
|
+
* Example definitions without content or metadata.
|
|
553
|
+
* Title and description are loaded dynamically from frontmatter.
|
|
507
554
|
*/
|
|
508
|
-
/** Example definitions without content - content is loaded lazily. */
|
|
509
555
|
const EXAMPLE_DEFINITIONS = [
|
|
510
556
|
{
|
|
511
557
|
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
558
|
filename: "simple.form.md",
|
|
515
|
-
path: "simple/simple.form.md"
|
|
559
|
+
path: "simple/simple.form.md",
|
|
560
|
+
type: "fill"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
id: "movie-research-minimal",
|
|
564
|
+
filename: "movie-research-minimal.form.md",
|
|
565
|
+
path: "movie-research/movie-research-minimal.form.md",
|
|
566
|
+
type: "research"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "movie-research-basic",
|
|
570
|
+
filename: "movie-research-basic.form.md",
|
|
571
|
+
path: "movie-research/movie-research-basic.form.md",
|
|
572
|
+
type: "research"
|
|
516
573
|
},
|
|
517
574
|
{
|
|
518
|
-
id: "
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
path: "political-research/political-research.form.md"
|
|
575
|
+
id: "movie-research-deep",
|
|
576
|
+
filename: "movie-research-deep.form.md",
|
|
577
|
+
path: "movie-research/movie-research-deep.form.md",
|
|
578
|
+
type: "research"
|
|
523
579
|
},
|
|
524
580
|
{
|
|
525
581
|
id: "earnings-analysis",
|
|
526
|
-
title: "Company Quarterly Analysis",
|
|
527
|
-
description: "Financial analysis with one user field (company) and agent-filled quarterly analysis sections.",
|
|
528
582
|
filename: "earnings-analysis.form.md",
|
|
529
|
-
path: "earnings-analysis/earnings-analysis.form.md"
|
|
583
|
+
path: "earnings-analysis/earnings-analysis.form.md",
|
|
584
|
+
type: "research"
|
|
530
585
|
},
|
|
531
586
|
{
|
|
532
587
|
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
588
|
filename: "startup-deep-research.form.md",
|
|
536
|
-
path: "startup-deep-research/startup-deep-research.form.md"
|
|
589
|
+
path: "startup-deep-research/startup-deep-research.form.md",
|
|
590
|
+
type: "research"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
id: "celebrity-deep-research",
|
|
594
|
+
filename: "celebrity-deep-research.form.md",
|
|
595
|
+
path: "celebrity-deep-research/celebrity-deep-research.form.md",
|
|
596
|
+
type: "research"
|
|
537
597
|
}
|
|
538
598
|
];
|
|
539
599
|
/**
|
|
@@ -547,7 +607,7 @@ function getExamplesDir() {
|
|
|
547
607
|
}
|
|
548
608
|
/**
|
|
549
609
|
* Load the content of an example form.
|
|
550
|
-
* @param exampleId - The example ID (e.g., 'simple', '
|
|
610
|
+
* @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
|
|
551
611
|
* @returns The form content as a string
|
|
552
612
|
* @throws Error if the example is not found
|
|
553
613
|
*/
|
|
@@ -569,7 +629,7 @@ function getExampleById(id) {
|
|
|
569
629
|
}
|
|
570
630
|
/**
|
|
571
631
|
* Get the absolute path to an example's source file.
|
|
572
|
-
* @param exampleId - The example ID (e.g., 'simple', '
|
|
632
|
+
* @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
|
|
573
633
|
* @returns The absolute path to the example form file
|
|
574
634
|
* @throws Error if the example is not found
|
|
575
635
|
*/
|
|
@@ -578,6 +638,48 @@ function getExamplePath(exampleId) {
|
|
|
578
638
|
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
579
639
|
return join(getExamplesDir(), example.path);
|
|
580
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Extract YAML frontmatter from a markdown file content.
|
|
643
|
+
* @param content - The markdown file content
|
|
644
|
+
* @returns The parsed frontmatter object or null if no frontmatter found
|
|
645
|
+
*/
|
|
646
|
+
function extractFrontmatter(content) {
|
|
647
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
648
|
+
if (!frontmatterMatch || !frontmatterMatch[1]) return null;
|
|
649
|
+
try {
|
|
650
|
+
return YAML.parse(frontmatterMatch[1]);
|
|
651
|
+
} catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Load metadata (title, description) from an example's YAML frontmatter.
|
|
657
|
+
* @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
|
|
658
|
+
* @returns Object with title and description from frontmatter
|
|
659
|
+
*/
|
|
660
|
+
function loadExampleMetadata(exampleId) {
|
|
661
|
+
const frontmatter = extractFrontmatter(loadExampleContent(exampleId));
|
|
662
|
+
if (!frontmatter || !frontmatter.markform) return {};
|
|
663
|
+
const markform = frontmatter.markform;
|
|
664
|
+
return {
|
|
665
|
+
title: typeof markform.title === "string" ? markform.title : void 0,
|
|
666
|
+
description: typeof markform.description === "string" ? markform.description : void 0
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Get all example definitions with metadata loaded from frontmatter.
|
|
671
|
+
* @returns Array of ExampleDefinition with title and description populated
|
|
672
|
+
*/
|
|
673
|
+
function getAllExamplesWithMetadata() {
|
|
674
|
+
return EXAMPLE_DEFINITIONS.map((example) => {
|
|
675
|
+
const metadata = loadExampleMetadata(example.id);
|
|
676
|
+
return {
|
|
677
|
+
...example,
|
|
678
|
+
title: metadata.title,
|
|
679
|
+
description: metadata.description
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
}
|
|
581
683
|
|
|
582
684
|
//#endregion
|
|
583
685
|
//#region src/cli/lib/versioning.ts
|
|
@@ -654,6 +756,29 @@ function generateVersionedPath(filePath) {
|
|
|
654
756
|
}
|
|
655
757
|
return candidate;
|
|
656
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Generate a versioned filename within the forms directory.
|
|
761
|
+
*
|
|
762
|
+
* Derives the base name from the input path and creates a versioned
|
|
763
|
+
* output path within the specified forms directory.
|
|
764
|
+
*
|
|
765
|
+
* @param inputPath - Original input file path (used to derive basename)
|
|
766
|
+
* @param formsDir - Absolute path to the forms directory
|
|
767
|
+
* @returns Absolute path to a non-existent versioned file in formsDir
|
|
768
|
+
*/
|
|
769
|
+
function generateVersionedPathInFormsDir(inputPath, formsDir) {
|
|
770
|
+
const inputFilename = basename(inputPath);
|
|
771
|
+
const parsed = parseVersionedPath(inputFilename);
|
|
772
|
+
const baseName = parsed?.base ?? inputFilename.replace(/\.form\.md$/i, "");
|
|
773
|
+
const extension = parsed?.extension ?? ".form.md";
|
|
774
|
+
let version = 1;
|
|
775
|
+
let candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
|
|
776
|
+
while (existsSync(candidate)) {
|
|
777
|
+
version++;
|
|
778
|
+
candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
|
|
779
|
+
}
|
|
780
|
+
return candidate;
|
|
781
|
+
}
|
|
657
782
|
|
|
658
783
|
//#endregion
|
|
659
784
|
//#region src/cli/lib/interactivePrompts.ts
|
|
@@ -683,7 +808,7 @@ function getFieldById(form, fieldId) {
|
|
|
683
808
|
*/
|
|
684
809
|
function formatFieldLabel(ctx) {
|
|
685
810
|
const required = ctx.field.required ? pc.red("*") : "";
|
|
686
|
-
const progress =
|
|
811
|
+
const progress = `(${ctx.index} of ${ctx.total})`;
|
|
687
812
|
return `${ctx.field.label}${required} ${progress}`;
|
|
688
813
|
}
|
|
689
814
|
/**
|
|
@@ -693,6 +818,7 @@ function createSkipPatch(field) {
|
|
|
693
818
|
return {
|
|
694
819
|
op: "skip_field",
|
|
695
820
|
fieldId: field.id,
|
|
821
|
+
role: "user",
|
|
696
822
|
reason: "User skipped in console"
|
|
697
823
|
};
|
|
698
824
|
}
|
|
@@ -1057,9 +1183,10 @@ async function runInteractiveFill(form, issues) {
|
|
|
1057
1183
|
const field = getFieldById(form, issue.ref);
|
|
1058
1184
|
if (!field) continue;
|
|
1059
1185
|
index++;
|
|
1186
|
+
const response = form.responsesByFieldId[field.id];
|
|
1060
1187
|
const patch = await promptForField({
|
|
1061
1188
|
field,
|
|
1062
|
-
currentValue:
|
|
1189
|
+
currentValue: response?.state === "answered" ? response.value : void 0,
|
|
1063
1190
|
description: getFieldDescription(form, field.id),
|
|
1064
1191
|
index,
|
|
1065
1192
|
total: uniqueFieldIssues.length
|
|
@@ -1110,6 +1237,196 @@ function showInteractiveOutro(patchCount, cancelled) {
|
|
|
1110
1237
|
p.outro(`✓ ${patchCount} field(s) updated.`);
|
|
1111
1238
|
}
|
|
1112
1239
|
|
|
1240
|
+
//#endregion
|
|
1241
|
+
//#region src/cli/lib/fileViewer.ts
|
|
1242
|
+
/**
|
|
1243
|
+
* File viewer utility for displaying files with colorization and pagination.
|
|
1244
|
+
*
|
|
1245
|
+
* Provides a modern console experience:
|
|
1246
|
+
* - Syntax highlighting for markdown and YAML
|
|
1247
|
+
* - Pagination using system pager (less) when available
|
|
1248
|
+
* - Fallback to console output when not interactive
|
|
1249
|
+
*/
|
|
1250
|
+
/**
|
|
1251
|
+
* Check if stdout is an interactive terminal.
|
|
1252
|
+
*/
|
|
1253
|
+
function isInteractive$2() {
|
|
1254
|
+
return process.stdout.isTTY === true;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Apply terminal formatting to markdown content.
|
|
1258
|
+
* Colorizes headers, code blocks, and other elements.
|
|
1259
|
+
*/
|
|
1260
|
+
function formatMarkdown$2(content) {
|
|
1261
|
+
const lines = content.split("\n");
|
|
1262
|
+
const formatted = [];
|
|
1263
|
+
let inCodeBlock = false;
|
|
1264
|
+
for (const line of lines) {
|
|
1265
|
+
if (line.startsWith("```")) {
|
|
1266
|
+
inCodeBlock = !inCodeBlock;
|
|
1267
|
+
formatted.push(pc.dim(line));
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
if (inCodeBlock) {
|
|
1271
|
+
formatted.push(pc.cyan(line));
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (line.startsWith("# ")) {
|
|
1275
|
+
formatted.push(pc.bold(pc.magenta(line)));
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (line.startsWith("## ")) {
|
|
1279
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (line.startsWith("### ")) {
|
|
1283
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (line.startsWith("#### ")) {
|
|
1287
|
+
formatted.push(pc.bold(line));
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1291
|
+
return pc.yellow(code);
|
|
1292
|
+
});
|
|
1293
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
1294
|
+
return pc.bold(text);
|
|
1295
|
+
});
|
|
1296
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
1297
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
1298
|
+
});
|
|
1299
|
+
formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
|
|
1300
|
+
return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
|
|
1301
|
+
});
|
|
1302
|
+
formatted.push(formattedLine);
|
|
1303
|
+
}
|
|
1304
|
+
return formatted.join("\n");
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Apply terminal formatting to YAML content.
|
|
1308
|
+
*/
|
|
1309
|
+
function formatYaml(content) {
|
|
1310
|
+
const lines = content.split("\n");
|
|
1311
|
+
const formatted = [];
|
|
1312
|
+
for (const line of lines) {
|
|
1313
|
+
if (line.trim().startsWith("#")) {
|
|
1314
|
+
formatted.push(pc.dim(line));
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
|
|
1318
|
+
if (match) {
|
|
1319
|
+
const [, indent, key, colon, value] = match;
|
|
1320
|
+
formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
if (line.trim().startsWith("-")) {
|
|
1324
|
+
formatted.push(pc.green(line));
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
formatted.push(line);
|
|
1328
|
+
}
|
|
1329
|
+
return formatted.join("\n");
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Format file content based on extension.
|
|
1333
|
+
*/
|
|
1334
|
+
function formatContent(content, filename) {
|
|
1335
|
+
if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
|
|
1336
|
+
if (filename.endsWith(".md")) return formatMarkdown$2(content);
|
|
1337
|
+
return content;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Display content using system pager (less) if available.
|
|
1341
|
+
* Falls back to console.log if not interactive or pager unavailable.
|
|
1342
|
+
*
|
|
1343
|
+
* @returns Promise that resolves when viewing is complete
|
|
1344
|
+
*/
|
|
1345
|
+
async function displayWithPager(content, title) {
|
|
1346
|
+
if (!isInteractive$2()) {
|
|
1347
|
+
console.log(content);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
|
|
1351
|
+
return new Promise((resolve$1) => {
|
|
1352
|
+
const pager = spawn("less", [
|
|
1353
|
+
"-R",
|
|
1354
|
+
"-S",
|
|
1355
|
+
"-X",
|
|
1356
|
+
"-F",
|
|
1357
|
+
"-K"
|
|
1358
|
+
], { stdio: [
|
|
1359
|
+
"pipe",
|
|
1360
|
+
"inherit",
|
|
1361
|
+
"inherit"
|
|
1362
|
+
] });
|
|
1363
|
+
pager.on("error", () => {
|
|
1364
|
+
console.log(header);
|
|
1365
|
+
console.log("");
|
|
1366
|
+
console.log(content);
|
|
1367
|
+
console.log("");
|
|
1368
|
+
resolve$1();
|
|
1369
|
+
});
|
|
1370
|
+
pager.on("close", () => {
|
|
1371
|
+
resolve$1();
|
|
1372
|
+
});
|
|
1373
|
+
pager.stdin.write(header + "\n\n");
|
|
1374
|
+
pager.stdin.write(content);
|
|
1375
|
+
pager.stdin.end();
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Load and display a file with formatting and pagination.
|
|
1380
|
+
*/
|
|
1381
|
+
async function viewFile(filePath) {
|
|
1382
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1383
|
+
const filename = basename(filePath);
|
|
1384
|
+
await displayWithPager(formatContent(content, filename), filename);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Show an interactive file viewer chooser.
|
|
1388
|
+
*
|
|
1389
|
+
* Presents a list of files to view:
|
|
1390
|
+
* - "Show report:" for the report output (.report.md) at the top
|
|
1391
|
+
* - "Show source:" for other files (.form.md, .raw.md, .yml)
|
|
1392
|
+
* - "Quit" at the bottom
|
|
1393
|
+
*
|
|
1394
|
+
* Loops until the user selects Quit.
|
|
1395
|
+
*
|
|
1396
|
+
* @param files Array of file options to display
|
|
1397
|
+
*/
|
|
1398
|
+
async function showFileViewerChooser(files) {
|
|
1399
|
+
if (!isInteractive$2()) return;
|
|
1400
|
+
console.log("");
|
|
1401
|
+
const reportFile = files.find((f) => f.path.endsWith(".report.md"));
|
|
1402
|
+
const sourceFiles = files.filter((f) => !f.path.endsWith(".report.md"));
|
|
1403
|
+
while (true) {
|
|
1404
|
+
const options = [];
|
|
1405
|
+
if (reportFile) options.push({
|
|
1406
|
+
value: reportFile.path,
|
|
1407
|
+
label: `Show report: ${pc.green(basename(reportFile.path))}`,
|
|
1408
|
+
hint: reportFile.hint ?? ""
|
|
1409
|
+
});
|
|
1410
|
+
for (const file of sourceFiles) options.push({
|
|
1411
|
+
value: file.path,
|
|
1412
|
+
label: `Show source: ${pc.green(basename(file.path))}`,
|
|
1413
|
+
hint: file.hint ?? ""
|
|
1414
|
+
});
|
|
1415
|
+
options.push({
|
|
1416
|
+
value: "quit",
|
|
1417
|
+
label: "Quit",
|
|
1418
|
+
hint: ""
|
|
1419
|
+
});
|
|
1420
|
+
const selection = await p.select({
|
|
1421
|
+
message: "View files:",
|
|
1422
|
+
options
|
|
1423
|
+
});
|
|
1424
|
+
if (p.isCancel(selection) || selection === "quit") break;
|
|
1425
|
+
await viewFile(selection);
|
|
1426
|
+
console.log("");
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1113
1430
|
//#endregion
|
|
1114
1431
|
//#region src/cli/commands/examples.ts
|
|
1115
1432
|
/**
|
|
@@ -1117,10 +1434,12 @@ function showInteractiveOutro(patchCount, cancelled) {
|
|
|
1117
1434
|
*/
|
|
1118
1435
|
function printExamplesList() {
|
|
1119
1436
|
console.log(pc.bold("Available examples:\n"));
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
console.log(`
|
|
1437
|
+
const examples = getAllExamplesWithMetadata();
|
|
1438
|
+
for (const example of examples) {
|
|
1439
|
+
const typeLabel = example.type === "research" ? pc.magenta("[research]") : pc.blue("[fill]");
|
|
1440
|
+
console.log(` ${pc.cyan(example.id)} ${typeLabel}`);
|
|
1441
|
+
console.log(` ${pc.bold(example.title ?? example.id)}`);
|
|
1442
|
+
console.log(` ${example.description ?? "No description"}`);
|
|
1124
1443
|
console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
|
|
1125
1444
|
console.log("");
|
|
1126
1445
|
}
|
|
@@ -1129,12 +1448,12 @@ function printExamplesList() {
|
|
|
1129
1448
|
* Display API availability status at startup.
|
|
1130
1449
|
*/
|
|
1131
1450
|
function showApiStatus() {
|
|
1132
|
-
console.log(
|
|
1451
|
+
console.log("API Status:");
|
|
1133
1452
|
for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1134
1453
|
const info = getProviderInfo(provider);
|
|
1135
1454
|
const hasKey = !!process.env[info.envVar];
|
|
1136
|
-
const status = hasKey ? pc.green("✓") :
|
|
1137
|
-
const envVar = hasKey ?
|
|
1455
|
+
const status = hasKey ? pc.green("✓") : "○";
|
|
1456
|
+
const envVar = hasKey ? info.envVar : pc.yellow(info.envVar);
|
|
1138
1457
|
console.log(` ${status} ${provider} (${envVar})`);
|
|
1139
1458
|
}
|
|
1140
1459
|
console.log("");
|
|
@@ -1146,7 +1465,7 @@ function buildModelOptions() {
|
|
|
1146
1465
|
const options = [];
|
|
1147
1466
|
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1148
1467
|
const info = getProviderInfo(provider);
|
|
1149
|
-
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") :
|
|
1468
|
+
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
|
|
1150
1469
|
for (const model of models) options.push({
|
|
1151
1470
|
value: `${provider}/${model}`,
|
|
1152
1471
|
label: `${provider}/${model}`,
|
|
@@ -1184,39 +1503,91 @@ async function promptForModel() {
|
|
|
1184
1503
|
return selection;
|
|
1185
1504
|
}
|
|
1186
1505
|
/**
|
|
1506
|
+
* Build model options filtered to providers with web search support.
|
|
1507
|
+
*/
|
|
1508
|
+
function buildWebSearchModelOptions() {
|
|
1509
|
+
const options = [];
|
|
1510
|
+
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1511
|
+
if (!hasWebSearchSupport(provider)) continue;
|
|
1512
|
+
const info = getProviderInfo(provider);
|
|
1513
|
+
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
|
|
1514
|
+
for (const model of models) options.push({
|
|
1515
|
+
value: `${provider}/${model}`,
|
|
1516
|
+
label: `${provider}/${model}`,
|
|
1517
|
+
hint: `${keyStatus} ${info.envVar}`
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
options.push({
|
|
1521
|
+
value: "custom",
|
|
1522
|
+
label: "Enter custom model ID...",
|
|
1523
|
+
hint: "provider/model-id format"
|
|
1524
|
+
});
|
|
1525
|
+
return options;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Prompt user to select a model with web search capability for research workflow.
|
|
1529
|
+
*/
|
|
1530
|
+
async function promptForWebSearchModel() {
|
|
1531
|
+
const modelOptions = buildWebSearchModelOptions();
|
|
1532
|
+
if (modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
|
|
1533
|
+
const selection = await p.select({
|
|
1534
|
+
message: "Select LLM model (web search required):",
|
|
1535
|
+
options: modelOptions
|
|
1536
|
+
});
|
|
1537
|
+
if (p.isCancel(selection)) return null;
|
|
1538
|
+
if (selection === "custom") {
|
|
1539
|
+
const customModel = await p.text({
|
|
1540
|
+
message: "Model ID (provider/model-id):",
|
|
1541
|
+
placeholder: "openai/gpt-5-mini",
|
|
1542
|
+
validate: (value) => {
|
|
1543
|
+
if (!value.includes("/")) return "Format: provider/model-id (e.g., openai/gpt-5-mini)";
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
if (p.isCancel(customModel)) return null;
|
|
1547
|
+
return customModel;
|
|
1548
|
+
}
|
|
1549
|
+
return selection;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1187
1552
|
* Run the agent fill workflow.
|
|
1553
|
+
* Accepts optional harness config overrides - research uses different defaults.
|
|
1188
1554
|
*/
|
|
1189
|
-
async function runAgentFill(form, modelId, _outputPath) {
|
|
1555
|
+
async function runAgentFill(form, modelId, _outputPath, configOverrides) {
|
|
1190
1556
|
const spinner = p.spinner();
|
|
1191
1557
|
try {
|
|
1192
1558
|
spinner.start(`Resolving model: ${modelId}`);
|
|
1193
|
-
const { model } = await resolveModel(modelId);
|
|
1559
|
+
const { model, provider } = await resolveModel(modelId);
|
|
1194
1560
|
spinner.stop(`Model resolved: ${modelId}`);
|
|
1195
1561
|
const harnessConfig = {
|
|
1196
|
-
maxTurns: DEFAULT_MAX_TURNS,
|
|
1197
|
-
maxPatchesPerTurn: DEFAULT_MAX_PATCHES_PER_TURN,
|
|
1198
|
-
|
|
1562
|
+
maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
1563
|
+
maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
|
|
1564
|
+
maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
|
|
1199
1565
|
targetRoles: [AGENT_ROLE],
|
|
1200
1566
|
fillMode: "continue"
|
|
1201
1567
|
};
|
|
1568
|
+
console.log("");
|
|
1569
|
+
console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
|
|
1202
1570
|
const harness = createHarness(form, harnessConfig);
|
|
1203
1571
|
const agent = createLiveAgent({
|
|
1204
1572
|
model,
|
|
1573
|
+
provider,
|
|
1205
1574
|
targetRole: AGENT_ROLE
|
|
1206
1575
|
});
|
|
1207
|
-
console.log("");
|
|
1208
1576
|
p.log.step(pc.bold("Agent fill in progress..."));
|
|
1209
1577
|
let stepResult = harness.step();
|
|
1210
1578
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1211
|
-
console.log(pc.
|
|
1212
|
-
const { patches } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1579
|
+
console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1580
|
+
const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1213
1581
|
for (const patch of patches) {
|
|
1214
1582
|
const typeName = formatPatchType(patch);
|
|
1215
1583
|
const value = formatPatchValue(patch);
|
|
1216
|
-
|
|
1584
|
+
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
1585
|
+
if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
|
|
1586
|
+
else console.log(` (${typeName}) = ${pc.green(value)}`);
|
|
1217
1587
|
}
|
|
1218
1588
|
stepResult = harness.apply(patches, stepResult.issues);
|
|
1219
|
-
|
|
1589
|
+
const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
|
|
1590
|
+
console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
|
|
1220
1591
|
if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
|
|
1221
1592
|
}
|
|
1222
1593
|
if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
|
|
@@ -1233,18 +1604,24 @@ async function runAgentFill(form, modelId, _outputPath) {
|
|
|
1233
1604
|
}
|
|
1234
1605
|
/**
|
|
1235
1606
|
* Run the interactive example scaffolding and filling flow.
|
|
1607
|
+
*
|
|
1608
|
+
* @param preselectedId Optional example ID to pre-select
|
|
1609
|
+
* @param formsDirOverride Optional forms directory override from CLI option
|
|
1236
1610
|
*/
|
|
1237
|
-
async function runInteractiveFlow(preselectedId) {
|
|
1611
|
+
async function runInteractiveFlow(preselectedId, formsDirOverride) {
|
|
1238
1612
|
const startTime = Date.now();
|
|
1239
1613
|
p.intro(pc.bgCyan(pc.black(" markform examples ")));
|
|
1614
|
+
const formsDir = getFormsDir(formsDirOverride);
|
|
1615
|
+
await ensureFormsDir(formsDir);
|
|
1240
1616
|
showApiStatus();
|
|
1241
1617
|
let selectedId = preselectedId;
|
|
1242
1618
|
if (!selectedId) {
|
|
1619
|
+
const examples = getAllExamplesWithMetadata();
|
|
1243
1620
|
const selection = await p.select({
|
|
1244
1621
|
message: "Select an example form to scaffold:",
|
|
1245
|
-
options:
|
|
1622
|
+
options: examples.map((example$1) => ({
|
|
1246
1623
|
value: example$1.id,
|
|
1247
|
-
label: example$1.title,
|
|
1624
|
+
label: example$1.title ?? example$1.id,
|
|
1248
1625
|
hint: example$1.description
|
|
1249
1626
|
}))
|
|
1250
1627
|
});
|
|
@@ -1259,9 +1636,9 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1259
1636
|
p.cancel(`Unknown example: ${selectedId}`);
|
|
1260
1637
|
process.exit(1);
|
|
1261
1638
|
}
|
|
1262
|
-
const defaultFilename =
|
|
1639
|
+
const defaultFilename = basename(generateVersionedPathInFormsDir(example.filename, formsDir));
|
|
1263
1640
|
const filenameResult = await p.text({
|
|
1264
|
-
message:
|
|
1641
|
+
message: `Output filename (in ${formatPath(formsDir)}):`,
|
|
1265
1642
|
initialValue: defaultFilename,
|
|
1266
1643
|
validate: (value) => {
|
|
1267
1644
|
if (!value.trim()) return "Filename is required";
|
|
@@ -1273,7 +1650,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1273
1650
|
process.exit(0);
|
|
1274
1651
|
}
|
|
1275
1652
|
const filename = filenameResult;
|
|
1276
|
-
const outputPath = join(
|
|
1653
|
+
const outputPath = join(formsDir, filename);
|
|
1277
1654
|
if (existsSync(outputPath)) {
|
|
1278
1655
|
const overwrite = await p.confirm({
|
|
1279
1656
|
message: `${filename} already exists. Overwrite?`,
|
|
@@ -1287,7 +1664,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1287
1664
|
let content;
|
|
1288
1665
|
try {
|
|
1289
1666
|
content = loadExampleContent(selectedId);
|
|
1290
|
-
|
|
1667
|
+
await writeFile(outputPath, content);
|
|
1291
1668
|
} catch (error) {
|
|
1292
1669
|
const message = error instanceof Error ? error.message : String(error);
|
|
1293
1670
|
p.cancel(`Failed to write file: ${message}`);
|
|
@@ -1296,6 +1673,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1296
1673
|
p.log.success(`Created ${formatPath(outputPath)}`);
|
|
1297
1674
|
const form = parseForm(content);
|
|
1298
1675
|
const targetRoles = [USER_ROLE];
|
|
1676
|
+
let userFillOutputs = null;
|
|
1299
1677
|
const inspectResult = inspect(form, { targetRoles });
|
|
1300
1678
|
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
1301
1679
|
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
@@ -1308,7 +1686,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1308
1686
|
dryRun: false,
|
|
1309
1687
|
quiet: false
|
|
1310
1688
|
}, "Total time", Date.now() - startTime);
|
|
1311
|
-
p.outro(
|
|
1689
|
+
p.outro("Form scaffolded with no fields to fill.");
|
|
1312
1690
|
return;
|
|
1313
1691
|
}
|
|
1314
1692
|
} else {
|
|
@@ -1319,13 +1697,13 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1319
1697
|
process.exit(1);
|
|
1320
1698
|
}
|
|
1321
1699
|
if (patches.length > 0) applyPatches(form, patches);
|
|
1322
|
-
|
|
1700
|
+
userFillOutputs = await exportMultiFormat(form, outputPath);
|
|
1323
1701
|
showInteractiveOutro(patches.length, false);
|
|
1324
1702
|
console.log("");
|
|
1325
1703
|
p.log.success("Outputs:");
|
|
1326
|
-
console.log(` ${formatPath(
|
|
1327
|
-
console.log(` ${formatPath(
|
|
1328
|
-
console.log(` ${formatPath(
|
|
1704
|
+
console.log(` ${formatPath(userFillOutputs.reportPath)} ${pc.dim("(output report)")}`);
|
|
1705
|
+
console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(output values)")}`);
|
|
1706
|
+
console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(filled markform source)")}`);
|
|
1329
1707
|
logTiming({
|
|
1330
1708
|
verbose: false,
|
|
1331
1709
|
format: "console",
|
|
@@ -1334,29 +1712,50 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1334
1712
|
}, "Fill time", Date.now() - startTime);
|
|
1335
1713
|
}
|
|
1336
1714
|
const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
|
|
1715
|
+
const isResearchExample = example.type === "research";
|
|
1337
1716
|
if (agentFieldIssues.length > 0) {
|
|
1338
1717
|
console.log("");
|
|
1718
|
+
const workflowLabel = isResearchExample ? "research" : "agent fill";
|
|
1339
1719
|
p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
|
|
1720
|
+
const confirmMessage = isResearchExample ? "Run research now? (requires web search)" : "Run agent fill now?";
|
|
1340
1721
|
const runAgent = await p.confirm({
|
|
1341
|
-
message:
|
|
1722
|
+
message: confirmMessage,
|
|
1342
1723
|
initialValue: true
|
|
1343
1724
|
});
|
|
1344
1725
|
if (p.isCancel(runAgent) || !runAgent) {
|
|
1345
1726
|
console.log("");
|
|
1346
|
-
|
|
1347
|
-
console.log(
|
|
1348
|
-
|
|
1727
|
+
const cliCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=<provider/model>` : ` markform fill ${formatPath(outputPath)} --model=<provider/model>`;
|
|
1728
|
+
console.log(`You can run ${workflowLabel} later with:`);
|
|
1729
|
+
console.log(cliCommand);
|
|
1730
|
+
if (userFillOutputs) await showFileViewerChooser([
|
|
1731
|
+
{
|
|
1732
|
+
path: userFillOutputs.reportPath,
|
|
1733
|
+
label: "Report",
|
|
1734
|
+
hint: "output report"
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
path: userFillOutputs.yamlPath,
|
|
1738
|
+
label: "Values",
|
|
1739
|
+
hint: "output values"
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
path: userFillOutputs.formPath,
|
|
1743
|
+
label: "Form",
|
|
1744
|
+
hint: "filled markform source"
|
|
1745
|
+
}
|
|
1746
|
+
]);
|
|
1747
|
+
p.outro("Happy form filling!");
|
|
1349
1748
|
return;
|
|
1350
1749
|
}
|
|
1351
|
-
const modelId = await promptForModel();
|
|
1750
|
+
const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
|
|
1352
1751
|
if (!modelId) {
|
|
1353
1752
|
p.cancel("Cancelled.");
|
|
1354
1753
|
process.exit(0);
|
|
1355
1754
|
}
|
|
1356
|
-
const agentDefaultFilename =
|
|
1755
|
+
const agentDefaultFilename = basename(generateVersionedPathInFormsDir(outputPath, formsDir));
|
|
1357
1756
|
const agentFilenameResult = await p.text({
|
|
1358
|
-
message:
|
|
1359
|
-
initialValue:
|
|
1757
|
+
message: `Agent output filename (in ${formatPath(formsDir)}):`,
|
|
1758
|
+
initialValue: agentDefaultFilename,
|
|
1360
1759
|
validate: (value) => {
|
|
1361
1760
|
if (!value.trim()) return "Filename is required";
|
|
1362
1761
|
}
|
|
@@ -1365,39 +1764,64 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1365
1764
|
p.cancel("Cancelled.");
|
|
1366
1765
|
process.exit(0);
|
|
1367
1766
|
}
|
|
1368
|
-
const agentOutputPath = join(
|
|
1767
|
+
const agentOutputPath = join(formsDir, agentFilenameResult);
|
|
1369
1768
|
const agentStartTime = Date.now();
|
|
1769
|
+
const timingLabel = isResearchExample ? "Research time" : "Agent fill time";
|
|
1770
|
+
const configOverrides = isResearchExample ? {
|
|
1771
|
+
maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
|
|
1772
|
+
maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
|
|
1773
|
+
} : void 0;
|
|
1370
1774
|
try {
|
|
1371
|
-
const { success
|
|
1775
|
+
const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
|
|
1372
1776
|
logTiming({
|
|
1373
1777
|
verbose: false,
|
|
1374
1778
|
format: "console",
|
|
1375
1779
|
dryRun: false,
|
|
1376
1780
|
quiet: false
|
|
1377
|
-
},
|
|
1378
|
-
const {
|
|
1781
|
+
}, timingLabel, Date.now() - agentStartTime);
|
|
1782
|
+
const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, agentOutputPath);
|
|
1379
1783
|
console.log("");
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
console.log(` ${formatPath(
|
|
1383
|
-
console.log(` ${formatPath(yamlPath)} ${pc.dim("(values
|
|
1784
|
+
const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
|
|
1785
|
+
p.log.success(successMessage);
|
|
1786
|
+
console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
|
|
1787
|
+
console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
|
|
1788
|
+
console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
|
|
1384
1789
|
if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
|
|
1790
|
+
await showFileViewerChooser([
|
|
1791
|
+
{
|
|
1792
|
+
path: reportPath,
|
|
1793
|
+
label: "Report",
|
|
1794
|
+
hint: "output report"
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
path: yamlPath,
|
|
1798
|
+
label: "Values",
|
|
1799
|
+
hint: "output values"
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
path: formPath,
|
|
1803
|
+
label: "Form",
|
|
1804
|
+
hint: "filled markform source"
|
|
1805
|
+
}
|
|
1806
|
+
]);
|
|
1385
1807
|
} catch (error) {
|
|
1386
1808
|
const message = error instanceof Error ? error.message : String(error);
|
|
1387
|
-
|
|
1809
|
+
const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
|
|
1810
|
+
p.log.error(`${failMessage}: ${message}`);
|
|
1388
1811
|
console.log("");
|
|
1389
|
-
console.log(
|
|
1390
|
-
|
|
1812
|
+
console.log("You can try again with:");
|
|
1813
|
+
const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
|
|
1814
|
+
console.log(retryCommand);
|
|
1391
1815
|
}
|
|
1392
1816
|
}
|
|
1393
|
-
p.outro(
|
|
1817
|
+
p.outro("Happy form filling!");
|
|
1394
1818
|
}
|
|
1395
1819
|
/**
|
|
1396
1820
|
* Register the examples command.
|
|
1397
1821
|
*/
|
|
1398
1822
|
function registerExamplesCommand(program) {
|
|
1399
|
-
program.command("examples").description("
|
|
1400
|
-
getCommandContext(cmd);
|
|
1823
|
+
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) => {
|
|
1824
|
+
const ctx = getCommandContext(cmd);
|
|
1401
1825
|
try {
|
|
1402
1826
|
if (options.list) {
|
|
1403
1827
|
printExamplesList();
|
|
@@ -1411,7 +1835,7 @@ function registerExamplesCommand(program) {
|
|
|
1411
1835
|
process.exit(1);
|
|
1412
1836
|
}
|
|
1413
1837
|
}
|
|
1414
|
-
await runInteractiveFlow(options.name);
|
|
1838
|
+
await runInteractiveFlow(options.name, ctx.formsDir);
|
|
1415
1839
|
} catch (error) {
|
|
1416
1840
|
logError(error instanceof Error ? error.message : String(error));
|
|
1417
1841
|
process.exit(1);
|
|
@@ -1434,7 +1858,7 @@ function registerExportCommand(program) {
|
|
|
1434
1858
|
else if (ctx.format === "markform") format = "markform";
|
|
1435
1859
|
try {
|
|
1436
1860
|
logVerbose(ctx, `Reading file: ${file}`);
|
|
1437
|
-
const content = await readFile(file);
|
|
1861
|
+
const content = await readFile$1(file);
|
|
1438
1862
|
logVerbose(ctx, "Parsing form...");
|
|
1439
1863
|
const form = parseForm(content);
|
|
1440
1864
|
if (format === "markform") {
|
|
@@ -1445,26 +1869,30 @@ function registerExportCommand(program) {
|
|
|
1445
1869
|
console.log(serializeRawMarkdown(form));
|
|
1446
1870
|
return;
|
|
1447
1871
|
}
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
})) } : {}
|
|
1464
|
-
}))
|
|
1872
|
+
const schema = {
|
|
1873
|
+
id: form.schema.id,
|
|
1874
|
+
title: form.schema.title,
|
|
1875
|
+
groups: form.schema.groups.map((group) => ({
|
|
1876
|
+
id: group.id,
|
|
1877
|
+
title: group.title,
|
|
1878
|
+
children: group.children.map((field) => ({
|
|
1879
|
+
id: field.id,
|
|
1880
|
+
kind: field.kind,
|
|
1881
|
+
label: field.label,
|
|
1882
|
+
required: field.required,
|
|
1883
|
+
...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
|
|
1884
|
+
id: opt.id,
|
|
1885
|
+
label: opt.label
|
|
1886
|
+
})) } : {}
|
|
1465
1887
|
}))
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1888
|
+
}))
|
|
1889
|
+
};
|
|
1890
|
+
const values = {};
|
|
1891
|
+
for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
|
|
1892
|
+
const output = {
|
|
1893
|
+
schema,
|
|
1894
|
+
values,
|
|
1895
|
+
notes: form.notes,
|
|
1468
1896
|
markdown: serialize(form)
|
|
1469
1897
|
};
|
|
1470
1898
|
if (format === "json") if (options.compact) console.log(JSON.stringify(output));
|
|
@@ -1498,7 +1926,7 @@ function formatConsoleSession(transcript, useColors) {
|
|
|
1498
1926
|
lines.push(bold("Harness Config:"));
|
|
1499
1927
|
lines.push(` Max turns: ${transcript.harness.maxTurns}`);
|
|
1500
1928
|
lines.push(` Max patches/turn: ${transcript.harness.maxPatchesPerTurn}`);
|
|
1501
|
-
lines.push(` Max issues: ${transcript.harness.
|
|
1929
|
+
lines.push(` Max issues/turn: ${transcript.harness.maxIssuesPerTurn}`);
|
|
1502
1930
|
lines.push("");
|
|
1503
1931
|
lines.push(bold(`Turns (${transcript.turns.length}):`));
|
|
1504
1932
|
for (const turn of transcript.turns) {
|
|
@@ -1517,7 +1945,7 @@ function formatConsoleSession(transcript, useColors) {
|
|
|
1517
1945
|
* Register the fill command.
|
|
1518
1946
|
*/
|
|
1519
1947
|
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-
|
|
1948
|
+
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
1949
|
const ctx = getCommandContext(cmd);
|
|
1522
1950
|
const filePath = resolve(file);
|
|
1523
1951
|
try {
|
|
@@ -1539,7 +1967,7 @@ function registerFillCommand(program) {
|
|
|
1539
1967
|
fillMode = options.mode;
|
|
1540
1968
|
}
|
|
1541
1969
|
logVerbose(ctx, `Reading form: ${filePath}`);
|
|
1542
|
-
const formContent = await readFile(filePath);
|
|
1970
|
+
const formContent = await readFile$1(filePath);
|
|
1543
1971
|
logVerbose(ctx, "Parsing form...");
|
|
1544
1972
|
const form = parseForm(formContent);
|
|
1545
1973
|
if (options.interactive) {
|
|
@@ -1567,24 +1995,30 @@ function registerFillCommand(program) {
|
|
|
1567
1995
|
}
|
|
1568
1996
|
if (patches.length > 0) applyPatches(form, patches);
|
|
1569
1997
|
const durationMs$1 = Date.now() - startTime;
|
|
1570
|
-
|
|
1998
|
+
let outputPath$1;
|
|
1999
|
+
if (options.output) outputPath$1 = resolve(options.output);
|
|
2000
|
+
else {
|
|
2001
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2002
|
+
await ensureFormsDir(formsDir);
|
|
2003
|
+
outputPath$1 = generateVersionedPathInFormsDir(filePath, formsDir);
|
|
2004
|
+
}
|
|
1571
2005
|
if (ctx.dryRun) {
|
|
1572
2006
|
logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
|
|
1573
2007
|
showInteractiveOutro(patches.length, false);
|
|
1574
2008
|
} else {
|
|
1575
|
-
const {
|
|
2009
|
+
const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, outputPath$1);
|
|
1576
2010
|
showInteractiveOutro(patches.length, false);
|
|
1577
2011
|
console.log("");
|
|
1578
2012
|
p.log.success("Outputs:");
|
|
1579
|
-
console.log(` ${formatPath(
|
|
1580
|
-
console.log(` ${formatPath(
|
|
1581
|
-
console.log(` ${formatPath(
|
|
2013
|
+
console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
|
|
2014
|
+
console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
|
|
2015
|
+
console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
|
|
1582
2016
|
}
|
|
1583
2017
|
logTiming(ctx, "Fill time", durationMs$1);
|
|
1584
2018
|
if (patches.length > 0) {
|
|
1585
2019
|
console.log("");
|
|
1586
|
-
console.log(
|
|
1587
|
-
console.log(
|
|
2020
|
+
console.log("Next step: fill remaining fields with agent");
|
|
2021
|
+
console.log(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`);
|
|
1588
2022
|
}
|
|
1589
2023
|
process.exit(0);
|
|
1590
2024
|
}
|
|
@@ -1599,22 +2033,22 @@ function registerFillCommand(program) {
|
|
|
1599
2033
|
process.exit(1);
|
|
1600
2034
|
}
|
|
1601
2035
|
if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
|
|
1602
|
-
const harnessConfig = {
|
|
1603
|
-
maxTurns:
|
|
1604
|
-
maxPatchesPerTurn:
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2036
|
+
const harnessConfig = resolveHarnessConfig(form, {
|
|
2037
|
+
maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
|
|
2038
|
+
maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
|
|
2039
|
+
maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
|
|
2040
|
+
maxFieldsPerTurn: options.maxFields ? parseInt(options.maxFields, 10) : void 0,
|
|
2041
|
+
maxGroupsPerTurn: options.maxGroups ? parseInt(options.maxGroups, 10) : void 0,
|
|
1608
2042
|
targetRoles,
|
|
1609
2043
|
fillMode
|
|
1610
|
-
};
|
|
2044
|
+
});
|
|
1611
2045
|
const harness = createHarness(form, harnessConfig);
|
|
1612
2046
|
let agent;
|
|
1613
2047
|
let mockPath;
|
|
1614
2048
|
if (options.mock) {
|
|
1615
2049
|
mockPath = resolve(options.mockSource);
|
|
1616
2050
|
logVerbose(ctx, `Reading mock source: ${mockPath}`);
|
|
1617
|
-
agent = createMockAgent(parseForm(await readFile(mockPath)));
|
|
2051
|
+
agent = createMockAgent(parseForm(await readFile$1(mockPath)));
|
|
1618
2052
|
} else {
|
|
1619
2053
|
const modelId = options.model;
|
|
1620
2054
|
logVerbose(ctx, `Resolving model: ${modelId}`);
|
|
@@ -1626,7 +2060,7 @@ function registerFillCommand(program) {
|
|
|
1626
2060
|
} else if (options.prompt) {
|
|
1627
2061
|
const promptPath = resolve(options.prompt);
|
|
1628
2062
|
logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
|
|
1629
|
-
systemPrompt = await readFile(promptPath);
|
|
2063
|
+
systemPrompt = await readFile$1(promptPath);
|
|
1630
2064
|
}
|
|
1631
2065
|
const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
|
|
1632
2066
|
const liveAgent = createLiveAgent({
|
|
@@ -1643,21 +2077,24 @@ function registerFillCommand(program) {
|
|
|
1643
2077
|
logInfo(ctx, `Agent: ${options.mock ? "mock" : "live"}${options.model ? ` (${options.model})` : ""}`);
|
|
1644
2078
|
logVerbose(ctx, `Max turns: ${harnessConfig.maxTurns}`);
|
|
1645
2079
|
logVerbose(ctx, `Max patches per turn: ${harnessConfig.maxPatchesPerTurn}`);
|
|
1646
|
-
logVerbose(ctx, `Max issues per
|
|
2080
|
+
logVerbose(ctx, `Max issues per turn: ${harnessConfig.maxIssuesPerTurn}`);
|
|
1647
2081
|
logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
|
|
1648
2082
|
logVerbose(ctx, `Fill mode: ${fillMode}`);
|
|
1649
2083
|
let stepResult = harness.step();
|
|
1650
|
-
logInfo(ctx, `Turn ${
|
|
2084
|
+
logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1651
2085
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1652
2086
|
const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1653
|
-
|
|
2087
|
+
const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
|
|
2088
|
+
logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
|
|
1654
2089
|
for (const patch of patches) {
|
|
1655
2090
|
const typeName = formatPatchType(patch);
|
|
1656
2091
|
const value = formatPatchValue(patch);
|
|
1657
|
-
|
|
2092
|
+
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
2093
|
+
if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
2094
|
+
else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
1658
2095
|
}
|
|
1659
2096
|
if (stats) {
|
|
1660
|
-
logVerbose(ctx, ` Stats:
|
|
2097
|
+
logVerbose(ctx, ` Stats: tokens ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0}`);
|
|
1661
2098
|
if (stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
|
|
1662
2099
|
if (stats.prompts) {
|
|
1663
2100
|
logVerbose(ctx, ``);
|
|
@@ -1679,14 +2116,20 @@ function registerFillCommand(program) {
|
|
|
1679
2116
|
if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
1680
2117
|
else if (!harness.hasReachedMaxTurns()) {
|
|
1681
2118
|
stepResult = harness.step();
|
|
1682
|
-
logInfo(ctx, `Turn ${
|
|
2119
|
+
logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1683
2120
|
}
|
|
1684
2121
|
}
|
|
1685
2122
|
const durationMs = Date.now() - startTime;
|
|
1686
2123
|
if (stepResult.isComplete) logSuccess(ctx, `Form completed in ${harness.getTurnNumber()} turn(s)`);
|
|
1687
2124
|
else if (harness.hasReachedMaxTurns()) logWarn(ctx, `Max turns reached (${harnessConfig.maxTurns})`);
|
|
1688
2125
|
logTiming(ctx, "Fill time", durationMs);
|
|
1689
|
-
|
|
2126
|
+
let outputPath;
|
|
2127
|
+
if (options.output) outputPath = resolve(options.output);
|
|
2128
|
+
else {
|
|
2129
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2130
|
+
await ensureFormsDir(formsDir);
|
|
2131
|
+
outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
|
|
2132
|
+
}
|
|
1690
2133
|
const formMarkdown = serialize(harness.getForm());
|
|
1691
2134
|
if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
|
|
1692
2135
|
else {
|
|
@@ -1750,6 +2193,18 @@ function formatState$1(state, useColors) {
|
|
|
1750
2193
|
return useColors ? colorFn(text) : text;
|
|
1751
2194
|
}
|
|
1752
2195
|
/**
|
|
2196
|
+
* Format answer state badge for console output.
|
|
2197
|
+
*/
|
|
2198
|
+
function formatAnswerState(state, useColors) {
|
|
2199
|
+
const [text, colorFn] = {
|
|
2200
|
+
answered: ["answered", pc.green],
|
|
2201
|
+
skipped: ["skipped", pc.yellow],
|
|
2202
|
+
aborted: ["aborted", pc.red],
|
|
2203
|
+
unanswered: ["unanswered", pc.dim]
|
|
2204
|
+
}[state] ?? [state, (s) => s];
|
|
2205
|
+
return useColors ? colorFn(text) : text;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
1753
2208
|
* Format priority badge for console output.
|
|
1754
2209
|
*
|
|
1755
2210
|
* Priority tiers and colors:
|
|
@@ -1796,6 +2251,8 @@ function formatFieldValue(value, useColors) {
|
|
|
1796
2251
|
if (entries.length === 0) return dim("(no entries)");
|
|
1797
2252
|
return entries.map(([k, v]) => `${k}:${v}`).join(", ");
|
|
1798
2253
|
}
|
|
2254
|
+
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
2255
|
+
case "url_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
|
|
1799
2256
|
default: return dim("(unknown)");
|
|
1800
2257
|
}
|
|
1801
2258
|
}
|
|
@@ -1823,13 +2280,11 @@ function formatConsoleReport$1(report, useColors) {
|
|
|
1823
2280
|
lines.push(bold("Progress:"));
|
|
1824
2281
|
lines.push(` Total fields: ${progress.counts.totalFields}`);
|
|
1825
2282
|
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}`);
|
|
2283
|
+
lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
|
|
2284
|
+
lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
|
|
2285
|
+
lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
|
|
2286
|
+
lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
|
|
2287
|
+
lines.push(` Total notes: ${progress.counts.totalNotes}`);
|
|
1833
2288
|
lines.push("");
|
|
1834
2289
|
lines.push(bold("Form Content:"));
|
|
1835
2290
|
for (const group of report.groups) {
|
|
@@ -1837,13 +2292,25 @@ function formatConsoleReport$1(report, useColors) {
|
|
|
1837
2292
|
for (const field of group.children) {
|
|
1838
2293
|
const reqBadge = field.required ? yellow("[required]") : dim("[optional]");
|
|
1839
2294
|
const roleBadge = field.role !== "agent" ? cyan(`[${field.role}]`) : "";
|
|
2295
|
+
const fieldProgress = progress.fields[field.id];
|
|
2296
|
+
const responseStateBadge = fieldProgress ? `[${formatAnswerState(fieldProgress.answerState, useColors)}]` : "";
|
|
2297
|
+
const notesBadge = fieldProgress?.hasNotes ? cyan(`[${fieldProgress.noteCount} note${fieldProgress.noteCount > 1 ? "s" : ""}]`) : "";
|
|
1840
2298
|
const value = report.values[field.id];
|
|
1841
2299
|
const valueStr = formatFieldValue(value, useColors);
|
|
1842
|
-
lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge}`.trim());
|
|
2300
|
+
lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge} ${responseStateBadge} ${notesBadge}`.trim());
|
|
1843
2301
|
lines.push(` ${dim("→")} ${valueStr}`);
|
|
1844
2302
|
}
|
|
1845
2303
|
}
|
|
1846
2304
|
lines.push("");
|
|
2305
|
+
if (report.notes.length > 0) {
|
|
2306
|
+
lines.push(bold(`Notes (${report.notes.length}):`));
|
|
2307
|
+
for (const note of report.notes) {
|
|
2308
|
+
const roleBadge = cyan(`[${note.role}]`);
|
|
2309
|
+
const refLabel = dim(`${note.ref}:`);
|
|
2310
|
+
lines.push(` ${note.id} ${roleBadge} ${refLabel} ${note.text}`.trim());
|
|
2311
|
+
}
|
|
2312
|
+
lines.push("");
|
|
2313
|
+
}
|
|
1847
2314
|
if (report.issues.length > 0) {
|
|
1848
2315
|
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
1849
2316
|
for (const issue of report.issues) {
|
|
@@ -1871,11 +2338,13 @@ function registerInspectCommand(program) {
|
|
|
1871
2338
|
process.exit(1);
|
|
1872
2339
|
}
|
|
1873
2340
|
logVerbose(ctx, `Reading file: ${file}`);
|
|
1874
|
-
const content = await readFile(file);
|
|
2341
|
+
const content = await readFile$1(file);
|
|
1875
2342
|
logVerbose(ctx, "Parsing form...");
|
|
1876
2343
|
const form = parseForm(content);
|
|
1877
2344
|
logVerbose(ctx, "Running inspection...");
|
|
1878
2345
|
const result = inspect(form, { targetRoles });
|
|
2346
|
+
const values = {};
|
|
2347
|
+
for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
|
|
1879
2348
|
const output = formatOutput(ctx, {
|
|
1880
2349
|
title: form.schema.title,
|
|
1881
2350
|
structure: result.structureSummary,
|
|
@@ -1892,7 +2361,8 @@ function registerInspectCommand(program) {
|
|
|
1892
2361
|
role: field.role
|
|
1893
2362
|
}))
|
|
1894
2363
|
})),
|
|
1895
|
-
values
|
|
2364
|
+
values,
|
|
2365
|
+
notes: form.notes,
|
|
1896
2366
|
issues: result.issues.map((issue) => ({
|
|
1897
2367
|
ref: issue.ref,
|
|
1898
2368
|
scope: issue.scope,
|
|
@@ -1912,7 +2382,7 @@ function registerInspectCommand(program) {
|
|
|
1912
2382
|
}
|
|
1913
2383
|
|
|
1914
2384
|
//#endregion
|
|
1915
|
-
//#region src/cli/commands/
|
|
2385
|
+
//#region src/cli/commands/readme.ts
|
|
1916
2386
|
/**
|
|
1917
2387
|
* Get the path to the README.md file.
|
|
1918
2388
|
* Works both during development and when installed as a package.
|
|
@@ -1938,6 +2408,128 @@ function loadReadme() {
|
|
|
1938
2408
|
* Apply basic terminal formatting to markdown content.
|
|
1939
2409
|
* Colorizes headers, code blocks, and other elements for better readability.
|
|
1940
2410
|
*/
|
|
2411
|
+
function formatMarkdown$1(content, useColors) {
|
|
2412
|
+
if (!useColors) return content;
|
|
2413
|
+
const lines = content.split("\n");
|
|
2414
|
+
const formatted = [];
|
|
2415
|
+
let inCodeBlock = false;
|
|
2416
|
+
for (const line of lines) {
|
|
2417
|
+
if (line.startsWith("```")) {
|
|
2418
|
+
inCodeBlock = !inCodeBlock;
|
|
2419
|
+
formatted.push(pc.dim(line));
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
if (inCodeBlock) {
|
|
2423
|
+
formatted.push(pc.dim(line));
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
if (line.startsWith("# ")) {
|
|
2427
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (line.startsWith("## ")) {
|
|
2431
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (line.startsWith("### ")) {
|
|
2435
|
+
formatted.push(pc.bold(line));
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
2439
|
+
return pc.yellow(code);
|
|
2440
|
+
});
|
|
2441
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
2442
|
+
return pc.bold(text);
|
|
2443
|
+
});
|
|
2444
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
2445
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
2446
|
+
});
|
|
2447
|
+
formatted.push(formattedLine);
|
|
2448
|
+
}
|
|
2449
|
+
return formatted.join("\n");
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Check if stdout is an interactive terminal.
|
|
2453
|
+
*/
|
|
2454
|
+
function isInteractive$1() {
|
|
2455
|
+
return process.stdout.isTTY === true;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Display content. In a future enhancement, could pipe to a pager for long output.
|
|
2459
|
+
*/
|
|
2460
|
+
function displayContent$1(content) {
|
|
2461
|
+
console.log(content);
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Register the readme command.
|
|
2465
|
+
*/
|
|
2466
|
+
function registerReadmeCommand(program) {
|
|
2467
|
+
program.command("readme").description("✨Display README documentation ← START HERE!").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
2468
|
+
const ctx = getCommandContext(cmd);
|
|
2469
|
+
try {
|
|
2470
|
+
displayContent$1(formatMarkdown$1(loadReadme(), !options.raw && isInteractive$1() && !ctx.quiet));
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2473
|
+
process.exit(1);
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
//#endregion
|
|
2479
|
+
//#region src/cli/commands/report.ts
|
|
2480
|
+
/**
|
|
2481
|
+
* Register the report command.
|
|
2482
|
+
*/
|
|
2483
|
+
function registerReportCommand(program) {
|
|
2484
|
+
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) => {
|
|
2485
|
+
const ctx = getCommandContext(cmd);
|
|
2486
|
+
try {
|
|
2487
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
2488
|
+
const content = await readFile$1(file);
|
|
2489
|
+
logVerbose(ctx, "Parsing form...");
|
|
2490
|
+
const form = parseForm(content);
|
|
2491
|
+
logVerbose(ctx, "Generating report...");
|
|
2492
|
+
const reportContent = serializeReportMarkdown(form);
|
|
2493
|
+
if (options.output) {
|
|
2494
|
+
let outputPath = options.output;
|
|
2495
|
+
if (!outputPath.endsWith(REPORT_EXTENSION) && !outputPath.endsWith(".md")) outputPath = outputPath + REPORT_EXTENSION;
|
|
2496
|
+
await writeFile(outputPath, reportContent);
|
|
2497
|
+
logVerbose(ctx, `Report written to: ${outputPath}`);
|
|
2498
|
+
} else console.log(reportContent);
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2501
|
+
process.exit(1);
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
//#endregion
|
|
2507
|
+
//#region src/cli/commands/spec.ts
|
|
2508
|
+
/**
|
|
2509
|
+
* Get the path to the SPEC.md file.
|
|
2510
|
+
* Works both during development and when installed as a package.
|
|
2511
|
+
*/
|
|
2512
|
+
function getSpecPath() {
|
|
2513
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
2514
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "SPEC.md");
|
|
2515
|
+
return join(dirname(dirname(dirname(thisDir))), "SPEC.md");
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Load the spec content.
|
|
2519
|
+
*/
|
|
2520
|
+
function loadSpec() {
|
|
2521
|
+
const specPath = getSpecPath();
|
|
2522
|
+
try {
|
|
2523
|
+
return readFileSync(specPath, "utf-8");
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2526
|
+
throw new Error(`Failed to load SPEC from ${specPath}: ${message}`);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Apply basic terminal formatting to markdown content.
|
|
2531
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
2532
|
+
*/
|
|
1941
2533
|
function formatMarkdown(content, useColors) {
|
|
1942
2534
|
if (!useColors) return content;
|
|
1943
2535
|
const lines = content.split("\n");
|
|
@@ -1991,13 +2583,13 @@ function displayContent(content) {
|
|
|
1991
2583
|
console.log(content);
|
|
1992
2584
|
}
|
|
1993
2585
|
/**
|
|
1994
|
-
* Register the
|
|
2586
|
+
* Register the spec command.
|
|
1995
2587
|
*/
|
|
1996
|
-
function
|
|
1997
|
-
program.command("
|
|
2588
|
+
function registerSpecCommand(program) {
|
|
2589
|
+
program.command("spec").description("Display the Markform specification").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
1998
2590
|
const ctx = getCommandContext(cmd);
|
|
1999
2591
|
try {
|
|
2000
|
-
displayContent(formatMarkdown(
|
|
2592
|
+
displayContent(formatMarkdown(loadSpec(), !options.raw && isInteractive() && !ctx.quiet));
|
|
2001
2593
|
} catch (error) {
|
|
2002
2594
|
logError(error instanceof Error ? error.message : String(error));
|
|
2003
2595
|
process.exit(1);
|
|
@@ -2077,15 +2669,18 @@ function openBrowser(url) {
|
|
|
2077
2669
|
* Register the serve command.
|
|
2078
2670
|
*/
|
|
2079
2671
|
function registerServeCommand(program) {
|
|
2080
|
-
program.command("serve <file>").description("Serve a
|
|
2672
|
+
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
2673
|
const ctx = getCommandContext(cmd);
|
|
2082
2674
|
const port = parseInt(options.port ?? String(DEFAULT_PORT), 10);
|
|
2083
2675
|
const filePath = resolve(file);
|
|
2676
|
+
const fileType = detectFileType(filePath);
|
|
2084
2677
|
try {
|
|
2085
2678
|
logVerbose(ctx, `Reading file: ${filePath}`);
|
|
2086
|
-
|
|
2679
|
+
const content = await readFile$1(filePath);
|
|
2680
|
+
let form = null;
|
|
2681
|
+
if (fileType === "form") form = parseForm(content);
|
|
2087
2682
|
const server = createServer((req, res) => {
|
|
2088
|
-
handleRequest(req, res,
|
|
2683
|
+
handleRequest(req, res, filePath, fileType, form, ctx, (updatedForm) => {
|
|
2089
2684
|
form = updatedForm;
|
|
2090
2685
|
}).catch((err) => {
|
|
2091
2686
|
console.error("Request error:", err);
|
|
@@ -2095,7 +2690,8 @@ function registerServeCommand(program) {
|
|
|
2095
2690
|
});
|
|
2096
2691
|
server.listen(port, () => {
|
|
2097
2692
|
const url = `http://localhost:${port}`;
|
|
2098
|
-
|
|
2693
|
+
const typeLabel = fileType === "form" ? "Form" : fileType === "unknown" ? "File" : fileType.toUpperCase();
|
|
2694
|
+
logInfo(ctx, pc.green(`\n✓ ${typeLabel} server running at ${pc.bold(url)}\n`));
|
|
2099
2695
|
logInfo(ctx, pc.dim("Press Ctrl+C to stop\n"));
|
|
2100
2696
|
if (options.open !== false) openBrowser(url);
|
|
2101
2697
|
});
|
|
@@ -2112,15 +2708,33 @@ function registerServeCommand(program) {
|
|
|
2112
2708
|
}
|
|
2113
2709
|
/**
|
|
2114
2710
|
* Handle HTTP requests.
|
|
2711
|
+
* Dispatches to appropriate renderer based on file type.
|
|
2115
2712
|
*/
|
|
2116
|
-
async function handleRequest(req, res,
|
|
2713
|
+
async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm) {
|
|
2117
2714
|
const url = req.url ?? "/";
|
|
2118
|
-
if (req.method === "GET" && url === "/") {
|
|
2715
|
+
if (req.method === "GET" && url === "/") if (fileType === "form" && form) {
|
|
2119
2716
|
const html = renderFormHtml(form);
|
|
2120
2717
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2121
2718
|
res.end(html);
|
|
2122
|
-
} else if (
|
|
2123
|
-
|
|
2719
|
+
} else if (fileType === "raw" || fileType === "report") {
|
|
2720
|
+
const html = renderMarkdownHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2721
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2722
|
+
res.end(html);
|
|
2723
|
+
} else if (fileType === "yaml") {
|
|
2724
|
+
const html = renderYamlHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2725
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2726
|
+
res.end(html);
|
|
2727
|
+
} else if (fileType === "json") {
|
|
2728
|
+
const html = renderJsonHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2729
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2730
|
+
res.end(html);
|
|
2731
|
+
} else {
|
|
2732
|
+
const html = renderPlainTextHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
2733
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2734
|
+
res.end(html);
|
|
2735
|
+
}
|
|
2736
|
+
else if (req.method === "POST" && url === "/save" && fileType === "form" && form) await handleSave(req, res, form, filePath, ctx, updateForm);
|
|
2737
|
+
else if (url === "/api/form" && form) {
|
|
2124
2738
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2125
2739
|
res.end(JSON.stringify({ schema: form.schema }));
|
|
2126
2740
|
} else {
|
|
@@ -2153,7 +2767,8 @@ function formDataToPatches(formData, form) {
|
|
|
2153
2767
|
if (formData[`__skip__${fieldId}`] === "1" && !field.required) {
|
|
2154
2768
|
patches.push({
|
|
2155
2769
|
op: "skip_field",
|
|
2156
|
-
fieldId
|
|
2770
|
+
fieldId,
|
|
2771
|
+
role: "user"
|
|
2157
2772
|
});
|
|
2158
2773
|
continue;
|
|
2159
2774
|
}
|
|
@@ -2334,9 +2949,9 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
|
|
|
2334
2949
|
* @public Exported for testing.
|
|
2335
2950
|
*/
|
|
2336
2951
|
function renderFormHtml(form) {
|
|
2337
|
-
const { schema,
|
|
2952
|
+
const { schema, responsesByFieldId } = form;
|
|
2338
2953
|
const formTitle = schema.title ?? schema.id;
|
|
2339
|
-
const groupsHtml = schema.groups.map((group) => renderGroup(group,
|
|
2954
|
+
const groupsHtml = schema.groups.map((group) => renderGroup(group, responsesByFieldId)).join("\n");
|
|
2340
2955
|
return `<!DOCTYPE html>
|
|
2341
2956
|
<html lang="en">
|
|
2342
2957
|
<head>
|
|
@@ -2583,9 +3198,12 @@ function renderFormHtml(form) {
|
|
|
2583
3198
|
/**
|
|
2584
3199
|
* Render a field group as HTML.
|
|
2585
3200
|
*/
|
|
2586
|
-
function renderGroup(group,
|
|
3201
|
+
function renderGroup(group, responses) {
|
|
2587
3202
|
const groupTitle = group.title ?? group.id;
|
|
2588
|
-
const fieldsHtml = group.children.map((field) =>
|
|
3203
|
+
const fieldsHtml = group.children.map((field) => {
|
|
3204
|
+
const response = responses[field.id];
|
|
3205
|
+
return renderFieldHtml(field, response?.state === "answered" ? response.value : void 0, response?.state === "skipped");
|
|
3206
|
+
}).join("\n");
|
|
2589
3207
|
return `
|
|
2590
3208
|
<div class="group">
|
|
2591
3209
|
<h2>${escapeHtml(groupTitle)}</h2>
|
|
@@ -2596,13 +3214,13 @@ function renderGroup(group, values, skips) {
|
|
|
2596
3214
|
* Render a field as HTML.
|
|
2597
3215
|
* @public Exported for testing.
|
|
2598
3216
|
*/
|
|
2599
|
-
function renderFieldHtml(field, value,
|
|
2600
|
-
const
|
|
3217
|
+
function renderFieldHtml(field, value, isSkipped) {
|
|
3218
|
+
const skipped = isSkipped === true;
|
|
2601
3219
|
const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
|
|
2602
3220
|
const typeLabel = `<span class="type-badge">${field.kind}</span>`;
|
|
2603
|
-
const skippedBadge =
|
|
2604
|
-
const fieldClass =
|
|
2605
|
-
const disabledAttr =
|
|
3221
|
+
const skippedBadge = skipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
|
|
3222
|
+
const fieldClass = skipped ? "field field-skipped" : "field";
|
|
3223
|
+
const disabledAttr = skipped ? " disabled" : "";
|
|
2606
3224
|
let inputHtml;
|
|
2607
3225
|
switch (field.kind) {
|
|
2608
3226
|
case "string":
|
|
@@ -2631,7 +3249,7 @@ function renderFieldHtml(field, value, skipInfo) {
|
|
|
2631
3249
|
break;
|
|
2632
3250
|
default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
|
|
2633
3251
|
}
|
|
2634
|
-
const skipButton = !field.required && !
|
|
3252
|
+
const skipButton = !field.required && !skipped ? `<div class="field-actions">
|
|
2635
3253
|
<button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
|
|
2636
3254
|
</div>` : "";
|
|
2637
3255
|
return `
|
|
@@ -2775,6 +3393,182 @@ function renderCheckboxesInput(field, value, disabledAttr) {
|
|
|
2775
3393
|
function escapeHtml(str) {
|
|
2776
3394
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2777
3395
|
}
|
|
3396
|
+
/** Common styles for read-only viewers */
|
|
3397
|
+
const READ_ONLY_STYLES = `
|
|
3398
|
+
* { box-sizing: border-box; }
|
|
3399
|
+
body {
|
|
3400
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
3401
|
+
line-height: 1.6;
|
|
3402
|
+
max-width: 900px;
|
|
3403
|
+
margin: 0 auto;
|
|
3404
|
+
padding: 2rem;
|
|
3405
|
+
background: #f8f9fa;
|
|
3406
|
+
color: #212529;
|
|
3407
|
+
}
|
|
3408
|
+
h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; font-size: 1.5rem; }
|
|
3409
|
+
.content {
|
|
3410
|
+
background: white;
|
|
3411
|
+
border-radius: 8px;
|
|
3412
|
+
padding: 1.5rem;
|
|
3413
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
3414
|
+
}
|
|
3415
|
+
pre {
|
|
3416
|
+
background: #1e1e1e;
|
|
3417
|
+
color: #d4d4d4;
|
|
3418
|
+
padding: 1rem;
|
|
3419
|
+
border-radius: 6px;
|
|
3420
|
+
overflow-x: auto;
|
|
3421
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
3422
|
+
font-size: 0.9rem;
|
|
3423
|
+
line-height: 1.5;
|
|
3424
|
+
}
|
|
3425
|
+
.badge {
|
|
3426
|
+
font-size: 0.75rem;
|
|
3427
|
+
padding: 0.2rem 0.5rem;
|
|
3428
|
+
background: #e9ecef;
|
|
3429
|
+
border-radius: 4px;
|
|
3430
|
+
color: #6c757d;
|
|
3431
|
+
margin-left: 0.75rem;
|
|
3432
|
+
font-weight: normal;
|
|
3433
|
+
}
|
|
3434
|
+
`;
|
|
3435
|
+
/**
|
|
3436
|
+
* Render markdown content as read-only HTML.
|
|
3437
|
+
* Simple rendering without full markdown parsing.
|
|
3438
|
+
*/
|
|
3439
|
+
function renderMarkdownHtml(content, filename) {
|
|
3440
|
+
const lines = content.split("\n");
|
|
3441
|
+
let html = "";
|
|
3442
|
+
let inParagraph = false;
|
|
3443
|
+
for (const line of lines) {
|
|
3444
|
+
const trimmed = line.trim();
|
|
3445
|
+
if (trimmed.startsWith("# ")) {
|
|
3446
|
+
if (inParagraph) {
|
|
3447
|
+
html += "</p>";
|
|
3448
|
+
inParagraph = false;
|
|
3449
|
+
}
|
|
3450
|
+
html += `<h2>${escapeHtml(trimmed.slice(2))}</h2>`;
|
|
3451
|
+
} else if (trimmed.startsWith("## ")) {
|
|
3452
|
+
if (inParagraph) {
|
|
3453
|
+
html += "</p>";
|
|
3454
|
+
inParagraph = false;
|
|
3455
|
+
}
|
|
3456
|
+
html += `<h3>${escapeHtml(trimmed.slice(3))}</h3>`;
|
|
3457
|
+
} else if (trimmed.startsWith("### ")) {
|
|
3458
|
+
if (inParagraph) {
|
|
3459
|
+
html += "</p>";
|
|
3460
|
+
inParagraph = false;
|
|
3461
|
+
}
|
|
3462
|
+
html += `<h4>${escapeHtml(trimmed.slice(4))}</h4>`;
|
|
3463
|
+
} else if (trimmed === "") {
|
|
3464
|
+
if (inParagraph) {
|
|
3465
|
+
html += "</p>";
|
|
3466
|
+
inParagraph = false;
|
|
3467
|
+
}
|
|
3468
|
+
} else {
|
|
3469
|
+
if (!inParagraph) {
|
|
3470
|
+
html += "<p>";
|
|
3471
|
+
inParagraph = true;
|
|
3472
|
+
} else html += "<br>";
|
|
3473
|
+
html += escapeHtml(trimmed);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
if (inParagraph) html += "</p>";
|
|
3477
|
+
return `<!DOCTYPE html>
|
|
3478
|
+
<html lang="en">
|
|
3479
|
+
<head>
|
|
3480
|
+
<meta charset="UTF-8">
|
|
3481
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3482
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3483
|
+
<style>${READ_ONLY_STYLES}
|
|
3484
|
+
h2 { color: #495057; font-size: 1.3rem; margin-top: 1.5rem; }
|
|
3485
|
+
h3 { color: #6c757d; font-size: 1.1rem; margin-top: 1.25rem; }
|
|
3486
|
+
h4 { color: #6c757d; font-size: 1rem; margin-top: 1rem; }
|
|
3487
|
+
p { margin: 0.75rem 0; }
|
|
3488
|
+
</style>
|
|
3489
|
+
</head>
|
|
3490
|
+
<body>
|
|
3491
|
+
<h1>${escapeHtml(filename)}<span class="badge">Markdown</span></h1>
|
|
3492
|
+
<div class="content">
|
|
3493
|
+
${html}
|
|
3494
|
+
</div>
|
|
3495
|
+
</body>
|
|
3496
|
+
</html>`;
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Render YAML content with syntax highlighting.
|
|
3500
|
+
*/
|
|
3501
|
+
function renderYamlHtml(content, filename) {
|
|
3502
|
+
const highlighted = content.split("\n").map((line) => {
|
|
3503
|
+
const colonIndex = line.indexOf(":");
|
|
3504
|
+
if (colonIndex > 0 && !line.trim().startsWith("#") && !line.trim().startsWith("-")) return `<span style="color:#9cdcfe">${escapeHtml(line.slice(0, colonIndex))}</span>${escapeHtml(line.slice(colonIndex))}`;
|
|
3505
|
+
if (line.trim().startsWith("#")) return `<span style="color:#6a9955">${escapeHtml(line)}</span>`;
|
|
3506
|
+
return escapeHtml(line);
|
|
3507
|
+
}).join("\n");
|
|
3508
|
+
return `<!DOCTYPE html>
|
|
3509
|
+
<html lang="en">
|
|
3510
|
+
<head>
|
|
3511
|
+
<meta charset="UTF-8">
|
|
3512
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3513
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3514
|
+
<style>${READ_ONLY_STYLES}</style>
|
|
3515
|
+
</head>
|
|
3516
|
+
<body>
|
|
3517
|
+
<h1>${escapeHtml(filename)}<span class="badge">YAML</span></h1>
|
|
3518
|
+
<div class="content">
|
|
3519
|
+
<pre>${highlighted}</pre>
|
|
3520
|
+
</div>
|
|
3521
|
+
</body>
|
|
3522
|
+
</html>`;
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Render JSON content with syntax highlighting and formatting.
|
|
3526
|
+
*/
|
|
3527
|
+
function renderJsonHtml(content, filename) {
|
|
3528
|
+
let formatted;
|
|
3529
|
+
try {
|
|
3530
|
+
const parsed = JSON.parse(content);
|
|
3531
|
+
formatted = JSON.stringify(parsed, null, 2);
|
|
3532
|
+
} catch {
|
|
3533
|
+
formatted = content;
|
|
3534
|
+
}
|
|
3535
|
+
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>");
|
|
3536
|
+
return `<!DOCTYPE html>
|
|
3537
|
+
<html lang="en">
|
|
3538
|
+
<head>
|
|
3539
|
+
<meta charset="UTF-8">
|
|
3540
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3541
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3542
|
+
<style>${READ_ONLY_STYLES}</style>
|
|
3543
|
+
</head>
|
|
3544
|
+
<body>
|
|
3545
|
+
<h1>${escapeHtml(filename)}<span class="badge">JSON</span></h1>
|
|
3546
|
+
<div class="content">
|
|
3547
|
+
<pre>${highlighted}</pre>
|
|
3548
|
+
</div>
|
|
3549
|
+
</body>
|
|
3550
|
+
</html>`;
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Render plain text content.
|
|
3554
|
+
*/
|
|
3555
|
+
function renderPlainTextHtml(content, filename) {
|
|
3556
|
+
return `<!DOCTYPE html>
|
|
3557
|
+
<html lang="en">
|
|
3558
|
+
<head>
|
|
3559
|
+
<meta charset="UTF-8">
|
|
3560
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3561
|
+
<title>${escapeHtml(filename)} - Markform Viewer</title>
|
|
3562
|
+
<style>${READ_ONLY_STYLES}</style>
|
|
3563
|
+
</head>
|
|
3564
|
+
<body>
|
|
3565
|
+
<h1>${escapeHtml(filename)}<span class="badge">Text</span></h1>
|
|
3566
|
+
<div class="content">
|
|
3567
|
+
<pre>${escapeHtml(content)}</pre>
|
|
3568
|
+
</div>
|
|
3569
|
+
</body>
|
|
3570
|
+
</html>`;
|
|
3571
|
+
}
|
|
2778
3572
|
|
|
2779
3573
|
//#endregion
|
|
2780
3574
|
//#region src/cli/commands/render.ts
|
|
@@ -2798,7 +3592,7 @@ function registerRenderCommand(program) {
|
|
|
2798
3592
|
const outputPath = options.output ? resolve(options.output) : getDefaultOutputPath(filePath);
|
|
2799
3593
|
try {
|
|
2800
3594
|
logVerbose(ctx, `Reading file: ${filePath}`);
|
|
2801
|
-
const content = await readFile(filePath);
|
|
3595
|
+
const content = await readFile$1(filePath);
|
|
2802
3596
|
logVerbose(ctx, "Parsing form...");
|
|
2803
3597
|
const form = parseForm(content);
|
|
2804
3598
|
logVerbose(ctx, "Rendering HTML...");
|
|
@@ -2816,6 +3610,156 @@ function registerRenderCommand(program) {
|
|
|
2816
3610
|
});
|
|
2817
3611
|
}
|
|
2818
3612
|
|
|
3613
|
+
//#endregion
|
|
3614
|
+
//#region src/cli/lib/initialValues.ts
|
|
3615
|
+
/**
|
|
3616
|
+
* Parse initial value inputs from CLI flags.
|
|
3617
|
+
*
|
|
3618
|
+
* Supports formats:
|
|
3619
|
+
* - fieldId=value (string values)
|
|
3620
|
+
* - fieldId:number=123 (explicit number)
|
|
3621
|
+
* - fieldId:list=a,b,c (comma-separated list)
|
|
3622
|
+
*
|
|
3623
|
+
* @param inputs Array of input strings in "fieldId=value" format
|
|
3624
|
+
* @returns Array of patches to apply as initial values
|
|
3625
|
+
*/
|
|
3626
|
+
function parseInitialValues(inputs) {
|
|
3627
|
+
const patches = [];
|
|
3628
|
+
for (const input of inputs) {
|
|
3629
|
+
const equalsIndex = input.indexOf("=");
|
|
3630
|
+
if (equalsIndex === -1) throw new Error(`Invalid input format: "${input}" (expected "fieldId=value")`);
|
|
3631
|
+
const fieldSpec = input.slice(0, equalsIndex);
|
|
3632
|
+
const value = input.slice(equalsIndex + 1);
|
|
3633
|
+
const colonIndex = fieldSpec.indexOf(":");
|
|
3634
|
+
let fieldId;
|
|
3635
|
+
let type = null;
|
|
3636
|
+
if (colonIndex !== -1) {
|
|
3637
|
+
fieldId = fieldSpec.slice(0, colonIndex);
|
|
3638
|
+
type = fieldSpec.slice(colonIndex + 1).toLowerCase();
|
|
3639
|
+
} else fieldId = fieldSpec;
|
|
3640
|
+
if (type === "number") {
|
|
3641
|
+
const numValue = parseFloat(value);
|
|
3642
|
+
if (isNaN(numValue)) throw new Error(`Invalid number value for "${fieldId}": "${value}"`);
|
|
3643
|
+
patches.push({
|
|
3644
|
+
op: "set_number",
|
|
3645
|
+
fieldId,
|
|
3646
|
+
value: numValue
|
|
3647
|
+
});
|
|
3648
|
+
} else if (type === "list") {
|
|
3649
|
+
const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3650
|
+
patches.push({
|
|
3651
|
+
op: "set_string_list",
|
|
3652
|
+
fieldId,
|
|
3653
|
+
items
|
|
3654
|
+
});
|
|
3655
|
+
} else patches.push({
|
|
3656
|
+
op: "set_string",
|
|
3657
|
+
fieldId,
|
|
3658
|
+
value
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
return patches;
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3664
|
+
* Validate that all initial values reference valid field IDs.
|
|
3665
|
+
*
|
|
3666
|
+
* @param patches Patches to validate
|
|
3667
|
+
* @param validFieldIds Set of valid field IDs from the form
|
|
3668
|
+
* @returns Array of invalid field IDs (empty if all valid)
|
|
3669
|
+
*/
|
|
3670
|
+
function validateInitialValueFields(patches, validFieldIds) {
|
|
3671
|
+
const invalid = [];
|
|
3672
|
+
for (const patch of patches) if ("fieldId" in patch && patch.fieldId && !validFieldIds.has(patch.fieldId)) invalid.push(patch.fieldId);
|
|
3673
|
+
return invalid;
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
//#endregion
|
|
3677
|
+
//#region src/cli/commands/research.ts
|
|
3678
|
+
/**
|
|
3679
|
+
* Register the research command.
|
|
3680
|
+
*/
|
|
3681
|
+
function registerResearchCommand(program) {
|
|
3682
|
+
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) => {
|
|
3683
|
+
const ctx = getCommandContext(cmd);
|
|
3684
|
+
const startTime = Date.now();
|
|
3685
|
+
try {
|
|
3686
|
+
if (!options.model) {
|
|
3687
|
+
logError("--model is required");
|
|
3688
|
+
console.log("");
|
|
3689
|
+
console.log(formatSuggestedLlms());
|
|
3690
|
+
process.exit(1);
|
|
3691
|
+
}
|
|
3692
|
+
const modelId = options.model;
|
|
3693
|
+
if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
|
|
3694
|
+
const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
|
|
3695
|
+
logError(`Model "${modelId}" does not support web search.`);
|
|
3696
|
+
console.log("");
|
|
3697
|
+
console.log(pc.yellow("Research forms require web search capabilities."));
|
|
3698
|
+
console.log(`Use a model from: ${webSearchProviders.join(", ")}`);
|
|
3699
|
+
console.log("");
|
|
3700
|
+
console.log("Examples:");
|
|
3701
|
+
console.log(" --model openai/gpt-5-mini");
|
|
3702
|
+
console.log(" --model anthropic/claude-sonnet-4-5");
|
|
3703
|
+
console.log(" --model google/gemini-2.5-flash");
|
|
3704
|
+
console.log(" --model xai/grok-4");
|
|
3705
|
+
process.exit(1);
|
|
3706
|
+
}
|
|
3707
|
+
const inputPath = resolve(input);
|
|
3708
|
+
logVerbose(ctx, `Input: ${inputPath}`);
|
|
3709
|
+
const form = parseForm(await readFile$1(inputPath));
|
|
3710
|
+
logVerbose(ctx, `Parsed form: ${form.schema.id}`);
|
|
3711
|
+
const initialInputs = options.input ?? [];
|
|
3712
|
+
if (initialInputs.length > 0) {
|
|
3713
|
+
const patches = parseInitialValues(initialInputs);
|
|
3714
|
+
const invalidFields = validateInitialValueFields(patches, new Set(form.orderIndex));
|
|
3715
|
+
if (invalidFields.length > 0) logWarn(ctx, `Unknown field IDs: ${invalidFields.join(", ")}`);
|
|
3716
|
+
applyPatches(form, patches);
|
|
3717
|
+
logInfo(ctx, `Applied ${patches.length} initial value(s)`);
|
|
3718
|
+
}
|
|
3719
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
3720
|
+
let outputPath;
|
|
3721
|
+
if (options.output) outputPath = resolve(options.output);
|
|
3722
|
+
else outputPath = generateVersionedPathInFormsDir(inputPath, formsDir);
|
|
3723
|
+
logVerbose(ctx, `Output: ${outputPath}`);
|
|
3724
|
+
const maxTurns = parseInt(options.maxTurns, 10);
|
|
3725
|
+
const maxPatchesPerTurn = parseInt(options.maxPatches, 10);
|
|
3726
|
+
const maxIssuesPerTurn = parseInt(options.maxIssues, 10);
|
|
3727
|
+
logInfo(ctx, `Research fill with model: ${modelId}`);
|
|
3728
|
+
logVerbose(ctx, `Max turns: ${maxTurns}`);
|
|
3729
|
+
logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
|
|
3730
|
+
logVerbose(ctx, `Max issues/turn: ${maxIssuesPerTurn}`);
|
|
3731
|
+
const result = await runResearch(form, {
|
|
3732
|
+
model: modelId,
|
|
3733
|
+
maxTurns,
|
|
3734
|
+
maxPatchesPerTurn,
|
|
3735
|
+
maxIssuesPerTurn,
|
|
3736
|
+
targetRoles: [AGENT_ROLE],
|
|
3737
|
+
fillMode: "continue"
|
|
3738
|
+
});
|
|
3739
|
+
if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
|
|
3740
|
+
logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
|
|
3741
|
+
logInfo(ctx, `Turns: ${result.totalTurns}`);
|
|
3742
|
+
if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
|
|
3743
|
+
const { reportPath, yamlPath, formPath } = await exportMultiFormat(result.form, outputPath);
|
|
3744
|
+
logSuccess(ctx, "Outputs:");
|
|
3745
|
+
console.log(` ${reportPath} ${pc.dim("(output report)")}`);
|
|
3746
|
+
console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
|
|
3747
|
+
console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
|
|
3748
|
+
if (options.transcript && result.transcript) {
|
|
3749
|
+
const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
|
|
3750
|
+
const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
|
|
3751
|
+
const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
|
|
3752
|
+
await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
|
|
3753
|
+
logInfo(ctx, `Transcript: ${transcriptPath}`);
|
|
3754
|
+
}
|
|
3755
|
+
logTiming(ctx, "Research fill", Date.now() - startTime);
|
|
3756
|
+
} catch (error) {
|
|
3757
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
3758
|
+
process.exit(1);
|
|
3759
|
+
}
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
|
|
2819
3763
|
//#endregion
|
|
2820
3764
|
//#region src/cli/commands/validate.ts
|
|
2821
3765
|
/**
|
|
@@ -2875,12 +3819,10 @@ function formatConsoleReport(report, useColors) {
|
|
|
2875
3819
|
lines.push(bold("Progress:"));
|
|
2876
3820
|
lines.push(` Total fields: ${progress.counts.totalFields}`);
|
|
2877
3821
|
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}`);
|
|
3822
|
+
lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
|
|
3823
|
+
lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
|
|
3824
|
+
lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
|
|
3825
|
+
lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
|
|
2884
3826
|
lines.push("");
|
|
2885
3827
|
if (report.issues.length > 0) {
|
|
2886
3828
|
lines.push(bold(`Issues (${report.issues.length}):`));
|
|
@@ -2900,7 +3842,7 @@ function registerValidateCommand(program) {
|
|
|
2900
3842
|
const ctx = getCommandContext(cmd);
|
|
2901
3843
|
try {
|
|
2902
3844
|
logVerbose(ctx, `Reading file: ${file}`);
|
|
2903
|
-
const content = await readFile(file);
|
|
3845
|
+
const content = await readFile$1(file);
|
|
2904
3846
|
logVerbose(ctx, "Parsing form...");
|
|
2905
3847
|
const form = parseForm(content);
|
|
2906
3848
|
logVerbose(ctx, "Running validation...");
|
|
@@ -2952,18 +3894,22 @@ function withColoredHelp(cmd) {
|
|
|
2952
3894
|
*/
|
|
2953
3895
|
function createProgram() {
|
|
2954
3896
|
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
|
-
|
|
3897
|
+
program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION).showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`);
|
|
3898
|
+
registerReadmeCommand(program);
|
|
3899
|
+
registerDocsCommand(program);
|
|
3900
|
+
registerSpecCommand(program);
|
|
2958
3901
|
registerApplyCommand(program);
|
|
2959
|
-
registerExportCommand(program);
|
|
2960
3902
|
registerDumpCommand(program);
|
|
2961
|
-
|
|
2962
|
-
|
|
3903
|
+
registerExamplesCommand(program);
|
|
3904
|
+
registerExportCommand(program);
|
|
2963
3905
|
registerFillCommand(program);
|
|
3906
|
+
registerInspectCommand(program);
|
|
2964
3907
|
registerModelsCommand(program);
|
|
2965
|
-
|
|
2966
|
-
|
|
3908
|
+
registerRenderCommand(program);
|
|
3909
|
+
registerReportCommand(program);
|
|
3910
|
+
registerResearchCommand(program);
|
|
3911
|
+
registerServeCommand(program);
|
|
3912
|
+
registerValidateCommand(program);
|
|
2967
3913
|
return program;
|
|
2968
3914
|
}
|
|
2969
3915
|
/**
|