markform 0.1.5 → 0.1.7
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/README.md +126 -68
- package/dist/ai-sdk.d.mts +1 -2
- package/dist/ai-sdk.mjs +3 -2
- package/dist/{apply-BCCiJzQr.mjs → apply-g23rRn7p.mjs} +29 -24
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +3 -6
- package/dist/{cli-D469amuk.mjs → cli-Bqlm-WWw.mjs} +1528 -730
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +2 -6
- package/dist/coreTypes-DCvD7feM.d.mts +3203 -0
- package/dist/{coreTypes-pyctKRgc.mjs → coreTypes-__Cwxz5q.mjs} +10 -3
- package/dist/index.d.mts +164 -8
- package/dist/index.mjs +6 -5
- package/dist/{session-uF0e6m6k.mjs → session-CgCNni0e.mjs} +3 -2
- package/dist/session-DruaYPZ1.mjs +4 -0
- package/dist/{shared-CZsyShck.mjs → shared-C9yW5FLZ.mjs} +2 -1
- package/dist/{shared-BqPnYXrn.mjs → shared-DQ6y3Ggc.mjs} +4 -2
- package/dist/{src-Df0XX7UB.mjs → src-BiuxbzF3.mjs} +495 -91
- package/docs/markform-apis.md +52 -1
- package/docs/markform-spec.md +11 -2
- package/examples/earnings-analysis/earnings-analysis.form.md +1 -0
- package/examples/movie-research/movie-research-basic.form.md +1 -0
- package/examples/movie-research/movie-research-deep.form.md +1 -0
- package/examples/movie-research/{movie-research-minimal.form.md → movie-research-demo.form.md} +4 -3
- package/examples/simple/simple.form.md +1 -0
- package/examples/simple/simple.schema.json +374 -0
- package/examples/startup-deep-research/startup-deep-research.form.md +1 -0
- package/examples/startup-research/startup-research.form.md +1 -0
- package/package.json +11 -9
- package/dist/coreTypes-9XZSNOv6.d.mts +0 -8951
- package/dist/session-B_stoXQn.mjs +0 -4
|
@@ -1,19 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
1
|
+
|
|
2
|
+
import { L as PatchSchema } from "./coreTypes-__Cwxz5q.mjs";
|
|
3
|
+
import { A as deriveSchemaPath, D as USER_ROLE, E as REPORT_EXTENSION, F as formatSuggestedLlms, L as hasWebSearchSupport, M as parseRolesFlag, N as SUGGESTED_LLMS, O as deriveExportPath, P as WEB_SEARCH_CONFIG, R as parseModelIdForDisplay, S as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, T as MAX_FORMS_IN_MENU, _ as DEFAULT_MAX_PATCHES_PER_TURN, d as serialize, f as serializeRawMarkdown, g as DEFAULT_MAX_ISSUES_PER_TURN, h as DEFAULT_FORMS_DIR, i as inspect, j as detectFileType, k as deriveReportPath, m as AGENT_ROLE, n as getAllFields, p as serializeReportMarkdown, t as applyPatches, v as DEFAULT_MAX_TURNS, x as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, y as DEFAULT_PORT } from "./apply-g23rRn7p.mjs";
|
|
4
|
+
import { E as parseForm, T as formToJsonSchema, a as resolveHarnessConfig, c as getProviderNames, f as createMockAgent, i as runResearch, l as resolveModel, m as createHarness, n as isResearchForm, o as fillForm, s as getProviderInfo, t as VERSION, u as createLiveAgent } from "./src-BiuxbzF3.mjs";
|
|
5
|
+
import { n as serializeSession } from "./session-CgCNni0e.mjs";
|
|
6
|
+
import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-DQ6y3Ggc.mjs";
|
|
6
7
|
import YAML from "yaml";
|
|
7
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import pc from "picocolors";
|
|
10
|
-
import {
|
|
10
|
+
import { exec, execSync, spawn } from "node:child_process";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
11
13
|
import { fileURLToPath } from "node:url";
|
|
12
14
|
import { readFile } from "node:fs/promises";
|
|
13
15
|
import * as p from "@clack/prompts";
|
|
14
|
-
import { exec, spawn } from "node:child_process";
|
|
15
16
|
import { createServer } from "node:http";
|
|
16
17
|
|
|
18
|
+
//#region src/cli/lib/cliVersion.ts
|
|
19
|
+
/**
|
|
20
|
+
* CLI version helper that computes git version in development mode.
|
|
21
|
+
*
|
|
22
|
+
* When running via tsx (development), __MARKFORM_VERSION__ is not injected,
|
|
23
|
+
* so VERSION from index.ts returns 'development'. This module computes the
|
|
24
|
+
* actual git version dynamically for consistent version display.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Get version from git tags with format: X.Y.Z-dev.N.hash
|
|
28
|
+
* Only called when running in development mode (via tsx).
|
|
29
|
+
*/
|
|
30
|
+
function getGitVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const git = (args) => execSync(`git ${args}`, {
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
stdio: [
|
|
35
|
+
"ignore",
|
|
36
|
+
"pipe",
|
|
37
|
+
"ignore"
|
|
38
|
+
]
|
|
39
|
+
}).trim();
|
|
40
|
+
const tag = git("describe --tags --abbrev=0");
|
|
41
|
+
const tagVersion = tag.replace(/^v/, "");
|
|
42
|
+
const [major, minor, patch] = tagVersion.split(".").map(Number);
|
|
43
|
+
const commitsSinceTag = parseInt(git(`rev-list ${tag}..HEAD --count`), 10);
|
|
44
|
+
const hash = git("rev-parse --short=7 HEAD");
|
|
45
|
+
let dirty = false;
|
|
46
|
+
try {
|
|
47
|
+
git("diff --quiet");
|
|
48
|
+
git("diff --cached --quiet");
|
|
49
|
+
} catch {
|
|
50
|
+
dirty = true;
|
|
51
|
+
}
|
|
52
|
+
if (commitsSinceTag === 0 && !dirty) return tagVersion;
|
|
53
|
+
return `${major}.${minor}.${(patch ?? 0) + 1}-dev.${commitsSinceTag}.${dirty ? `${hash}-dirty` : hash}`;
|
|
54
|
+
} catch {
|
|
55
|
+
return "development";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* CLI version - uses build-time VERSION if available, otherwise computes from git.
|
|
60
|
+
*/
|
|
61
|
+
const CLI_VERSION = VERSION === "development" ? getGitVersion() : VERSION;
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/cli/lib/paths.ts
|
|
65
|
+
/**
|
|
66
|
+
* Path utilities for CLI commands.
|
|
67
|
+
*
|
|
68
|
+
* This module contains Node.js-dependent path utilities that are only used
|
|
69
|
+
* by CLI code. Keeping these separate from settings.ts allows the core
|
|
70
|
+
* library to remain Node.js-free.
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the forms directory path to an absolute path.
|
|
74
|
+
* Uses the provided override or falls back to DEFAULT_FORMS_DIR.
|
|
75
|
+
*
|
|
76
|
+
* @param override Optional override path from CLI --forms-dir option
|
|
77
|
+
* @param cwd Base directory for resolving relative paths (defaults to process.cwd())
|
|
78
|
+
* @returns Absolute path to the forms directory
|
|
79
|
+
*/
|
|
80
|
+
function getFormsDir(override, cwd = process.cwd()) {
|
|
81
|
+
return resolve(cwd, override ?? DEFAULT_FORMS_DIR);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
17
85
|
//#region src/cli/commands/apis.ts
|
|
18
86
|
/**
|
|
19
87
|
* Get the path to the markform-apis.md file.
|
|
@@ -124,7 +192,7 @@ function formatState$2(state, useColors) {
|
|
|
124
192
|
/**
|
|
125
193
|
* Format apply report for console output.
|
|
126
194
|
*/
|
|
127
|
-
function formatConsoleReport$
|
|
195
|
+
function formatConsoleReport$3(report, useColors) {
|
|
128
196
|
const lines = [];
|
|
129
197
|
const bold = useColors ? pc.bold : (s) => s;
|
|
130
198
|
const dim = useColors ? pc.dim : (s) => s;
|
|
@@ -206,7 +274,7 @@ function registerApplyCommand(program) {
|
|
|
206
274
|
structure: result.structureSummary,
|
|
207
275
|
progress: result.progressSummary,
|
|
208
276
|
issues: result.issues
|
|
209
|
-
}, (data, useColors) => formatConsoleReport$
|
|
277
|
+
}, (data, useColors) => formatConsoleReport$3(data, useColors));
|
|
210
278
|
console.error(output);
|
|
211
279
|
process.exit(1);
|
|
212
280
|
}
|
|
@@ -218,7 +286,7 @@ function registerApplyCommand(program) {
|
|
|
218
286
|
structure: result.structureSummary,
|
|
219
287
|
progress: result.progressSummary,
|
|
220
288
|
issues: result.issues
|
|
221
|
-
}, (data, useColors) => formatConsoleReport$
|
|
289
|
+
}, (data, useColors) => formatConsoleReport$3(data, useColors));
|
|
222
290
|
if (options.output) {
|
|
223
291
|
await writeFile(options.output, output);
|
|
224
292
|
logSuccess(ctx, `Report written to ${options.output}`);
|
|
@@ -238,34 +306,26 @@ function registerApplyCommand(program) {
|
|
|
238
306
|
}
|
|
239
307
|
|
|
240
308
|
//#endregion
|
|
241
|
-
//#region src/cli/
|
|
309
|
+
//#region src/cli/lib/fileViewer.ts
|
|
242
310
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
311
|
+
* File viewer utility for displaying files with colorization and pagination.
|
|
312
|
+
*
|
|
313
|
+
* Provides a modern console experience:
|
|
314
|
+
* - Syntax highlighting for markdown and YAML
|
|
315
|
+
* - Pagination using system pager (less) when available
|
|
316
|
+
* - Fallback to console output when not interactive
|
|
245
317
|
*/
|
|
246
|
-
function getDocsPath() {
|
|
247
|
-
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
248
|
-
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-reference.md");
|
|
249
|
-
return join(dirname(dirname(dirname(thisDir))), "docs", "markform-reference.md");
|
|
250
|
-
}
|
|
251
318
|
/**
|
|
252
|
-
*
|
|
319
|
+
* Check if stdout is an interactive terminal.
|
|
253
320
|
*/
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
return readFileSync(docsPath, "utf-8");
|
|
258
|
-
} catch (error) {
|
|
259
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
260
|
-
throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);
|
|
261
|
-
}
|
|
321
|
+
function isInteractive$3() {
|
|
322
|
+
return process.stdout.isTTY === true;
|
|
262
323
|
}
|
|
263
324
|
/**
|
|
264
|
-
* Apply
|
|
265
|
-
* Colorizes headers, code blocks, and other elements
|
|
325
|
+
* Apply terminal formatting to markdown content.
|
|
326
|
+
* Colorizes headers, code blocks, and other elements.
|
|
266
327
|
*/
|
|
267
|
-
function formatMarkdown$3(content
|
|
268
|
-
if (!useColors) return content;
|
|
328
|
+
function formatMarkdown$3(content) {
|
|
269
329
|
const lines = content.split("\n");
|
|
270
330
|
const formatted = [];
|
|
271
331
|
let inCodeBlock = false;
|
|
@@ -276,11 +336,11 @@ function formatMarkdown$3(content, useColors) {
|
|
|
276
336
|
continue;
|
|
277
337
|
}
|
|
278
338
|
if (inCodeBlock) {
|
|
279
|
-
formatted.push(pc.
|
|
339
|
+
formatted.push(pc.cyan(line));
|
|
280
340
|
continue;
|
|
281
341
|
}
|
|
282
342
|
if (line.startsWith("# ")) {
|
|
283
|
-
formatted.push(pc.bold(pc.
|
|
343
|
+
formatted.push(pc.bold(pc.magenta(line)));
|
|
284
344
|
continue;
|
|
285
345
|
}
|
|
286
346
|
if (line.startsWith("## ")) {
|
|
@@ -288,6 +348,10 @@ function formatMarkdown$3(content, useColors) {
|
|
|
288
348
|
continue;
|
|
289
349
|
}
|
|
290
350
|
if (line.startsWith("### ")) {
|
|
351
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (line.startsWith("#### ")) {
|
|
291
355
|
formatted.push(pc.bold(line));
|
|
292
356
|
continue;
|
|
293
357
|
}
|
|
@@ -300,30 +364,290 @@ function formatMarkdown$3(content, useColors) {
|
|
|
300
364
|
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
301
365
|
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
302
366
|
});
|
|
367
|
+
formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
|
|
368
|
+
return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
|
|
369
|
+
});
|
|
303
370
|
formatted.push(formattedLine);
|
|
304
371
|
}
|
|
305
372
|
return formatted.join("\n");
|
|
306
373
|
}
|
|
307
374
|
/**
|
|
308
|
-
*
|
|
375
|
+
* Apply terminal formatting to YAML content.
|
|
309
376
|
*/
|
|
310
|
-
function
|
|
311
|
-
|
|
377
|
+
function formatYaml(content) {
|
|
378
|
+
const lines = content.split("\n");
|
|
379
|
+
const formatted = [];
|
|
380
|
+
for (const line of lines) {
|
|
381
|
+
if (line.trim().startsWith("#")) {
|
|
382
|
+
formatted.push(pc.dim(line));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
|
|
386
|
+
if (match) {
|
|
387
|
+
const [, indent, key, colon, value] = match;
|
|
388
|
+
formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (line.trim().startsWith("-")) {
|
|
392
|
+
formatted.push(pc.green(line));
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
formatted.push(line);
|
|
396
|
+
}
|
|
397
|
+
return formatted.join("\n");
|
|
312
398
|
}
|
|
313
399
|
/**
|
|
314
|
-
*
|
|
400
|
+
* Format file content based on extension.
|
|
315
401
|
*/
|
|
316
|
-
function
|
|
317
|
-
|
|
402
|
+
function formatContent(content, filename) {
|
|
403
|
+
if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
|
|
404
|
+
if (filename.endsWith(".md")) return formatMarkdown$3(content);
|
|
405
|
+
return content;
|
|
318
406
|
}
|
|
319
407
|
/**
|
|
320
|
-
*
|
|
408
|
+
* Display content using system pager (less) if available.
|
|
409
|
+
* Falls back to console.log if not interactive or pager unavailable.
|
|
410
|
+
*
|
|
411
|
+
* @returns Promise that resolves when viewing is complete
|
|
321
412
|
*/
|
|
322
|
-
function
|
|
323
|
-
|
|
413
|
+
async function displayWithPager(content, title) {
|
|
414
|
+
if (!isInteractive$3()) {
|
|
415
|
+
console.log(content);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
|
|
419
|
+
return new Promise((resolve$1) => {
|
|
420
|
+
const pager = spawn("less", [
|
|
421
|
+
"-R",
|
|
422
|
+
"-S",
|
|
423
|
+
"-X",
|
|
424
|
+
"-F",
|
|
425
|
+
"-K"
|
|
426
|
+
], { stdio: [
|
|
427
|
+
"pipe",
|
|
428
|
+
"inherit",
|
|
429
|
+
"inherit"
|
|
430
|
+
] });
|
|
431
|
+
pager.on("error", () => {
|
|
432
|
+
console.log(header);
|
|
433
|
+
console.log("");
|
|
434
|
+
console.log(content);
|
|
435
|
+
console.log("");
|
|
436
|
+
resolve$1();
|
|
437
|
+
});
|
|
438
|
+
pager.on("close", () => {
|
|
439
|
+
resolve$1();
|
|
440
|
+
});
|
|
441
|
+
pager.stdin.write(header + "\n\n");
|
|
442
|
+
pager.stdin.write(content);
|
|
443
|
+
pager.stdin.end();
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Load and display a file with formatting and pagination.
|
|
448
|
+
*/
|
|
449
|
+
async function viewFile(filePath) {
|
|
450
|
+
const content = readFileSync(filePath, "utf-8");
|
|
451
|
+
const filename = basename(filePath);
|
|
452
|
+
await displayWithPager(formatContent(content, filename), filename);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Show an interactive file viewer chooser.
|
|
456
|
+
*
|
|
457
|
+
* Presents a list of files to view:
|
|
458
|
+
* - "Show report:" for the report output (.report.md) at the top
|
|
459
|
+
* - "Show source:" for other files (.form.md, .raw.md, .yml)
|
|
460
|
+
* - "Quit" at the bottom
|
|
461
|
+
*
|
|
462
|
+
* Loops until the user selects Quit.
|
|
463
|
+
*
|
|
464
|
+
* @param files Array of file options to display
|
|
465
|
+
*/
|
|
466
|
+
async function showFileViewerChooser(files) {
|
|
467
|
+
if (!isInteractive$3()) return;
|
|
468
|
+
console.log("");
|
|
469
|
+
const reportFile = files.find((f) => f.path.endsWith(".report.md"));
|
|
470
|
+
const sourceFiles = files.filter((f) => !f.path.endsWith(".report.md"));
|
|
471
|
+
while (true) {
|
|
472
|
+
const options = [];
|
|
473
|
+
if (reportFile) options.push({
|
|
474
|
+
value: reportFile.path,
|
|
475
|
+
label: `Show report: ${pc.green(basename(reportFile.path))}`,
|
|
476
|
+
hint: reportFile.hint ?? ""
|
|
477
|
+
});
|
|
478
|
+
for (const file of sourceFiles) options.push({
|
|
479
|
+
value: file.path,
|
|
480
|
+
label: `Show source: ${pc.green(basename(file.path))}`,
|
|
481
|
+
hint: file.hint ?? ""
|
|
482
|
+
});
|
|
483
|
+
options.push({
|
|
484
|
+
value: "quit",
|
|
485
|
+
label: "Quit",
|
|
486
|
+
hint: ""
|
|
487
|
+
});
|
|
488
|
+
const selection = await p.select({
|
|
489
|
+
message: "View files:",
|
|
490
|
+
options
|
|
491
|
+
});
|
|
492
|
+
if (p.isCancel(selection) || selection === "quit") break;
|
|
493
|
+
await viewFile(selection);
|
|
494
|
+
console.log("");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/cli/commands/browse.ts
|
|
500
|
+
/**
|
|
501
|
+
* Browse command - Interactive file browser for the forms directory.
|
|
502
|
+
*
|
|
503
|
+
* Provides a menu-based interface for viewing files with syntax highlighting
|
|
504
|
+
* and pagination. Shows .form.md, .report.md, .yml, and .schema.json files.
|
|
505
|
+
*
|
|
506
|
+
* Usage:
|
|
507
|
+
* markform browse # Browse all files in forms/
|
|
508
|
+
* markform browse --filter=foo # Only show files matching pattern
|
|
509
|
+
*/
|
|
510
|
+
/** File extensions we support viewing */
|
|
511
|
+
const VIEWABLE_EXTENSIONS = [
|
|
512
|
+
".form.md",
|
|
513
|
+
".report.md",
|
|
514
|
+
".yml",
|
|
515
|
+
".yaml",
|
|
516
|
+
".schema.json",
|
|
517
|
+
".raw.md"
|
|
518
|
+
];
|
|
519
|
+
/**
|
|
520
|
+
* Check if a file has a viewable extension.
|
|
521
|
+
*/
|
|
522
|
+
function isViewableFile(filename) {
|
|
523
|
+
return VIEWABLE_EXTENSIONS.some((ext) => filename.endsWith(ext));
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get the display extension for sorting/grouping.
|
|
527
|
+
*/
|
|
528
|
+
function getExtension(filename) {
|
|
529
|
+
for (const ext of VIEWABLE_EXTENSIONS) if (filename.endsWith(ext)) return ext;
|
|
530
|
+
return "";
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Scan forms directory for viewable files.
|
|
534
|
+
*/
|
|
535
|
+
function scanFormsDirectory$1(formsDir, filter) {
|
|
536
|
+
const entries = [];
|
|
537
|
+
try {
|
|
538
|
+
const files = readdirSync(formsDir);
|
|
539
|
+
for (const file of files) {
|
|
540
|
+
if (!isViewableFile(file)) continue;
|
|
541
|
+
if (filter && !file.toLowerCase().includes(filter.toLowerCase())) continue;
|
|
542
|
+
const fullPath = join(formsDir, file);
|
|
543
|
+
try {
|
|
544
|
+
const stat = statSync(fullPath);
|
|
545
|
+
if (stat.isFile()) entries.push({
|
|
546
|
+
path: fullPath,
|
|
547
|
+
filename: file,
|
|
548
|
+
mtime: stat.mtime,
|
|
549
|
+
extension: getExtension(file)
|
|
550
|
+
});
|
|
551
|
+
} catch {}
|
|
552
|
+
}
|
|
553
|
+
} catch {}
|
|
554
|
+
entries.sort((a, b) => {
|
|
555
|
+
const timeDiff = b.mtime.getTime() - a.mtime.getTime();
|
|
556
|
+
if (timeDiff !== 0) return timeDiff;
|
|
557
|
+
return a.filename.localeCompare(b.filename);
|
|
558
|
+
});
|
|
559
|
+
return entries;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get extension hint for display.
|
|
563
|
+
*/
|
|
564
|
+
function getExtensionHint(ext) {
|
|
565
|
+
switch (ext) {
|
|
566
|
+
case ".form.md": return "markform source";
|
|
567
|
+
case ".report.md": return "output report";
|
|
568
|
+
case ".yml":
|
|
569
|
+
case ".yaml": return "YAML values";
|
|
570
|
+
case ".schema.json": return "JSON Schema";
|
|
571
|
+
case ".raw.md": return "raw markdown";
|
|
572
|
+
default: return "";
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Format file entry for menu display.
|
|
577
|
+
*/
|
|
578
|
+
function formatFileLabel(entry) {
|
|
579
|
+
return `${entry.extension === ".report.md" ? pc.green("*") : " "} ${entry.filename}`;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Browse specific output files after a form run.
|
|
583
|
+
*
|
|
584
|
+
* This is a convenience function for the examples workflow that shows
|
|
585
|
+
* the standard output files (report, yml, form, schema) for a completed form.
|
|
586
|
+
*
|
|
587
|
+
* @param basePath - Base path of the output (e.g., "forms/movie-research-demo-filled1")
|
|
588
|
+
*/
|
|
589
|
+
async function browseOutputFiles(basePath) {
|
|
590
|
+
const outputExtensions = [
|
|
591
|
+
".report.md",
|
|
592
|
+
".yml",
|
|
593
|
+
".form.md",
|
|
594
|
+
".schema.json"
|
|
595
|
+
];
|
|
596
|
+
const files = [];
|
|
597
|
+
for (const ext of outputExtensions) {
|
|
598
|
+
const fullPath = basePath + ext;
|
|
599
|
+
try {
|
|
600
|
+
statSync(fullPath);
|
|
601
|
+
files.push({
|
|
602
|
+
path: fullPath,
|
|
603
|
+
label: basename(fullPath),
|
|
604
|
+
hint: getExtensionHint(ext)
|
|
605
|
+
});
|
|
606
|
+
} catch {}
|
|
607
|
+
}
|
|
608
|
+
if (files.length === 0) return;
|
|
609
|
+
await showFileViewerChooser(files);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Register the browse command.
|
|
613
|
+
*/
|
|
614
|
+
function registerBrowseCommand(program) {
|
|
615
|
+
program.command("browse").description("Browse and view files in the forms directory").option("--filter <pattern>", "Only show files matching pattern").action(async (options, cmd) => {
|
|
324
616
|
const ctx = getCommandContext(cmd);
|
|
325
617
|
try {
|
|
326
|
-
|
|
618
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
619
|
+
p.intro(pc.bgCyan(pc.black(" markform browse ")));
|
|
620
|
+
const entries = scanFormsDirectory$1(formsDir, options.filter);
|
|
621
|
+
if (entries.length === 0) {
|
|
622
|
+
if (options.filter) p.log.warn(`No files matching "${options.filter}" found in ${formatPath(formsDir)}`);
|
|
623
|
+
else p.log.warn(`No viewable files found in ${formatPath(formsDir)}`);
|
|
624
|
+
console.log("");
|
|
625
|
+
console.log(`Run ${pc.cyan("'markform examples'")} to get started.`);
|
|
626
|
+
p.outro("");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
console.log(pc.dim(`Found ${entries.length} file(s) in ${formatPath(formsDir)}`));
|
|
630
|
+
const menuOptions = entries.map((entry) => ({
|
|
631
|
+
value: entry.path,
|
|
632
|
+
label: formatFileLabel(entry),
|
|
633
|
+
hint: getExtensionHint(entry.extension)
|
|
634
|
+
}));
|
|
635
|
+
menuOptions.push({
|
|
636
|
+
value: "quit",
|
|
637
|
+
label: "Quit",
|
|
638
|
+
hint: ""
|
|
639
|
+
});
|
|
640
|
+
while (true) {
|
|
641
|
+
const selection = await p.select({
|
|
642
|
+
message: "Select a file to view:",
|
|
643
|
+
options: menuOptions
|
|
644
|
+
});
|
|
645
|
+
if (p.isCancel(selection)) break;
|
|
646
|
+
if (selection === "quit") break;
|
|
647
|
+
await viewFile(selection);
|
|
648
|
+
console.log("");
|
|
649
|
+
}
|
|
650
|
+
p.outro("");
|
|
327
651
|
} catch (error) {
|
|
328
652
|
logError(error instanceof Error ? error.message : String(error));
|
|
329
653
|
process.exit(1);
|
|
@@ -332,19 +656,114 @@ function registerDocsCommand(program) {
|
|
|
332
656
|
}
|
|
333
657
|
|
|
334
658
|
//#endregion
|
|
335
|
-
//#region src/cli/
|
|
659
|
+
//#region src/cli/commands/docs.ts
|
|
336
660
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* Provides reusable functions for exporting forms to multiple formats:
|
|
340
|
-
* - Markform format (.form.md) - canonical form with directives
|
|
341
|
-
* - Raw markdown (.raw.md) - plain readable markdown
|
|
342
|
-
* - YAML values (.yml) - extracted field values
|
|
661
|
+
* Get the path to the markform-reference.md file.
|
|
662
|
+
* Works both during development and when installed as a package.
|
|
343
663
|
*/
|
|
664
|
+
function getDocsPath() {
|
|
665
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
666
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-reference.md");
|
|
667
|
+
return join(dirname(dirname(dirname(thisDir))), "docs", "markform-reference.md");
|
|
668
|
+
}
|
|
344
669
|
/**
|
|
345
|
-
*
|
|
346
|
-
|
|
347
|
-
|
|
670
|
+
* Load the docs content.
|
|
671
|
+
*/
|
|
672
|
+
function loadDocs() {
|
|
673
|
+
const docsPath = getDocsPath();
|
|
674
|
+
try {
|
|
675
|
+
return readFileSync(docsPath, "utf-8");
|
|
676
|
+
} catch (error) {
|
|
677
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
678
|
+
throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Apply basic terminal formatting to markdown content.
|
|
683
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
684
|
+
*/
|
|
685
|
+
function formatMarkdown$2(content, useColors) {
|
|
686
|
+
if (!useColors) return content;
|
|
687
|
+
const lines = content.split("\n");
|
|
688
|
+
const formatted = [];
|
|
689
|
+
let inCodeBlock = false;
|
|
690
|
+
for (const line of lines) {
|
|
691
|
+
if (line.startsWith("```")) {
|
|
692
|
+
inCodeBlock = !inCodeBlock;
|
|
693
|
+
formatted.push(pc.dim(line));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (inCodeBlock) {
|
|
697
|
+
formatted.push(pc.dim(line));
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (line.startsWith("# ")) {
|
|
701
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (line.startsWith("## ")) {
|
|
705
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (line.startsWith("### ")) {
|
|
709
|
+
formatted.push(pc.bold(line));
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
713
|
+
return pc.yellow(code);
|
|
714
|
+
});
|
|
715
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
716
|
+
return pc.bold(text);
|
|
717
|
+
});
|
|
718
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
719
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
720
|
+
});
|
|
721
|
+
formatted.push(formattedLine);
|
|
722
|
+
}
|
|
723
|
+
return formatted.join("\n");
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Check if stdout is an interactive terminal.
|
|
727
|
+
*/
|
|
728
|
+
function isInteractive$2() {
|
|
729
|
+
return process.stdout.isTTY === true;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Display content.
|
|
733
|
+
*/
|
|
734
|
+
function displayContent$2(content) {
|
|
735
|
+
console.log(content);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Register the docs command.
|
|
739
|
+
*/
|
|
740
|
+
function registerDocsCommand(program) {
|
|
741
|
+
program.command("docs").description("Display concise Markform syntax reference (agent-friendly)").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
742
|
+
const ctx = getCommandContext(cmd);
|
|
743
|
+
try {
|
|
744
|
+
displayContent$2(formatMarkdown$2(loadDocs(), !options.raw && isInteractive$2() && !ctx.quiet));
|
|
745
|
+
} catch (error) {
|
|
746
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/cli/lib/exportHelpers.ts
|
|
754
|
+
/**
|
|
755
|
+
* Export helpers for multi-format form output.
|
|
756
|
+
*
|
|
757
|
+
* Provides reusable functions for exporting forms to multiple formats:
|
|
758
|
+
* - Markform format (.form.md) - canonical form with directives
|
|
759
|
+
* - Raw markdown (.raw.md) - plain readable markdown
|
|
760
|
+
* - YAML values (.yml) - extracted field values
|
|
761
|
+
* - JSON Schema (.schema.json) - form structure for validation/tooling
|
|
762
|
+
*/
|
|
763
|
+
/**
|
|
764
|
+
* Convert field responses to structured format for export (markform-218).
|
|
765
|
+
*
|
|
766
|
+
* Includes state for all fields:
|
|
348
767
|
* - { state: 'unanswered' } for unfilled fields
|
|
349
768
|
* - { state: 'skipped' } for skipped fields
|
|
350
769
|
* - { state: 'aborted' } for aborted fields
|
|
@@ -428,7 +847,7 @@ function toNotesArray(form) {
|
|
|
428
847
|
* Derive export paths from a base form path.
|
|
429
848
|
* Uses centralized extension constants from settings.ts.
|
|
430
849
|
*
|
|
431
|
-
* Standard exports: report, values (yaml), form.
|
|
850
|
+
* Standard exports: report, values (yaml), form, schema.
|
|
432
851
|
* Raw markdown is available via CLI but not in standard exports.
|
|
433
852
|
*
|
|
434
853
|
* @param basePath - Path to the .form.md file
|
|
@@ -438,7 +857,8 @@ function deriveExportPaths(basePath) {
|
|
|
438
857
|
return {
|
|
439
858
|
reportPath: deriveReportPath(basePath),
|
|
440
859
|
yamlPath: deriveExportPath(basePath, "yaml"),
|
|
441
|
-
formPath: deriveExportPath(basePath, "form")
|
|
860
|
+
formPath: deriveExportPath(basePath, "form"),
|
|
861
|
+
schemaPath: deriveSchemaPath(basePath)
|
|
442
862
|
};
|
|
443
863
|
}
|
|
444
864
|
/**
|
|
@@ -448,6 +868,7 @@ function deriveExportPaths(basePath) {
|
|
|
448
868
|
* - Report format (.report.md) - filtered markdown (excludes instructions, report=false)
|
|
449
869
|
* - YAML values (.yml) - structured format with state and notes
|
|
450
870
|
* - Markform format (.form.md) - canonical form with directives
|
|
871
|
+
* - JSON Schema (.schema.json) - form structure for validation/tooling
|
|
451
872
|
*
|
|
452
873
|
* Note: Raw markdown (.raw.md) is available via CLI `markform export --raw`
|
|
453
874
|
* but is not included in standard multi-format export.
|
|
@@ -470,6 +891,9 @@ async function exportMultiFormat(form, basePath) {
|
|
|
470
891
|
await writeFile(paths.yamlPath, yamlContent);
|
|
471
892
|
const formContent = serialize(form);
|
|
472
893
|
await writeFile(paths.formPath, formContent);
|
|
894
|
+
const schemaResult = formToJsonSchema(form);
|
|
895
|
+
const schemaContent = JSON.stringify(schemaResult.schema, null, 2) + "\n";
|
|
896
|
+
await writeFile(paths.schemaPath, schemaContent);
|
|
473
897
|
return paths;
|
|
474
898
|
}
|
|
475
899
|
|
|
@@ -545,96 +969,6 @@ function registerDumpCommand(program) {
|
|
|
545
969
|
});
|
|
546
970
|
}
|
|
547
971
|
|
|
548
|
-
//#endregion
|
|
549
|
-
//#region src/cli/lib/patchFormat.ts
|
|
550
|
-
/** Maximum characters for a patch value display before truncation */
|
|
551
|
-
const PATCH_VALUE_MAX_LENGTH = 1e3;
|
|
552
|
-
/**
|
|
553
|
-
* Truncate a string to max length with ellipsis if needed.
|
|
554
|
-
*/
|
|
555
|
-
function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
|
|
556
|
-
if (value.length <= maxLength) return value;
|
|
557
|
-
return value.slice(0, maxLength) + "…";
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Format a patch value for display with truncation.
|
|
561
|
-
*/
|
|
562
|
-
function formatPatchValue(patch) {
|
|
563
|
-
switch (patch.op) {
|
|
564
|
-
case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
565
|
-
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
566
|
-
case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
567
|
-
case "set_single_select": return patch.selected ?? "(none)";
|
|
568
|
-
case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
|
|
569
|
-
case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
|
|
570
|
-
case "clear_field": return "(cleared)";
|
|
571
|
-
case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
|
|
572
|
-
case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
|
|
573
|
-
case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
574
|
-
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
575
|
-
case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
576
|
-
case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
577
|
-
case "set_table": return patch.rows.length > 0 ? truncate(`[${patch.rows.length} rows]`) : "(empty)";
|
|
578
|
-
case "add_note": return truncate(`note: ${patch.text}`);
|
|
579
|
-
case "remove_note": return `(remove note ${patch.noteId})`;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* Get a short display name for the patch operation type.
|
|
584
|
-
*/
|
|
585
|
-
function formatPatchType(patch) {
|
|
586
|
-
switch (patch.op) {
|
|
587
|
-
case "set_string": return "string";
|
|
588
|
-
case "set_number": return "number";
|
|
589
|
-
case "set_string_list": return "string_list";
|
|
590
|
-
case "set_single_select": return "select";
|
|
591
|
-
case "set_multi_select": return "multi_select";
|
|
592
|
-
case "set_checkboxes": return "checkboxes";
|
|
593
|
-
case "clear_field": return "clear";
|
|
594
|
-
case "skip_field": return "skip";
|
|
595
|
-
case "abort_field": return "abort";
|
|
596
|
-
case "set_url": return "url";
|
|
597
|
-
case "set_url_list": return "url_list";
|
|
598
|
-
case "set_date": return "date";
|
|
599
|
-
case "set_year": return "year";
|
|
600
|
-
case "set_table": return "table";
|
|
601
|
-
case "add_note": return "note";
|
|
602
|
-
case "remove_note": return "remove_note";
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
//#endregion
|
|
607
|
-
//#region src/cli/lib/formatting.ts
|
|
608
|
-
/**
|
|
609
|
-
* Get a short status word from an issue reason.
|
|
610
|
-
*/
|
|
611
|
-
function issueReasonToStatus(reason) {
|
|
612
|
-
switch (reason) {
|
|
613
|
-
case "required_missing": return "missing";
|
|
614
|
-
case "validation_error": return "invalid";
|
|
615
|
-
case "checkbox_incomplete": return "incomplete";
|
|
616
|
-
case "min_items_not_met": return "too-few";
|
|
617
|
-
case "optional_empty": return "empty";
|
|
618
|
-
default: return "issue";
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Format a single issue as "fieldId (status)".
|
|
623
|
-
*/
|
|
624
|
-
function formatIssueBrief(issue) {
|
|
625
|
-
const status = issueReasonToStatus(issue.reason);
|
|
626
|
-
return `${issue.ref} (${status})`;
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Format issues for turn logging - shows count and brief field list.
|
|
630
|
-
* Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
|
|
631
|
-
*/
|
|
632
|
-
function formatTurnIssues(issues, maxShow = 5) {
|
|
633
|
-
const count = issues.length;
|
|
634
|
-
if (count === 0) return "0 issues";
|
|
635
|
-
return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
972
|
//#endregion
|
|
639
973
|
//#region src/cli/examples/exampleRegistry.ts
|
|
640
974
|
/**
|
|
@@ -649,18 +983,18 @@ function formatTurnIssues(issues, maxShow = 5) {
|
|
|
649
983
|
* Title and description are loaded dynamically from frontmatter.
|
|
650
984
|
*/
|
|
651
985
|
const EXAMPLE_DEFINITIONS = [
|
|
986
|
+
{
|
|
987
|
+
id: "movie-research-demo",
|
|
988
|
+
filename: "movie-research-demo.form.md",
|
|
989
|
+
path: "movie-research/movie-research-demo.form.md",
|
|
990
|
+
type: "research"
|
|
991
|
+
},
|
|
652
992
|
{
|
|
653
993
|
id: "simple",
|
|
654
994
|
filename: "simple.form.md",
|
|
655
995
|
path: "simple/simple.form.md",
|
|
656
996
|
type: "fill"
|
|
657
997
|
},
|
|
658
|
-
{
|
|
659
|
-
id: "movie-research-minimal",
|
|
660
|
-
filename: "movie-research-minimal.form.md",
|
|
661
|
-
path: "movie-research/movie-research-minimal.form.md",
|
|
662
|
-
type: "research"
|
|
663
|
-
},
|
|
664
998
|
{
|
|
665
999
|
id: "movie-research-basic",
|
|
666
1000
|
filename: "movie-research-basic.form.md",
|
|
@@ -673,19 +1007,29 @@ const EXAMPLE_DEFINITIONS = [
|
|
|
673
1007
|
path: "movie-research/movie-research-deep.form.md",
|
|
674
1008
|
type: "research"
|
|
675
1009
|
},
|
|
676
|
-
{
|
|
677
|
-
id: "earnings-analysis",
|
|
678
|
-
filename: "earnings-analysis.form.md",
|
|
679
|
-
path: "earnings-analysis/earnings-analysis.form.md",
|
|
680
|
-
type: "research"
|
|
681
|
-
},
|
|
682
1010
|
{
|
|
683
1011
|
id: "startup-deep-research",
|
|
684
1012
|
filename: "startup-deep-research.form.md",
|
|
685
1013
|
path: "startup-deep-research/startup-deep-research.form.md",
|
|
686
1014
|
type: "research"
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
id: "earnings-analysis",
|
|
1018
|
+
filename: "earnings-analysis.form.md",
|
|
1019
|
+
path: "earnings-analysis/earnings-analysis.form.md",
|
|
1020
|
+
type: "research"
|
|
687
1021
|
}
|
|
688
1022
|
];
|
|
1023
|
+
/** Default example ID for menus (movie-research-demo, index 0) */
|
|
1024
|
+
const DEFAULT_EXAMPLE_ID = "movie-research-demo";
|
|
1025
|
+
/**
|
|
1026
|
+
* Get the canonical order index for an example by filename.
|
|
1027
|
+
* Returns -1 if not found (unknown files sort to the end).
|
|
1028
|
+
*/
|
|
1029
|
+
function getExampleOrder(filename) {
|
|
1030
|
+
const index = EXAMPLE_DEFINITIONS.findIndex((e) => e.filename === filename);
|
|
1031
|
+
return index >= 0 ? index : EXAMPLE_DEFINITIONS.length;
|
|
1032
|
+
}
|
|
689
1033
|
/**
|
|
690
1034
|
* Get the path to the examples directory.
|
|
691
1035
|
* Works both during development and when installed as a package.
|
|
@@ -772,46 +1116,200 @@ function getAllExamplesWithMetadata() {
|
|
|
772
1116
|
}
|
|
773
1117
|
|
|
774
1118
|
//#endregion
|
|
775
|
-
//#region src/cli/lib/
|
|
1119
|
+
//#region src/cli/lib/formatting.ts
|
|
776
1120
|
/**
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
* Generates versioned filenames to avoid overwriting existing files.
|
|
780
|
-
* Pattern: name.form.md → name-filled1.form.md → name-filled2.form.md
|
|
1121
|
+
* Color and output formatting utilities for CLI.
|
|
781
1122
|
*/
|
|
782
1123
|
/**
|
|
783
|
-
*
|
|
784
|
-
* Also matches legacy -vN pattern for backwards compatibility.
|
|
1124
|
+
* Get a short status word from an issue reason.
|
|
785
1125
|
*/
|
|
786
|
-
|
|
1126
|
+
function issueReasonToStatus(reason) {
|
|
1127
|
+
switch (reason) {
|
|
1128
|
+
case "required_missing": return "missing";
|
|
1129
|
+
case "validation_error": return "invalid";
|
|
1130
|
+
case "checkbox_incomplete": return "incomplete";
|
|
1131
|
+
case "min_items_not_met": return "too-few";
|
|
1132
|
+
case "optional_empty": return "empty";
|
|
1133
|
+
default: return "issue";
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
787
1136
|
/**
|
|
788
|
-
*
|
|
1137
|
+
* Format a single issue as "fieldId (status)".
|
|
789
1138
|
*/
|
|
790
|
-
|
|
1139
|
+
function formatIssueBrief(issue) {
|
|
1140
|
+
const status = issueReasonToStatus(issue.reason);
|
|
1141
|
+
return `${issue.ref} (${status})`;
|
|
1142
|
+
}
|
|
791
1143
|
/**
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
* @param filePath - Path to parse
|
|
795
|
-
* @returns Parsed components or null if not a valid form file
|
|
1144
|
+
* Format issues for turn logging - shows count and brief field list.
|
|
1145
|
+
* Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
|
|
796
1146
|
*/
|
|
797
|
-
function
|
|
798
|
-
const
|
|
799
|
-
if (
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1147
|
+
function formatTurnIssues(issues, maxShow = 5) {
|
|
1148
|
+
const count = issues.length;
|
|
1149
|
+
if (count === 0) return "0 issues";
|
|
1150
|
+
return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Format form info for menu label display.
|
|
1154
|
+
* Format: "filename - Title [runMode]"
|
|
1155
|
+
* Example: "movie-research-deep.form.md - Movie Research (Deep) [research]"
|
|
1156
|
+
*/
|
|
1157
|
+
function formatFormLabel(info) {
|
|
1158
|
+
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
1159
|
+
const runModePart = info.runMode ? ` [${info.runMode}]` : "";
|
|
1160
|
+
return `${info.filename}${titlePart}${runModePart}`;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Format form info for menu hint display.
|
|
1164
|
+
* Returns description without parentheses (prompts library adds them).
|
|
1165
|
+
*/
|
|
1166
|
+
function formatFormHint(info) {
|
|
1167
|
+
return info.description ?? "";
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Format form info for log line (e.g., after copying).
|
|
1171
|
+
* Format: "filename - Title" (dimmed title)
|
|
1172
|
+
* Example: "✓ movie-research-deep.form.md - Movie Research (Deep)"
|
|
1173
|
+
*/
|
|
1174
|
+
function formatFormLogLine(info, prefix) {
|
|
1175
|
+
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
1176
|
+
return `${prefix} ${info.filename}${pc.dim(titlePart)}`;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/cli/lib/runMode.ts
|
|
1181
|
+
/**
|
|
1182
|
+
* Get the set of unique roles present in a form's fields.
|
|
1183
|
+
*/
|
|
1184
|
+
function getFieldRoles(form) {
|
|
1185
|
+
const allFields = getAllFields(form);
|
|
1186
|
+
return new Set(allFields.map((field) => field.role));
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Validate that run_mode is consistent with form structure.
|
|
1190
|
+
*
|
|
1191
|
+
* Rules:
|
|
1192
|
+
* - interactive: Form MUST have at least one role="user" field
|
|
1193
|
+
* - fill: Form MUST have at least one role="agent" field
|
|
1194
|
+
* - research: Form MUST have at least one role="agent" field
|
|
1195
|
+
*/
|
|
1196
|
+
function validateRunMode(form, runMode) {
|
|
1197
|
+
const roles = getFieldRoles(form);
|
|
1198
|
+
switch (runMode) {
|
|
1199
|
+
case "interactive":
|
|
1200
|
+
if (!roles.has(USER_ROLE)) return {
|
|
1201
|
+
valid: false,
|
|
1202
|
+
error: `run_mode="interactive" but form has no user-role fields. Available roles: ${[...roles].join(", ") || "(none)"}`
|
|
1203
|
+
};
|
|
1204
|
+
break;
|
|
1205
|
+
case "fill":
|
|
1206
|
+
case "research":
|
|
1207
|
+
if (!roles.has(AGENT_ROLE)) return {
|
|
1208
|
+
valid: false,
|
|
1209
|
+
error: `run_mode="${runMode}" but form has no agent-role fields. Available roles: ${[...roles].join(", ") || "(none)"}`
|
|
1210
|
+
};
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
return { valid: true };
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Determine the run mode for a form.
|
|
1217
|
+
*
|
|
1218
|
+
* 1. If explicit run_mode in frontmatter, validate and use it
|
|
1219
|
+
* 2. Otherwise, infer from field roles:
|
|
1220
|
+
* - All user fields → interactive
|
|
1221
|
+
* - All agent fields → fill (or research if isResearchForm)
|
|
1222
|
+
* - Mixed roles → error (require explicit run_mode)
|
|
1223
|
+
*/
|
|
1224
|
+
function determineRunMode(form) {
|
|
1225
|
+
const explicitMode = form.metadata?.runMode;
|
|
1226
|
+
if (explicitMode) {
|
|
1227
|
+
const validation = validateRunMode(form, explicitMode);
|
|
1228
|
+
if (!validation.valid) return {
|
|
1229
|
+
success: false,
|
|
1230
|
+
error: validation.error
|
|
1231
|
+
};
|
|
1232
|
+
return {
|
|
1233
|
+
success: true,
|
|
1234
|
+
runMode: explicitMode,
|
|
1235
|
+
source: "explicit"
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
const roles = getFieldRoles(form);
|
|
1239
|
+
if (roles.size === 0) return {
|
|
1240
|
+
success: false,
|
|
1241
|
+
error: "Form has no fields"
|
|
1242
|
+
};
|
|
1243
|
+
if (roles.size === 1 && roles.has(USER_ROLE)) return {
|
|
1244
|
+
success: true,
|
|
1245
|
+
runMode: "interactive",
|
|
1246
|
+
source: "inferred"
|
|
1247
|
+
};
|
|
1248
|
+
if (roles.size === 1 && roles.has(AGENT_ROLE)) {
|
|
1249
|
+
if (isResearchForm(form)) return {
|
|
1250
|
+
success: true,
|
|
1251
|
+
runMode: "research",
|
|
1252
|
+
source: "inferred"
|
|
1253
|
+
};
|
|
1254
|
+
return {
|
|
1255
|
+
success: true,
|
|
1256
|
+
runMode: "fill",
|
|
1257
|
+
source: "inferred"
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
success: false,
|
|
1262
|
+
error: `Cannot determine run mode. Form has roles: ${[...roles].join(", ")}. Add 'run_mode' to frontmatter: interactive, fill, or research.`
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Get a human-readable description of the run mode source.
|
|
1267
|
+
*/
|
|
1268
|
+
function formatRunModeSource(source) {
|
|
1269
|
+
return source === "explicit" ? "from frontmatter" : "inferred from field roles";
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
//#endregion
|
|
1273
|
+
//#region src/cli/lib/versioning.ts
|
|
1274
|
+
/**
|
|
1275
|
+
* Versioned filename utilities for form output.
|
|
1276
|
+
*
|
|
1277
|
+
* Generates versioned filenames to avoid overwriting existing files.
|
|
1278
|
+
* Pattern: name.form.md → name-filled1.form.md → name-filled2.form.md
|
|
1279
|
+
*/
|
|
1280
|
+
/**
|
|
1281
|
+
* Version pattern that matches -filledN or _filledN before the extension.
|
|
1282
|
+
* Also matches legacy -vN pattern for backwards compatibility.
|
|
1283
|
+
*/
|
|
1284
|
+
const VERSION_PATTERN = /^(.+?)(?:[-_]?(?:filled|v)(\d+))?(\.form\.md)$/i;
|
|
1285
|
+
/**
|
|
1286
|
+
* Extension pattern for fallback matching.
|
|
1287
|
+
*/
|
|
1288
|
+
const EXTENSION_PATTERN = /^(.+)(\.form\.md)$/i;
|
|
1289
|
+
/**
|
|
1290
|
+
* Parse a versioned filename into its components.
|
|
1291
|
+
*
|
|
1292
|
+
* @param filePath - Path to parse
|
|
1293
|
+
* @returns Parsed components or null if not a valid form file
|
|
1294
|
+
*/
|
|
1295
|
+
function parseVersionedPath(filePath) {
|
|
1296
|
+
const match = VERSION_PATTERN.exec(filePath);
|
|
1297
|
+
if (match) {
|
|
1298
|
+
const base = match[1];
|
|
1299
|
+
const versionStr = match[2];
|
|
1300
|
+
const ext = match[3];
|
|
1301
|
+
if (base !== void 0 && ext !== void 0) return {
|
|
1302
|
+
base,
|
|
1303
|
+
version: versionStr ? parseInt(versionStr, 10) : null,
|
|
1304
|
+
extension: ext
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
const extMatch = EXTENSION_PATTERN.exec(filePath);
|
|
1308
|
+
if (extMatch) {
|
|
1309
|
+
const base = extMatch[1];
|
|
1310
|
+
const ext = extMatch[2];
|
|
1311
|
+
if (base !== void 0 && ext !== void 0) return {
|
|
1312
|
+
base,
|
|
815
1313
|
version: null,
|
|
816
1314
|
extension: ext
|
|
817
1315
|
};
|
|
@@ -1411,198 +1909,489 @@ function showInteractiveOutro(patchCount, cancelled) {
|
|
|
1411
1909
|
}
|
|
1412
1910
|
|
|
1413
1911
|
//#endregion
|
|
1414
|
-
//#region src/cli/lib/
|
|
1912
|
+
//#region src/cli/lib/patchFormat.ts
|
|
1913
|
+
/** Maximum characters for a patch value display before truncation */
|
|
1914
|
+
const PATCH_VALUE_MAX_LENGTH = 1e3;
|
|
1415
1915
|
/**
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
* Provides a modern console experience:
|
|
1419
|
-
* - Syntax highlighting for markdown and YAML
|
|
1420
|
-
* - Pagination using system pager (less) when available
|
|
1421
|
-
* - Fallback to console output when not interactive
|
|
1916
|
+
* Truncate a string to max length with ellipsis if needed.
|
|
1422
1917
|
*/
|
|
1918
|
+
function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
|
|
1919
|
+
if (value.length <= maxLength) return value;
|
|
1920
|
+
return value.slice(0, maxLength) + "…";
|
|
1921
|
+
}
|
|
1423
1922
|
/**
|
|
1424
|
-
*
|
|
1923
|
+
* Format a patch value for display with truncation.
|
|
1425
1924
|
*/
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1925
|
+
function formatPatchValue(patch) {
|
|
1926
|
+
switch (patch.op) {
|
|
1927
|
+
case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1928
|
+
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
1929
|
+
case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
1930
|
+
case "set_single_select": return patch.selected ?? "(none)";
|
|
1931
|
+
case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
|
|
1932
|
+
case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
|
|
1933
|
+
case "clear_field": return "(cleared)";
|
|
1934
|
+
case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
|
|
1935
|
+
case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
|
|
1936
|
+
case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1937
|
+
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
1938
|
+
case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1939
|
+
case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
1940
|
+
case "set_table": return patch.rows.length > 0 ? truncate(`[${patch.rows.length} rows]`) : "(empty)";
|
|
1941
|
+
case "add_note": return truncate(`note: ${patch.text}`);
|
|
1942
|
+
case "remove_note": return `(remove note ${patch.noteId})`;
|
|
1943
|
+
}
|
|
1428
1944
|
}
|
|
1429
1945
|
/**
|
|
1430
|
-
*
|
|
1431
|
-
* Colorizes headers, code blocks, and other elements.
|
|
1946
|
+
* Get a short display name for the patch operation type.
|
|
1432
1947
|
*/
|
|
1433
|
-
function
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (line.startsWith("## ")) {
|
|
1452
|
-
formatted.push(pc.bold(pc.blue(line)));
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
if (line.startsWith("### ")) {
|
|
1456
|
-
formatted.push(pc.bold(pc.cyan(line)));
|
|
1457
|
-
continue;
|
|
1458
|
-
}
|
|
1459
|
-
if (line.startsWith("#### ")) {
|
|
1460
|
-
formatted.push(pc.bold(line));
|
|
1461
|
-
continue;
|
|
1462
|
-
}
|
|
1463
|
-
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1464
|
-
return pc.yellow(code);
|
|
1465
|
-
});
|
|
1466
|
-
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
1467
|
-
return pc.bold(text);
|
|
1468
|
-
});
|
|
1469
|
-
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
1470
|
-
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
1471
|
-
});
|
|
1472
|
-
formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
|
|
1473
|
-
return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
|
|
1474
|
-
});
|
|
1475
|
-
formatted.push(formattedLine);
|
|
1948
|
+
function formatPatchType(patch) {
|
|
1949
|
+
switch (patch.op) {
|
|
1950
|
+
case "set_string": return "string";
|
|
1951
|
+
case "set_number": return "number";
|
|
1952
|
+
case "set_string_list": return "string_list";
|
|
1953
|
+
case "set_single_select": return "select";
|
|
1954
|
+
case "set_multi_select": return "multi_select";
|
|
1955
|
+
case "set_checkboxes": return "checkboxes";
|
|
1956
|
+
case "clear_field": return "clear";
|
|
1957
|
+
case "skip_field": return "skip";
|
|
1958
|
+
case "abort_field": return "abort";
|
|
1959
|
+
case "set_url": return "url";
|
|
1960
|
+
case "set_url_list": return "url_list";
|
|
1961
|
+
case "set_date": return "date";
|
|
1962
|
+
case "set_year": return "year";
|
|
1963
|
+
case "set_table": return "table";
|
|
1964
|
+
case "add_note": return "note";
|
|
1965
|
+
case "remove_note": return "remove_note";
|
|
1476
1966
|
}
|
|
1477
|
-
return formatted.join("\n");
|
|
1478
1967
|
}
|
|
1968
|
+
|
|
1969
|
+
//#endregion
|
|
1970
|
+
//#region src/cli/lib/fillLogging.ts
|
|
1479
1971
|
/**
|
|
1480
|
-
*
|
|
1972
|
+
* Fill Logging Callbacks - Create FillCallbacks for unified CLI logging.
|
|
1973
|
+
*
|
|
1974
|
+
* Provides consistent turn-by-turn logging across all CLI commands that
|
|
1975
|
+
* run form-filling (fill, run, examples). API consumers can also use
|
|
1976
|
+
* these callbacks or implement their own.
|
|
1977
|
+
*
|
|
1978
|
+
* Default output (always shown unless --quiet):
|
|
1979
|
+
* - Turn numbers with issues list (field IDs + issue types)
|
|
1980
|
+
* - Patches per turn (field ID + value)
|
|
1981
|
+
* - Completion status
|
|
1982
|
+
*
|
|
1983
|
+
* Verbose output (--verbose flag):
|
|
1984
|
+
* - Token counts per turn
|
|
1985
|
+
* - Tool call start/end with timing
|
|
1986
|
+
* - Detailed stats and LLM metadata
|
|
1481
1987
|
*/
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1988
|
+
/**
|
|
1989
|
+
* Create FillCallbacks that produce standard CLI logging output.
|
|
1990
|
+
*
|
|
1991
|
+
* Default output (always shown unless --quiet):
|
|
1992
|
+
* - Turn numbers with issues list (field IDs + issue types)
|
|
1993
|
+
* - Patches per turn (field ID + value)
|
|
1994
|
+
* - Completion status
|
|
1995
|
+
*
|
|
1996
|
+
* Verbose output (--verbose flag):
|
|
1997
|
+
* - Token counts per turn
|
|
1998
|
+
* - Tool call start/end with timing
|
|
1999
|
+
* - Detailed stats and LLM metadata
|
|
2000
|
+
*
|
|
2001
|
+
* This is used by fill, run, and examples commands for consistent output.
|
|
2002
|
+
*
|
|
2003
|
+
* @param ctx - Command context for verbose/quiet flags
|
|
2004
|
+
* @param options - Optional spinner for tool progress
|
|
2005
|
+
* @returns FillCallbacks with all logging implemented
|
|
2006
|
+
*
|
|
2007
|
+
* @example
|
|
2008
|
+
* ```typescript
|
|
2009
|
+
* const callbacks = createFillLoggingCallbacks(ctx, { spinner });
|
|
2010
|
+
* const result = await fillForm({
|
|
2011
|
+
* form: formMarkdown,
|
|
2012
|
+
* model: 'anthropic/claude-sonnet-4-5',
|
|
2013
|
+
* enableWebSearch: true,
|
|
2014
|
+
* callbacks,
|
|
2015
|
+
* });
|
|
2016
|
+
* ```
|
|
2017
|
+
*/
|
|
2018
|
+
function createFillLoggingCallbacks(ctx, options = {}) {
|
|
2019
|
+
return {
|
|
2020
|
+
onIssuesIdentified: ({ turnNumber, issues }) => {
|
|
2021
|
+
logInfo(ctx, `${pc.bold(`Turn ${turnNumber}:`)} ${formatTurnIssues(issues)}`);
|
|
2022
|
+
},
|
|
2023
|
+
onPatchesGenerated: ({ patches, stats }) => {
|
|
2024
|
+
logInfo(ctx, ` -> ${pc.yellow(String(patches.length))} patch(es):`);
|
|
2025
|
+
for (const patch of patches) {
|
|
2026
|
+
const typeName = formatPatchType(patch);
|
|
2027
|
+
const value = formatPatchValue(patch);
|
|
2028
|
+
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
2029
|
+
if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
2030
|
+
else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
2031
|
+
}
|
|
2032
|
+
if (stats && ctx.verbose) {
|
|
2033
|
+
logVerbose(ctx, ` Tokens: in=${stats.inputTokens ?? 0} out=${stats.outputTokens ?? 0}`);
|
|
2034
|
+
if (stats.toolCalls && stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
onTurnComplete: ({ isComplete }) => {
|
|
2038
|
+
if (isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
2039
|
+
},
|
|
2040
|
+
onToolStart: ({ name }) => {
|
|
2041
|
+
if (name.includes("search")) options.spinner?.message(`Web search...`);
|
|
2042
|
+
logVerbose(ctx, ` Tool started: ${name}`);
|
|
2043
|
+
},
|
|
2044
|
+
onToolEnd: ({ name, durationMs, error }) => {
|
|
2045
|
+
if (error) logVerbose(ctx, ` Tool ${name} failed: ${error} (${durationMs}ms)`);
|
|
2046
|
+
else logVerbose(ctx, ` Tool ${name} completed (${durationMs}ms)`);
|
|
2047
|
+
},
|
|
2048
|
+
onLlmCallStart: ({ model }) => {
|
|
2049
|
+
logVerbose(ctx, ` LLM call: ${model}`);
|
|
2050
|
+
},
|
|
2051
|
+
onLlmCallEnd: ({ model, inputTokens, outputTokens }) => {
|
|
2052
|
+
logVerbose(ctx, ` LLM response: ${model} (in=${inputTokens} out=${outputTokens})`);
|
|
1499
2053
|
}
|
|
1500
|
-
|
|
1501
|
-
}
|
|
1502
|
-
return formatted.join("\n");
|
|
2054
|
+
};
|
|
1503
2055
|
}
|
|
2056
|
+
|
|
2057
|
+
//#endregion
|
|
2058
|
+
//#region src/cli/commands/run.ts
|
|
1504
2059
|
/**
|
|
1505
|
-
*
|
|
2060
|
+
* Run command - Interactive launcher for running forms.
|
|
2061
|
+
*
|
|
2062
|
+
* Provides a menu-based interface for selecting and running forms
|
|
2063
|
+
* from the forms directory. Automatically detects run mode based
|
|
2064
|
+
* on frontmatter or field roles.
|
|
2065
|
+
*
|
|
2066
|
+
* Usage:
|
|
2067
|
+
* markform run # Browse forms, select, run
|
|
2068
|
+
* markform run movie.form.md # Run specific form directly
|
|
2069
|
+
* markform run --limit=50 # Override menu limit
|
|
1506
2070
|
*/
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2071
|
+
/**
|
|
2072
|
+
* Scan forms directory for .form.md files.
|
|
2073
|
+
*/
|
|
2074
|
+
function scanFormsDirectory(formsDir) {
|
|
2075
|
+
const entries = [];
|
|
2076
|
+
try {
|
|
2077
|
+
const files = readdirSync(formsDir);
|
|
2078
|
+
for (const file of files) {
|
|
2079
|
+
if (!file.endsWith(".form.md")) continue;
|
|
2080
|
+
const fullPath = join(formsDir, file);
|
|
2081
|
+
try {
|
|
2082
|
+
const stat = statSync(fullPath);
|
|
2083
|
+
if (stat.isFile()) entries.push({
|
|
2084
|
+
path: fullPath,
|
|
2085
|
+
filename: file,
|
|
2086
|
+
mtime: stat.mtime
|
|
2087
|
+
});
|
|
2088
|
+
} catch {}
|
|
2089
|
+
}
|
|
2090
|
+
} catch {}
|
|
2091
|
+
entries.sort((a, b) => {
|
|
2092
|
+
const orderDiff = getExampleOrder(a.filename) - getExampleOrder(b.filename);
|
|
2093
|
+
if (orderDiff !== 0) return orderDiff;
|
|
2094
|
+
return a.filename.localeCompare(b.filename);
|
|
2095
|
+
});
|
|
2096
|
+
return entries;
|
|
1511
2097
|
}
|
|
1512
2098
|
/**
|
|
1513
|
-
*
|
|
1514
|
-
* Falls back to console.log if not interactive or pager unavailable.
|
|
1515
|
-
*
|
|
1516
|
-
* @returns Promise that resolves when viewing is complete
|
|
2099
|
+
* Load form metadata for menu display.
|
|
1517
2100
|
*/
|
|
1518
|
-
async function
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2101
|
+
async function enrichFormEntry(entry) {
|
|
2102
|
+
try {
|
|
2103
|
+
const form = parseForm(await readFile$1(entry.path));
|
|
2104
|
+
const runModeResult = determineRunMode(form);
|
|
2105
|
+
return {
|
|
2106
|
+
...entry,
|
|
2107
|
+
title: form.schema.title,
|
|
2108
|
+
description: form.schema.description,
|
|
2109
|
+
runMode: runModeResult.success ? runModeResult.runMode : void 0
|
|
2110
|
+
};
|
|
2111
|
+
} catch {
|
|
2112
|
+
return entry;
|
|
1522
2113
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
console.log(header);
|
|
1538
|
-
console.log("");
|
|
1539
|
-
console.log(content);
|
|
1540
|
-
console.log("");
|
|
1541
|
-
resolve$1();
|
|
1542
|
-
});
|
|
1543
|
-
pager.on("close", () => {
|
|
1544
|
-
resolve$1();
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Build model options for the select prompt.
|
|
2117
|
+
*/
|
|
2118
|
+
function buildModelOptions(webSearchOnly) {
|
|
2119
|
+
const options = [];
|
|
2120
|
+
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
2121
|
+
if (webSearchOnly && !hasWebSearchSupport(provider)) continue;
|
|
2122
|
+
const info = getProviderInfo(provider);
|
|
2123
|
+
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
|
|
2124
|
+
for (const model of models) options.push({
|
|
2125
|
+
value: `${provider}/${model}`,
|
|
2126
|
+
label: `${provider}/${model}`,
|
|
2127
|
+
hint: `${keyStatus} ${info.envVar}`
|
|
1545
2128
|
});
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2129
|
+
}
|
|
2130
|
+
options.push({
|
|
2131
|
+
value: "custom",
|
|
2132
|
+
label: "Enter custom model ID...",
|
|
2133
|
+
hint: "provider/model-id format"
|
|
1549
2134
|
});
|
|
2135
|
+
return options;
|
|
1550
2136
|
}
|
|
1551
2137
|
/**
|
|
1552
|
-
*
|
|
2138
|
+
* Prompt user to select a model.
|
|
1553
2139
|
*/
|
|
1554
|
-
async function
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
2140
|
+
async function promptForModel(webSearchRequired) {
|
|
2141
|
+
const modelOptions = buildModelOptions(webSearchRequired);
|
|
2142
|
+
if (webSearchRequired && modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
|
|
2143
|
+
const message = webSearchRequired ? "Select LLM model (web search required):" : "Select LLM model:";
|
|
2144
|
+
const selection = await p.select({
|
|
2145
|
+
message,
|
|
2146
|
+
options: modelOptions
|
|
2147
|
+
});
|
|
2148
|
+
if (p.isCancel(selection)) return null;
|
|
2149
|
+
if (selection === "custom") {
|
|
2150
|
+
const customModel = await p.text({
|
|
2151
|
+
message: "Model ID (provider/model-id):",
|
|
2152
|
+
placeholder: "anthropic/claude-sonnet-4-20250514",
|
|
2153
|
+
validate: (value) => {
|
|
2154
|
+
if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
if (p.isCancel(customModel)) return null;
|
|
2158
|
+
return customModel;
|
|
2159
|
+
}
|
|
2160
|
+
return selection;
|
|
1558
2161
|
}
|
|
1559
2162
|
/**
|
|
1560
|
-
*
|
|
1561
|
-
*
|
|
1562
|
-
* Presents a list of files to view:
|
|
1563
|
-
* - "Show report:" for the report output (.report.md) at the top
|
|
1564
|
-
* - "Show source:" for other files (.form.md, .raw.md, .yml)
|
|
1565
|
-
* - "Quit" at the bottom
|
|
1566
|
-
*
|
|
1567
|
-
* Loops until the user selects Quit.
|
|
1568
|
-
*
|
|
1569
|
-
* @param files Array of file options to display
|
|
2163
|
+
* Collect user input interactively (without exporting).
|
|
2164
|
+
* Returns true if successful, false if cancelled.
|
|
1570
2165
|
*/
|
|
1571
|
-
async function
|
|
1572
|
-
|
|
2166
|
+
async function collectUserInput(form) {
|
|
2167
|
+
const targetRoles = [USER_ROLE];
|
|
2168
|
+
const inspectResult = inspect(form, { targetRoles });
|
|
2169
|
+
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
2170
|
+
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
2171
|
+
if (uniqueFieldIds.size === 0) return true;
|
|
2172
|
+
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
2173
|
+
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
2174
|
+
if (cancelled) {
|
|
2175
|
+
showInteractiveOutro(0, true);
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2178
|
+
if (patches.length > 0) applyPatches(form, patches);
|
|
2179
|
+
showInteractiveOutro(patches.length, false);
|
|
2180
|
+
return true;
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Run interactive fill workflow.
|
|
2184
|
+
* @returns ExportResult with paths to output files, or undefined if cancelled/no fields
|
|
2185
|
+
*/
|
|
2186
|
+
async function runInteractiveWorkflow(form, filePath, formsDir) {
|
|
2187
|
+
const startTime = Date.now();
|
|
2188
|
+
const targetRoles = [USER_ROLE];
|
|
2189
|
+
const inspectResult = inspect(form, { targetRoles });
|
|
2190
|
+
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
2191
|
+
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
2192
|
+
if (uniqueFieldIds.size === 0) {
|
|
2193
|
+
p.log.info("No user-role fields to fill.");
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
2197
|
+
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
2198
|
+
if (cancelled) {
|
|
2199
|
+
showInteractiveOutro(0, true);
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
if (patches.length > 0) applyPatches(form, patches);
|
|
2203
|
+
await ensureFormsDir(formsDir);
|
|
2204
|
+
const exportResult = await exportMultiFormat(form, generateVersionedPathInFormsDir(filePath, formsDir));
|
|
2205
|
+
showInteractiveOutro(patches.length, false);
|
|
1573
2206
|
console.log("");
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2207
|
+
p.log.success("Outputs:");
|
|
2208
|
+
console.log(` ${formatPath(exportResult.reportPath)} ${pc.dim("(output report)")}`);
|
|
2209
|
+
console.log(` ${formatPath(exportResult.yamlPath)} ${pc.dim("(output values)")}`);
|
|
2210
|
+
console.log(` ${formatPath(exportResult.formPath)} ${pc.dim("(filled markform source)")}`);
|
|
2211
|
+
console.log(` ${formatPath(exportResult.schemaPath)} ${pc.dim("(JSON Schema)")}`);
|
|
2212
|
+
logTiming({
|
|
2213
|
+
verbose: false,
|
|
2214
|
+
format: "console",
|
|
2215
|
+
dryRun: false,
|
|
2216
|
+
quiet: false,
|
|
2217
|
+
overwrite: false
|
|
2218
|
+
}, "Fill time", Date.now() - startTime);
|
|
2219
|
+
return exportResult;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Run agent fill workflow using fillForm with logging callbacks.
|
|
2223
|
+
* @returns ExportResult with paths to output files
|
|
2224
|
+
*/
|
|
2225
|
+
async function runAgentFillWorkflow(form, modelId, formsDir, filePath, isResearch, overwrite, ctx) {
|
|
2226
|
+
const startTime = Date.now();
|
|
2227
|
+
const maxTurns = DEFAULT_MAX_TURNS;
|
|
2228
|
+
const maxPatchesPerTurn = isResearch ? DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN : DEFAULT_MAX_PATCHES_PER_TURN;
|
|
2229
|
+
const maxIssuesPerTurn = isResearch ? DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN : DEFAULT_MAX_ISSUES_PER_TURN;
|
|
2230
|
+
logVerbose(ctx, `Config: max_turns=${maxTurns}, max_issues_per_turn=${maxIssuesPerTurn}, max_patches_per_turn=${maxPatchesPerTurn}`);
|
|
2231
|
+
const callbacks = createFillLoggingCallbacks(ctx);
|
|
2232
|
+
const workflowLabel = isResearch ? "Research" : "Agent fill";
|
|
2233
|
+
p.log.step(pc.bold(`${workflowLabel} in progress...`));
|
|
2234
|
+
const result = await fillForm({
|
|
2235
|
+
form,
|
|
2236
|
+
model: modelId,
|
|
2237
|
+
maxTurns,
|
|
2238
|
+
maxPatchesPerTurn,
|
|
2239
|
+
maxIssuesPerTurn,
|
|
2240
|
+
targetRoles: [AGENT_ROLE],
|
|
2241
|
+
fillMode: overwrite ? "overwrite" : "continue",
|
|
2242
|
+
enableWebSearch: isResearch,
|
|
2243
|
+
callbacks
|
|
2244
|
+
});
|
|
2245
|
+
if (result.status.ok) p.log.success(pc.green(`Form completed in ${result.turns} turn(s)`));
|
|
2246
|
+
else if (result.status.reason === "max_turns") p.log.warn(pc.yellow(`Max turns reached (${maxTurns})`));
|
|
2247
|
+
else throw new Error(result.status.message ?? `Fill failed: ${result.status.reason}`);
|
|
2248
|
+
await ensureFormsDir(formsDir);
|
|
2249
|
+
const outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
|
|
2250
|
+
const exportResult = await exportMultiFormat(result.form, outputPath);
|
|
2251
|
+
console.log("");
|
|
2252
|
+
p.log.success(`${workflowLabel} complete. Outputs:`);
|
|
2253
|
+
console.log(` ${formatPath(exportResult.reportPath)} ${pc.dim("(output report)")}`);
|
|
2254
|
+
console.log(` ${formatPath(exportResult.yamlPath)} ${pc.dim("(output values)")}`);
|
|
2255
|
+
console.log(` ${formatPath(exportResult.formPath)} ${pc.dim("(filled markform source)")}`);
|
|
2256
|
+
console.log(` ${formatPath(exportResult.schemaPath)} ${pc.dim("(JSON Schema)")}`);
|
|
2257
|
+
logTiming(ctx, isResearch ? "Research time" : "Fill time", Date.now() - startTime);
|
|
2258
|
+
return exportResult;
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Run a form directly (callable from other commands).
|
|
2262
|
+
* This executes the same workflow as `markform run <file>`.
|
|
2263
|
+
*
|
|
2264
|
+
* @param selectedPath - Path to the form file
|
|
2265
|
+
* @param formsDir - Directory for output files
|
|
2266
|
+
* @param overwrite - Whether to overwrite existing field values
|
|
2267
|
+
* @param ctx - Optional command context for logging (defaults to non-verbose/quiet)
|
|
2268
|
+
* @returns ExportResult with paths to output files, or undefined if cancelled/no output
|
|
2269
|
+
*/
|
|
2270
|
+
async function runForm(selectedPath, formsDir, overwrite, ctx) {
|
|
2271
|
+
const effectiveCtx = ctx ?? {
|
|
2272
|
+
verbose: false,
|
|
2273
|
+
quiet: false,
|
|
2274
|
+
dryRun: false,
|
|
2275
|
+
format: "console",
|
|
2276
|
+
overwrite
|
|
2277
|
+
};
|
|
2278
|
+
const form = parseForm(await readFile$1(selectedPath));
|
|
2279
|
+
const runModeResult = determineRunMode(form);
|
|
2280
|
+
if (!runModeResult.success) throw new Error(runModeResult.error);
|
|
2281
|
+
const { runMode } = runModeResult;
|
|
2282
|
+
switch (runMode) {
|
|
2283
|
+
case "interactive": return runInteractiveWorkflow(form, selectedPath, formsDir);
|
|
2284
|
+
case "fill":
|
|
2285
|
+
case "research": {
|
|
2286
|
+
const isResearch = runMode === "research";
|
|
2287
|
+
if (!await collectUserInput(form)) {
|
|
2288
|
+
p.cancel("Cancelled.");
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const modelId = await promptForModel(isResearch);
|
|
2292
|
+
if (!modelId) {
|
|
2293
|
+
p.cancel("Cancelled.");
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
return runAgentFillWorkflow(form, modelId, formsDir, selectedPath, isResearch, overwrite, effectiveCtx);
|
|
2297
|
+
}
|
|
1600
2298
|
}
|
|
1601
2299
|
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Register the run command.
|
|
2302
|
+
*/
|
|
2303
|
+
function registerRunCommand(program) {
|
|
2304
|
+
program.command("run [file]").description("Browse and run forms from the forms directory").option("--limit <n>", `Maximum forms to show in menu (default: ${MAX_FORMS_IN_MENU})`, String(MAX_FORMS_IN_MENU)).action(async (file, options, cmd) => {
|
|
2305
|
+
const ctx = getCommandContext(cmd);
|
|
2306
|
+
try {
|
|
2307
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2308
|
+
const limit = options.limit ? parseInt(options.limit, 10) : MAX_FORMS_IN_MENU;
|
|
2309
|
+
let selectedPath;
|
|
2310
|
+
if (file) {
|
|
2311
|
+
selectedPath = file.startsWith("/") ? file : join(formsDir, file);
|
|
2312
|
+
if (!selectedPath.endsWith(".form.md") && !selectedPath.endsWith(".md")) selectedPath = `${selectedPath}.form.md`;
|
|
2313
|
+
} else {
|
|
2314
|
+
p.intro(pc.bgCyan(pc.black(" markform run ")));
|
|
2315
|
+
const entries = scanFormsDirectory(formsDir);
|
|
2316
|
+
if (entries.length === 0) {
|
|
2317
|
+
p.log.warn(`No forms found in ${formatPath(formsDir)}`);
|
|
2318
|
+
console.log("");
|
|
2319
|
+
console.log(`Run ${pc.cyan("'markform examples'")} to get started.`);
|
|
2320
|
+
p.outro("");
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const entriesToShow = entries.slice(0, limit);
|
|
2324
|
+
const enrichedEntries = await Promise.all(entriesToShow.map(enrichFormEntry));
|
|
2325
|
+
const menuOptions = enrichedEntries.map((entry) => ({
|
|
2326
|
+
value: entry.path,
|
|
2327
|
+
label: formatFormLabel(entry),
|
|
2328
|
+
hint: formatFormHint(entry)
|
|
2329
|
+
}));
|
|
2330
|
+
const defaultExample = getExampleById(DEFAULT_EXAMPLE_ID);
|
|
2331
|
+
const initialValue = enrichedEntries.find((e) => e.filename === defaultExample?.filename)?.path;
|
|
2332
|
+
if (entries.length > limit) console.log(pc.dim(`Showing ${limit} of ${entries.length} forms`));
|
|
2333
|
+
const selection = await p.select({
|
|
2334
|
+
message: "Select a form to run:",
|
|
2335
|
+
options: menuOptions,
|
|
2336
|
+
initialValue
|
|
2337
|
+
});
|
|
2338
|
+
if (p.isCancel(selection)) {
|
|
2339
|
+
p.cancel("Cancelled.");
|
|
2340
|
+
process.exit(0);
|
|
2341
|
+
}
|
|
2342
|
+
selectedPath = selection;
|
|
2343
|
+
}
|
|
2344
|
+
logVerbose(ctx, `Reading form: ${selectedPath}`);
|
|
2345
|
+
const form = parseForm(await readFile$1(selectedPath));
|
|
2346
|
+
const runModeResult = determineRunMode(form);
|
|
2347
|
+
if (!runModeResult.success) {
|
|
2348
|
+
logError(runModeResult.error);
|
|
2349
|
+
process.exit(1);
|
|
2350
|
+
}
|
|
2351
|
+
const { runMode, source } = runModeResult;
|
|
2352
|
+
logInfo(ctx, `Run mode: ${runMode} (${formatRunModeSource(source)})`);
|
|
2353
|
+
switch (runMode) {
|
|
2354
|
+
case "interactive":
|
|
2355
|
+
await runInteractiveWorkflow(form, selectedPath, formsDir);
|
|
2356
|
+
break;
|
|
2357
|
+
case "fill":
|
|
2358
|
+
case "research": {
|
|
2359
|
+
const isResearch = runMode === "research";
|
|
2360
|
+
if (!await collectUserInput(form)) {
|
|
2361
|
+
p.cancel("Cancelled.");
|
|
2362
|
+
process.exit(0);
|
|
2363
|
+
}
|
|
2364
|
+
const modelId = await promptForModel(isResearch);
|
|
2365
|
+
if (!modelId) {
|
|
2366
|
+
p.cancel("Cancelled.");
|
|
2367
|
+
process.exit(0);
|
|
2368
|
+
}
|
|
2369
|
+
await runAgentFillWorkflow(form, modelId, formsDir, selectedPath, isResearch, ctx.overwrite, ctx);
|
|
2370
|
+
break;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
if (!file) p.outro("Happy form filling!");
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2376
|
+
process.exit(1);
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
1602
2380
|
|
|
1603
2381
|
//#endregion
|
|
1604
2382
|
//#region src/cli/commands/examples.ts
|
|
1605
2383
|
/**
|
|
2384
|
+
* Examples command - Copy bundled example forms to the forms directory.
|
|
2385
|
+
*
|
|
2386
|
+
* This command provides a simple way to get started with markform by
|
|
2387
|
+
* copying bundled example forms to your local forms directory.
|
|
2388
|
+
*
|
|
2389
|
+
* Usage:
|
|
2390
|
+
* markform examples # Copy all bundled examples to ./forms/
|
|
2391
|
+
* markform examples --list # List bundled examples (no copy)
|
|
2392
|
+
* markform examples --name=foo # Copy specific example only
|
|
2393
|
+
*/
|
|
2394
|
+
/**
|
|
1606
2395
|
* Print non-interactive list of examples.
|
|
1607
2396
|
*/
|
|
1608
2397
|
function printExamplesList() {
|
|
@@ -1618,409 +2407,149 @@ function printExamplesList() {
|
|
|
1618
2407
|
}
|
|
1619
2408
|
}
|
|
1620
2409
|
/**
|
|
1621
|
-
*
|
|
2410
|
+
* Copy an example form to the forms directory.
|
|
2411
|
+
*
|
|
2412
|
+
* @returns true if copied, false if skipped
|
|
1622
2413
|
*/
|
|
1623
|
-
function
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
2414
|
+
async function copyExample(exampleId, formsDir, overwrite, _quiet) {
|
|
2415
|
+
const example = getExampleById(exampleId);
|
|
2416
|
+
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
2417
|
+
const outputPath = join(formsDir, example.filename);
|
|
2418
|
+
if (existsSync(outputPath)) {
|
|
2419
|
+
if (!overwrite) return {
|
|
2420
|
+
copied: false,
|
|
2421
|
+
skipped: true,
|
|
2422
|
+
path: outputPath
|
|
2423
|
+
};
|
|
1631
2424
|
}
|
|
1632
|
-
|
|
2425
|
+
await writeFile(outputPath, loadExampleContent(exampleId));
|
|
2426
|
+
return {
|
|
2427
|
+
copied: true,
|
|
2428
|
+
skipped: false,
|
|
2429
|
+
path: outputPath
|
|
2430
|
+
};
|
|
1633
2431
|
}
|
|
1634
2432
|
/**
|
|
1635
|
-
*
|
|
2433
|
+
* Scan forms directory and enrich entries with metadata.
|
|
1636
2434
|
*/
|
|
1637
|
-
function
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
const
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2435
|
+
async function getFormEntries(formsDir) {
|
|
2436
|
+
const entries = [];
|
|
2437
|
+
try {
|
|
2438
|
+
const files = readdirSync(formsDir);
|
|
2439
|
+
for (const file of files) {
|
|
2440
|
+
if (!file.endsWith(".form.md")) continue;
|
|
2441
|
+
const fullPath = join(formsDir, file);
|
|
2442
|
+
try {
|
|
2443
|
+
if (statSync(fullPath).isFile()) {
|
|
2444
|
+
const form = parseForm(await readFile$1(fullPath));
|
|
2445
|
+
const runModeResult = determineRunMode(form);
|
|
2446
|
+
entries.push({
|
|
2447
|
+
path: fullPath,
|
|
2448
|
+
filename: file,
|
|
2449
|
+
title: form.schema.title,
|
|
2450
|
+
description: form.schema.description,
|
|
2451
|
+
runMode: runModeResult.success ? runModeResult.runMode : void 0
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
} catch {}
|
|
2455
|
+
}
|
|
2456
|
+
} catch {}
|
|
2457
|
+
return entries;
|
|
1654
2458
|
}
|
|
1655
2459
|
/**
|
|
1656
|
-
*
|
|
2460
|
+
* Copy all examples to the forms directory.
|
|
2461
|
+
* Returns { copied, skipped } counts for the caller to handle prompts.
|
|
1657
2462
|
*/
|
|
1658
|
-
async function
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
if (p.isCancel(selection)) return null;
|
|
1665
|
-
if (selection === "custom") {
|
|
1666
|
-
const customModel = await p.text({
|
|
1667
|
-
message: "Model ID (provider/model-id):",
|
|
1668
|
-
placeholder: "anthropic/claude-sonnet-4-20250514",
|
|
1669
|
-
validate: (value) => {
|
|
1670
|
-
if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
|
|
1671
|
-
}
|
|
1672
|
-
});
|
|
1673
|
-
if (p.isCancel(customModel)) return null;
|
|
1674
|
-
return customModel;
|
|
2463
|
+
async function copyAllExamples(formsDir, overwrite, quiet) {
|
|
2464
|
+
const examples = getAllExamplesWithMetadata();
|
|
2465
|
+
const total = examples.length;
|
|
2466
|
+
if (!quiet) {
|
|
2467
|
+
console.log(`Copying ${total} example forms to ${formatPath(formsDir)}...`);
|
|
2468
|
+
console.log("");
|
|
1675
2469
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
for (const model of models) options.push({
|
|
1688
|
-
value: `${provider}/${model}`,
|
|
1689
|
-
label: `${provider}/${model}`,
|
|
1690
|
-
hint: `${keyStatus} ${info.envVar}`
|
|
1691
|
-
});
|
|
2470
|
+
let copied = 0;
|
|
2471
|
+
let skipped = 0;
|
|
2472
|
+
for (const example of examples) {
|
|
2473
|
+
const result = await copyExample(example.id, formsDir, overwrite, quiet);
|
|
2474
|
+
if (result.copied) {
|
|
2475
|
+
copied++;
|
|
2476
|
+
if (!quiet) console.log(formatFormLogLine(example, ` ${pc.green("✓")}`));
|
|
2477
|
+
} else if (result.skipped) {
|
|
2478
|
+
skipped++;
|
|
2479
|
+
if (!quiet) console.log(`${formatFormLogLine(example, ` ${pc.yellow("○")}`)} ${pc.dim("(exists, skipped)")}`);
|
|
2480
|
+
}
|
|
1692
2481
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
return
|
|
2482
|
+
if (!quiet) {
|
|
2483
|
+
console.log("");
|
|
2484
|
+
if (skipped > 0) console.log(pc.yellow(`Skipped ${skipped} existing file(s). Use --overwrite to replace them.`));
|
|
2485
|
+
console.log(pc.green(`Done! Copied ${copied} example form(s) to ${formatPath(formsDir)}`));
|
|
2486
|
+
}
|
|
2487
|
+
return {
|
|
2488
|
+
copied,
|
|
2489
|
+
skipped
|
|
2490
|
+
};
|
|
1699
2491
|
}
|
|
1700
2492
|
/**
|
|
1701
|
-
*
|
|
2493
|
+
* Show form selection menu and return the selected path.
|
|
1702
2494
|
*/
|
|
1703
|
-
async function
|
|
1704
|
-
const
|
|
1705
|
-
if (
|
|
2495
|
+
async function showFormMenu(formsDir) {
|
|
2496
|
+
const entries = await getFormEntries(formsDir);
|
|
2497
|
+
if (entries.length === 0) return null;
|
|
2498
|
+
const sortedEntries = [...entries].sort((a, b) => {
|
|
2499
|
+
return getExampleOrder(a.filename) - getExampleOrder(b.filename);
|
|
2500
|
+
});
|
|
2501
|
+
const defaultExample = getExampleById(DEFAULT_EXAMPLE_ID);
|
|
2502
|
+
const defaultIndex = sortedEntries.findIndex((e) => e.filename === defaultExample?.filename);
|
|
2503
|
+
const menuOptions = sortedEntries.map((entry) => ({
|
|
2504
|
+
value: entry.path,
|
|
2505
|
+
label: formatFormLabel(entry),
|
|
2506
|
+
hint: formatFormHint(entry)
|
|
2507
|
+
}));
|
|
2508
|
+
const initialValue = (defaultIndex >= 0 ? sortedEntries[defaultIndex] : void 0)?.path;
|
|
1706
2509
|
const selection = await p.select({
|
|
1707
|
-
message: "Select
|
|
1708
|
-
options:
|
|
2510
|
+
message: "Select a form to run:",
|
|
2511
|
+
options: menuOptions,
|
|
2512
|
+
initialValue
|
|
1709
2513
|
});
|
|
1710
2514
|
if (p.isCancel(selection)) return null;
|
|
1711
|
-
if (selection === "custom") {
|
|
1712
|
-
const customModel = await p.text({
|
|
1713
|
-
message: "Model ID (provider/model-id):",
|
|
1714
|
-
placeholder: "openai/gpt-5-mini",
|
|
1715
|
-
validate: (value) => {
|
|
1716
|
-
if (!value.includes("/")) return "Format: provider/model-id (e.g., openai/gpt-5-mini)";
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1719
|
-
if (p.isCancel(customModel)) return null;
|
|
1720
|
-
return customModel;
|
|
1721
|
-
}
|
|
1722
2515
|
return selection;
|
|
1723
2516
|
}
|
|
1724
2517
|
/**
|
|
1725
|
-
*
|
|
1726
|
-
* Accepts optional harness config overrides - research uses different defaults.
|
|
1727
|
-
*/
|
|
1728
|
-
async function runAgentFill(form, modelId, _outputPath, configOverrides) {
|
|
1729
|
-
const { provider: providerName, model: modelName } = parseModelIdForDisplay(modelId);
|
|
1730
|
-
const resolveSpinner = createSpinner({
|
|
1731
|
-
type: "compute",
|
|
1732
|
-
operation: `Resolving model: ${modelId}`
|
|
1733
|
-
});
|
|
1734
|
-
let model, provider;
|
|
1735
|
-
try {
|
|
1736
|
-
const result = await resolveModel(modelId);
|
|
1737
|
-
model = result.model;
|
|
1738
|
-
provider = result.provider;
|
|
1739
|
-
resolveSpinner.stop(`✓ Model resolved: ${modelId}`);
|
|
1740
|
-
} catch (error) {
|
|
1741
|
-
resolveSpinner.error("Model resolution failed");
|
|
1742
|
-
throw error;
|
|
1743
|
-
}
|
|
1744
|
-
const harnessConfig = {
|
|
1745
|
-
maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
1746
|
-
maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
|
|
1747
|
-
maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
|
|
1748
|
-
targetRoles: [AGENT_ROLE],
|
|
1749
|
-
fillMode: "continue"
|
|
1750
|
-
};
|
|
1751
|
-
console.log("");
|
|
1752
|
-
console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
|
|
1753
|
-
const harness = createHarness(form, harnessConfig);
|
|
1754
|
-
const agent = createLiveAgent({
|
|
1755
|
-
model,
|
|
1756
|
-
provider,
|
|
1757
|
-
targetRole: AGENT_ROLE,
|
|
1758
|
-
enableWebSearch: true
|
|
1759
|
-
});
|
|
1760
|
-
p.log.step(pc.bold("Agent fill in progress..."));
|
|
1761
|
-
let stepResult = harness.step();
|
|
1762
|
-
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1763
|
-
console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1764
|
-
const llmSpinner = createSpinner({
|
|
1765
|
-
type: "api",
|
|
1766
|
-
provider: providerName,
|
|
1767
|
-
model: modelName,
|
|
1768
|
-
turnNumber: stepResult.turnNumber
|
|
1769
|
-
});
|
|
1770
|
-
let response;
|
|
1771
|
-
try {
|
|
1772
|
-
response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1773
|
-
llmSpinner.stop();
|
|
1774
|
-
} catch (error) {
|
|
1775
|
-
llmSpinner.error("LLM call failed");
|
|
1776
|
-
throw error;
|
|
1777
|
-
}
|
|
1778
|
-
const { patches, stats } = response;
|
|
1779
|
-
for (const patch of patches) {
|
|
1780
|
-
const typeName = formatPatchType(patch);
|
|
1781
|
-
const value = formatPatchValue(patch);
|
|
1782
|
-
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
1783
|
-
if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
|
|
1784
|
-
else console.log(` (${typeName}) = ${pc.green(value)}`);
|
|
1785
|
-
}
|
|
1786
|
-
stepResult = harness.apply(patches, stepResult.issues);
|
|
1787
|
-
const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
|
|
1788
|
-
console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
|
|
1789
|
-
if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
|
|
1790
|
-
}
|
|
1791
|
-
if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
|
|
1792
|
-
else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
|
|
1793
|
-
Object.assign(form, harness.getForm());
|
|
1794
|
-
return {
|
|
1795
|
-
success: stepResult.isComplete,
|
|
1796
|
-
turnCount: harness.getTurnNumber()
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
/**
|
|
1800
|
-
* Run the interactive example scaffolding and filling flow.
|
|
1801
|
-
*
|
|
1802
|
-
* @param preselectedId Optional example ID to pre-select
|
|
1803
|
-
* @param formsDirOverride Optional forms directory override from CLI option
|
|
2518
|
+
* Copy a specific example to the forms directory.
|
|
1804
2519
|
*/
|
|
1805
|
-
async function
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
await
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
const examples = getAllExamplesWithMetadata();
|
|
1814
|
-
const selection = await p.select({
|
|
1815
|
-
message: "Select an example form to scaffold:",
|
|
1816
|
-
options: examples.map((example$1) => ({
|
|
1817
|
-
value: example$1.id,
|
|
1818
|
-
label: example$1.title ?? example$1.id,
|
|
1819
|
-
hint: example$1.description
|
|
1820
|
-
}))
|
|
1821
|
-
});
|
|
1822
|
-
if (p.isCancel(selection)) {
|
|
1823
|
-
p.cancel("Cancelled.");
|
|
1824
|
-
process.exit(0);
|
|
1825
|
-
}
|
|
1826
|
-
selectedId = selection;
|
|
1827
|
-
}
|
|
1828
|
-
const example = getExampleById(selectedId);
|
|
1829
|
-
if (!example) {
|
|
1830
|
-
p.cancel(`Unknown example: ${selectedId}`);
|
|
1831
|
-
process.exit(1);
|
|
1832
|
-
}
|
|
1833
|
-
const defaultFilename = basename(generateVersionedPathInFormsDir(example.filename, formsDir));
|
|
1834
|
-
const filenameResult = await p.text({
|
|
1835
|
-
message: `Output filename (in ${formatPath(formsDir)}):`,
|
|
1836
|
-
initialValue: defaultFilename,
|
|
1837
|
-
validate: (value) => {
|
|
1838
|
-
if (!value.trim()) return "Filename is required";
|
|
1839
|
-
if (!value.endsWith(".form.md") && !value.endsWith(".md")) return "Filename should end with .form.md or .md";
|
|
1840
|
-
}
|
|
1841
|
-
});
|
|
1842
|
-
if (p.isCancel(filenameResult)) {
|
|
1843
|
-
p.cancel("Cancelled.");
|
|
1844
|
-
process.exit(0);
|
|
1845
|
-
}
|
|
1846
|
-
const filename = filenameResult;
|
|
1847
|
-
const outputPath = join(formsDir, filename);
|
|
1848
|
-
if (existsSync(outputPath)) {
|
|
1849
|
-
const overwrite = await p.confirm({
|
|
1850
|
-
message: `${filename} already exists. Overwrite?`,
|
|
1851
|
-
initialValue: false
|
|
1852
|
-
});
|
|
1853
|
-
if (p.isCancel(overwrite) || !overwrite) {
|
|
1854
|
-
p.cancel("Cancelled.");
|
|
1855
|
-
process.exit(0);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
let content;
|
|
1859
|
-
try {
|
|
1860
|
-
content = loadExampleContent(selectedId);
|
|
1861
|
-
await writeFile(outputPath, content);
|
|
1862
|
-
} catch (error) {
|
|
1863
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1864
|
-
p.cancel(`Failed to write file: ${message}`);
|
|
1865
|
-
process.exit(1);
|
|
1866
|
-
}
|
|
1867
|
-
p.log.success(`Created ${formatPath(outputPath)}`);
|
|
1868
|
-
const form = parseForm(content);
|
|
1869
|
-
const targetRoles = [USER_ROLE];
|
|
1870
|
-
let userFillOutputs = null;
|
|
1871
|
-
const inspectResult = inspect(form, { targetRoles });
|
|
1872
|
-
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
1873
|
-
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
1874
|
-
if (uniqueFieldIds.size === 0) {
|
|
1875
|
-
p.log.info("No user-role fields to fill in this example.");
|
|
1876
|
-
if (inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field").length === 0) {
|
|
1877
|
-
logTiming({
|
|
1878
|
-
verbose: false,
|
|
1879
|
-
format: "console",
|
|
1880
|
-
dryRun: false,
|
|
1881
|
-
quiet: false
|
|
1882
|
-
}, "Total time", Date.now() - startTime);
|
|
1883
|
-
p.outro("Form scaffolded with no fields to fill.");
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
|
-
} else {
|
|
1887
|
-
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
1888
|
-
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
1889
|
-
if (cancelled) {
|
|
1890
|
-
showInteractiveOutro(0, true);
|
|
1891
|
-
process.exit(1);
|
|
1892
|
-
}
|
|
1893
|
-
if (patches.length > 0) applyPatches(form, patches);
|
|
1894
|
-
userFillOutputs = await exportMultiFormat(form, outputPath);
|
|
1895
|
-
showInteractiveOutro(patches.length, false);
|
|
1896
|
-
console.log("");
|
|
1897
|
-
p.log.success("Outputs:");
|
|
1898
|
-
console.log(` ${formatPath(userFillOutputs.reportPath)} ${pc.dim("(output report)")}`);
|
|
1899
|
-
console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(output values)")}`);
|
|
1900
|
-
console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(filled markform source)")}`);
|
|
1901
|
-
logTiming({
|
|
1902
|
-
verbose: false,
|
|
1903
|
-
format: "console",
|
|
1904
|
-
dryRun: false,
|
|
1905
|
-
quiet: false
|
|
1906
|
-
}, "Fill time", Date.now() - startTime);
|
|
1907
|
-
}
|
|
1908
|
-
const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
|
|
1909
|
-
const isResearchExample = example.type === "research";
|
|
1910
|
-
if (agentFieldIssues.length > 0) {
|
|
1911
|
-
console.log("");
|
|
1912
|
-
const workflowLabel = isResearchExample ? "research" : "agent fill";
|
|
1913
|
-
p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
|
|
1914
|
-
const confirmMessage = isResearchExample ? "Run research now? (requires web search)" : "Run agent fill now?";
|
|
1915
|
-
const runAgent = await p.confirm({
|
|
1916
|
-
message: confirmMessage,
|
|
1917
|
-
initialValue: true
|
|
1918
|
-
});
|
|
1919
|
-
if (p.isCancel(runAgent) || !runAgent) {
|
|
2520
|
+
async function copySingleExample(exampleId, formsDir, overwrite, quiet) {
|
|
2521
|
+
const example = getExampleById(exampleId);
|
|
2522
|
+
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
2523
|
+
if (!quiet) console.log(`Copying ${example.filename} to ${formatPath(formsDir)}...`);
|
|
2524
|
+
const result = await copyExample(exampleId, formsDir, overwrite, quiet);
|
|
2525
|
+
if (result.copied) {
|
|
2526
|
+
if (!quiet) {
|
|
2527
|
+
console.log(formatFormLogLine(example, ` ${pc.green("✓")}`));
|
|
1920
2528
|
console.log("");
|
|
1921
|
-
|
|
1922
|
-
console.log(`
|
|
1923
|
-
console.log(cliCommand);
|
|
1924
|
-
if (userFillOutputs) await showFileViewerChooser([
|
|
1925
|
-
{
|
|
1926
|
-
path: userFillOutputs.reportPath,
|
|
1927
|
-
label: "Report",
|
|
1928
|
-
hint: "output report"
|
|
1929
|
-
},
|
|
1930
|
-
{
|
|
1931
|
-
path: userFillOutputs.yamlPath,
|
|
1932
|
-
label: "Values",
|
|
1933
|
-
hint: "output values"
|
|
1934
|
-
},
|
|
1935
|
-
{
|
|
1936
|
-
path: userFillOutputs.formPath,
|
|
1937
|
-
label: "Form",
|
|
1938
|
-
hint: "filled markform source"
|
|
1939
|
-
}
|
|
1940
|
-
]);
|
|
1941
|
-
p.outro("Happy form filling!");
|
|
1942
|
-
return;
|
|
1943
|
-
}
|
|
1944
|
-
const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
|
|
1945
|
-
if (!modelId) {
|
|
1946
|
-
p.cancel("Cancelled.");
|
|
1947
|
-
process.exit(0);
|
|
1948
|
-
}
|
|
1949
|
-
const agentDefaultFilename = basename(generateVersionedPathInFormsDir(outputPath, formsDir));
|
|
1950
|
-
const agentFilenameResult = await p.text({
|
|
1951
|
-
message: `Agent output filename (in ${formatPath(formsDir)}):`,
|
|
1952
|
-
initialValue: agentDefaultFilename,
|
|
1953
|
-
validate: (value) => {
|
|
1954
|
-
if (!value.trim()) return "Filename is required";
|
|
1955
|
-
}
|
|
1956
|
-
});
|
|
1957
|
-
if (p.isCancel(agentFilenameResult)) {
|
|
1958
|
-
p.cancel("Cancelled.");
|
|
1959
|
-
process.exit(0);
|
|
2529
|
+
console.log(pc.green("Done!"));
|
|
2530
|
+
console.log(`Run ${pc.cyan(`'markform run ${example.filename}'`)} to try it.`);
|
|
1960
2531
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
const configOverrides = isResearchExample ? {
|
|
1965
|
-
maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
|
|
1966
|
-
maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
|
|
1967
|
-
} : void 0;
|
|
1968
|
-
try {
|
|
1969
|
-
const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
|
|
1970
|
-
logTiming({
|
|
1971
|
-
verbose: false,
|
|
1972
|
-
format: "console",
|
|
1973
|
-
dryRun: false,
|
|
1974
|
-
quiet: false
|
|
1975
|
-
}, timingLabel, Date.now() - agentStartTime);
|
|
1976
|
-
const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, agentOutputPath);
|
|
1977
|
-
console.log("");
|
|
1978
|
-
const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
|
|
1979
|
-
p.log.success(successMessage);
|
|
1980
|
-
console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
|
|
1981
|
-
console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
|
|
1982
|
-
console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
|
|
1983
|
-
if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
|
|
1984
|
-
await showFileViewerChooser([
|
|
1985
|
-
{
|
|
1986
|
-
path: reportPath,
|
|
1987
|
-
label: "Report",
|
|
1988
|
-
hint: "output report"
|
|
1989
|
-
},
|
|
1990
|
-
{
|
|
1991
|
-
path: yamlPath,
|
|
1992
|
-
label: "Values",
|
|
1993
|
-
hint: "output values"
|
|
1994
|
-
},
|
|
1995
|
-
{
|
|
1996
|
-
path: formPath,
|
|
1997
|
-
label: "Form",
|
|
1998
|
-
hint: "filled markform source"
|
|
1999
|
-
}
|
|
2000
|
-
]);
|
|
2001
|
-
} catch (error) {
|
|
2002
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2003
|
-
const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
|
|
2004
|
-
p.log.error(`${failMessage}: ${message}`);
|
|
2532
|
+
} else if (result.skipped) {
|
|
2533
|
+
if (!quiet) {
|
|
2534
|
+
console.log(`${formatFormLogLine(example, ` ${pc.yellow("○")}`)} ${pc.dim("(exists, skipped)")}`);
|
|
2005
2535
|
console.log("");
|
|
2006
|
-
console.log(
|
|
2007
|
-
const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
|
|
2008
|
-
console.log(retryCommand);
|
|
2536
|
+
console.log(pc.yellow(`File already exists. Use --overwrite to replace it.`));
|
|
2009
2537
|
}
|
|
2010
2538
|
}
|
|
2011
|
-
p.outro("Happy form filling!");
|
|
2012
2539
|
}
|
|
2013
2540
|
/**
|
|
2014
2541
|
* Register the examples command.
|
|
2015
2542
|
*/
|
|
2016
2543
|
function registerExamplesCommand(program) {
|
|
2017
|
-
program.command("examples").description("
|
|
2544
|
+
program.command("examples").description("Copy bundled example forms to the forms directory").option("--list", "List available examples without copying").option("--name <example>", "Copy specific example by ID").action(async (options, cmd) => {
|
|
2018
2545
|
const ctx = getCommandContext(cmd);
|
|
2019
2546
|
try {
|
|
2020
2547
|
if (options.list) {
|
|
2021
2548
|
printExamplesList();
|
|
2022
2549
|
return;
|
|
2023
2550
|
}
|
|
2551
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2552
|
+
await ensureFormsDir(formsDir);
|
|
2024
2553
|
if (options.name) {
|
|
2025
2554
|
if (!getExampleById(options.name)) {
|
|
2026
2555
|
logError(`Unknown example: ${options.name}`);
|
|
@@ -2028,8 +2557,35 @@ function registerExamplesCommand(program) {
|
|
|
2028
2557
|
for (const ex of EXAMPLE_DEFINITIONS) console.log(` ${ex.id}`);
|
|
2029
2558
|
process.exit(1);
|
|
2030
2559
|
}
|
|
2560
|
+
await copySingleExample(options.name, formsDir, ctx.overwrite, ctx.quiet);
|
|
2561
|
+
} else {
|
|
2562
|
+
const { copied } = await copyAllExamples(formsDir, ctx.overwrite, ctx.quiet);
|
|
2563
|
+
if (!ctx.quiet && copied > 0) {
|
|
2564
|
+
console.log("");
|
|
2565
|
+
const wantToRun = await p.confirm({
|
|
2566
|
+
message: "Do you want to try running a form?",
|
|
2567
|
+
initialValue: true
|
|
2568
|
+
});
|
|
2569
|
+
if (p.isCancel(wantToRun) || !wantToRun) {
|
|
2570
|
+
console.log("");
|
|
2571
|
+
console.log(`Run ${pc.cyan("'markform run'")} to select and run a form later.`);
|
|
2572
|
+
} else {
|
|
2573
|
+
const selectedPath = await showFormMenu(formsDir);
|
|
2574
|
+
if (selectedPath) {
|
|
2575
|
+
console.log("");
|
|
2576
|
+
const exportResult = await runForm(selectedPath, formsDir, ctx.overwrite);
|
|
2577
|
+
if (exportResult) {
|
|
2578
|
+
console.log("");
|
|
2579
|
+
const wantToBrowse = await p.confirm({
|
|
2580
|
+
message: "Would you like to view the output files?",
|
|
2581
|
+
initialValue: true
|
|
2582
|
+
});
|
|
2583
|
+
if (!p.isCancel(wantToBrowse) && wantToBrowse) await browseOutputFiles(exportResult.formPath.replace(/\.form\.md$/, ""));
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2031
2588
|
}
|
|
2032
|
-
await runInteractiveFlow(options.name, ctx.formsDir);
|
|
2033
2589
|
} catch (error) {
|
|
2034
2590
|
logError(error instanceof Error ? error.message : String(error));
|
|
2035
2591
|
process.exit(1);
|
|
@@ -2101,6 +2657,39 @@ function registerExportCommand(program) {
|
|
|
2101
2657
|
});
|
|
2102
2658
|
}
|
|
2103
2659
|
|
|
2660
|
+
//#endregion
|
|
2661
|
+
//#region src/cli/lib/fillCallbacks.ts
|
|
2662
|
+
/**
|
|
2663
|
+
* Create FillCallbacks for CLI commands.
|
|
2664
|
+
*
|
|
2665
|
+
* Provides spinner feedback during tool execution (especially web search).
|
|
2666
|
+
* Only implements tool callbacks - turn/LLM callbacks are handled by CLI's
|
|
2667
|
+
* own logging which has richer context.
|
|
2668
|
+
*
|
|
2669
|
+
* @param spinner - Active spinner handle to update
|
|
2670
|
+
* @param ctx - Command context for verbose logging
|
|
2671
|
+
* @returns FillCallbacks with onToolStart and onToolEnd
|
|
2672
|
+
*
|
|
2673
|
+
* @example
|
|
2674
|
+
* ```typescript
|
|
2675
|
+
* const spinner = createSpinner({ type: 'api', provider, model });
|
|
2676
|
+
* const callbacks = createCliToolCallbacks(spinner, ctx);
|
|
2677
|
+
* const agent = createLiveAgent({ model, callbacks, ... });
|
|
2678
|
+
* ```
|
|
2679
|
+
*/
|
|
2680
|
+
function createCliToolCallbacks(spinner, ctx) {
|
|
2681
|
+
return {
|
|
2682
|
+
onToolStart: ({ name }) => {
|
|
2683
|
+
if (name.includes("search")) spinner.message(`🔍 Web search...`);
|
|
2684
|
+
logVerbose(ctx, ` Tool started: ${name}`);
|
|
2685
|
+
},
|
|
2686
|
+
onToolEnd: ({ name, durationMs, error }) => {
|
|
2687
|
+
if (error) logVerbose(ctx, ` Tool ${name} failed: ${error} (${durationMs}ms)`);
|
|
2688
|
+
else logVerbose(ctx, ` Tool ${name} completed (${durationMs}ms)`);
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2104
2693
|
//#endregion
|
|
2105
2694
|
//#region src/cli/commands/fill.ts
|
|
2106
2695
|
/**
|
|
@@ -2202,13 +2791,14 @@ function registerFillCommand(program) {
|
|
|
2202
2791
|
logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
|
|
2203
2792
|
showInteractiveOutro(patches.length, false);
|
|
2204
2793
|
} else {
|
|
2205
|
-
const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, outputPath$1);
|
|
2794
|
+
const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(form, outputPath$1);
|
|
2206
2795
|
showInteractiveOutro(patches.length, false);
|
|
2207
2796
|
console.log("");
|
|
2208
2797
|
p.log.success("Outputs:");
|
|
2209
2798
|
console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
|
|
2210
2799
|
console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
|
|
2211
2800
|
console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
|
|
2801
|
+
console.log(` ${formatPath(schemaPath)} ${pc.dim("(JSON Schema)")}`);
|
|
2212
2802
|
}
|
|
2213
2803
|
logTiming(ctx, "Fill time", durationMs$1);
|
|
2214
2804
|
if (patches.length > 0) {
|
|
@@ -2243,6 +2833,7 @@ function registerFillCommand(program) {
|
|
|
2243
2833
|
let mockPath;
|
|
2244
2834
|
let agentProvider;
|
|
2245
2835
|
let agentModelName;
|
|
2836
|
+
let currentSpinner = null;
|
|
2246
2837
|
if (options.mock) {
|
|
2247
2838
|
mockPath = resolve(options.mockSource);
|
|
2248
2839
|
logVerbose(ctx, `Reading mock source: ${mockPath}`);
|
|
@@ -2262,13 +2853,21 @@ function registerFillCommand(program) {
|
|
|
2262
2853
|
logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
|
|
2263
2854
|
systemPrompt = await readFile$1(promptPath);
|
|
2264
2855
|
}
|
|
2856
|
+
const callbacks = createCliToolCallbacks({
|
|
2857
|
+
message: (msg) => currentSpinner?.message(msg),
|
|
2858
|
+
update: (context) => currentSpinner?.update(context),
|
|
2859
|
+
stop: (msg) => currentSpinner?.stop(msg),
|
|
2860
|
+
error: (msg) => currentSpinner?.error(msg),
|
|
2861
|
+
getElapsedMs: () => currentSpinner?.getElapsedMs() ?? 0
|
|
2862
|
+
}, ctx);
|
|
2265
2863
|
const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
|
|
2266
2864
|
const liveAgent = createLiveAgent({
|
|
2267
2865
|
model,
|
|
2268
2866
|
provider,
|
|
2269
2867
|
systemPromptAddition: systemPrompt,
|
|
2270
2868
|
targetRole: primaryRole,
|
|
2271
|
-
enableWebSearch: true
|
|
2869
|
+
enableWebSearch: true,
|
|
2870
|
+
callbacks
|
|
2272
2871
|
});
|
|
2273
2872
|
agent = liveAgent;
|
|
2274
2873
|
logInfo(ctx, `Available tools: ${liveAgent.getAvailableToolNames().join(", ")}`);
|
|
@@ -2285,18 +2884,23 @@ function registerFillCommand(program) {
|
|
|
2285
2884
|
logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
2286
2885
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
2287
2886
|
let spinner = null;
|
|
2288
|
-
if (!options.mock && agentProvider && agentModelName && process.stdout.isTTY && !ctx.quiet)
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2887
|
+
if (!options.mock && agentProvider && agentModelName && process.stdout.isTTY && !ctx.quiet) {
|
|
2888
|
+
spinner = createSpinner({
|
|
2889
|
+
type: "api",
|
|
2890
|
+
provider: agentProvider,
|
|
2891
|
+
model: agentModelName,
|
|
2892
|
+
turnNumber: stepResult.turnNumber
|
|
2893
|
+
});
|
|
2894
|
+
currentSpinner = spinner;
|
|
2895
|
+
}
|
|
2294
2896
|
let response;
|
|
2295
2897
|
try {
|
|
2296
2898
|
response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
2297
2899
|
spinner?.stop();
|
|
2900
|
+
currentSpinner = null;
|
|
2298
2901
|
} catch (error) {
|
|
2299
2902
|
spinner?.error("LLM call failed");
|
|
2903
|
+
currentSpinner = null;
|
|
2300
2904
|
throw error;
|
|
2301
2905
|
}
|
|
2302
2906
|
const { patches, stats } = response;
|
|
@@ -2475,7 +3079,7 @@ function formatFieldValue(value, useColors) {
|
|
|
2475
3079
|
/**
|
|
2476
3080
|
* Format inspect report for console output.
|
|
2477
3081
|
*/
|
|
2478
|
-
function formatConsoleReport$
|
|
3082
|
+
function formatConsoleReport$2(report, useColors) {
|
|
2479
3083
|
const lines = [];
|
|
2480
3084
|
const bold = useColors ? pc.bold : (s) => s;
|
|
2481
3085
|
const dim = useColors ? pc.dim : (s) => s;
|
|
@@ -2588,7 +3192,7 @@ function registerInspectCommand(program) {
|
|
|
2588
3192
|
severity: issue.severity,
|
|
2589
3193
|
blockedBy: issue.blockedBy
|
|
2590
3194
|
}))
|
|
2591
|
-
}, (data, useColors) => formatConsoleReport$
|
|
3195
|
+
}, (data, useColors) => formatConsoleReport$2(data, useColors));
|
|
2592
3196
|
console.log(output);
|
|
2593
3197
|
} catch (error) {
|
|
2594
3198
|
logError(error instanceof Error ? error.message : String(error));
|
|
@@ -4029,15 +4633,16 @@ function registerResearchCommand(program) {
|
|
|
4029
4633
|
logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
|
|
4030
4634
|
logInfo(ctx, `Turns: ${result.totalTurns}`);
|
|
4031
4635
|
if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
|
|
4032
|
-
const { reportPath, yamlPath, formPath } = await exportMultiFormat(result.form, outputPath);
|
|
4636
|
+
const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(result.form, outputPath);
|
|
4033
4637
|
logSuccess(ctx, "Outputs:");
|
|
4034
4638
|
console.log(` ${reportPath} ${pc.dim("(output report)")}`);
|
|
4035
4639
|
console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
|
|
4036
4640
|
console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
|
|
4641
|
+
console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
|
|
4037
4642
|
if (options.transcript && result.transcript) {
|
|
4038
|
-
const { serializeSession: serializeSession$1 } = await import("./session-
|
|
4643
|
+
const { serializeSession: serializeSession$1 } = await import("./session-DruaYPZ1.mjs");
|
|
4039
4644
|
const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
|
|
4040
|
-
const { writeFile: writeFile$1 } = await import("./shared-
|
|
4645
|
+
const { writeFile: writeFile$1 } = await import("./shared-C9yW5FLZ.mjs");
|
|
4041
4646
|
await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
|
|
4042
4647
|
logInfo(ctx, `Transcript: ${transcriptPath}`);
|
|
4043
4648
|
}
|
|
@@ -4049,6 +4654,195 @@ function registerResearchCommand(program) {
|
|
|
4049
4654
|
});
|
|
4050
4655
|
}
|
|
4051
4656
|
|
|
4657
|
+
//#endregion
|
|
4658
|
+
//#region src/cli/commands/schema.ts
|
|
4659
|
+
const VALID_DRAFTS = [
|
|
4660
|
+
"2020-12",
|
|
4661
|
+
"2019-09",
|
|
4662
|
+
"draft-07"
|
|
4663
|
+
];
|
|
4664
|
+
/**
|
|
4665
|
+
* Register the schema command.
|
|
4666
|
+
*/
|
|
4667
|
+
function registerSchemaCommand(program) {
|
|
4668
|
+
program.command("schema <file>").description("Export form structure as JSON Schema").option("--pure", "Exclude x-markform extension properties").option("--draft <version>", `JSON Schema draft version: ${VALID_DRAFTS.join(", ")}`, "2020-12").option("--compact", "Output compact JSON (no formatting)").action(async (file, options, cmd) => {
|
|
4669
|
+
const ctx = getCommandContext(cmd);
|
|
4670
|
+
try {
|
|
4671
|
+
const draft = options.draft ?? "2020-12";
|
|
4672
|
+
if (!VALID_DRAFTS.includes(draft)) throw new Error(`Invalid draft version: ${options.draft}. Valid options: ${VALID_DRAFTS.join(", ")}`);
|
|
4673
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
4674
|
+
const content = await readFile$1(file);
|
|
4675
|
+
logVerbose(ctx, "Parsing form...");
|
|
4676
|
+
const form = parseForm(content);
|
|
4677
|
+
logVerbose(ctx, "Generating JSON Schema...");
|
|
4678
|
+
const result = formToJsonSchema(form, {
|
|
4679
|
+
includeExtensions: !options.pure,
|
|
4680
|
+
draft
|
|
4681
|
+
});
|
|
4682
|
+
if (ctx.format === "yaml") console.log(YAML.stringify(result.schema));
|
|
4683
|
+
else if (options.compact) console.log(JSON.stringify(result.schema));
|
|
4684
|
+
else console.log(JSON.stringify(result.schema, null, 2));
|
|
4685
|
+
} catch (error) {
|
|
4686
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
4687
|
+
process.exit(1);
|
|
4688
|
+
}
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
//#endregion
|
|
4693
|
+
//#region src/cli/commands/status.ts
|
|
4694
|
+
/**
|
|
4695
|
+
* Status command - Display form fill status with per-role breakdown.
|
|
4696
|
+
*
|
|
4697
|
+
* Provides:
|
|
4698
|
+
* - Overall progress counts
|
|
4699
|
+
* - Per-role fill statistics
|
|
4700
|
+
* - Run mode (explicit or inferred)
|
|
4701
|
+
* - Suggested next command
|
|
4702
|
+
*/
|
|
4703
|
+
/**
|
|
4704
|
+
* Compute field statistics from a list of fields.
|
|
4705
|
+
*/
|
|
4706
|
+
function computeFieldStats(form, fields) {
|
|
4707
|
+
let answered = 0;
|
|
4708
|
+
let skipped = 0;
|
|
4709
|
+
let aborted = 0;
|
|
4710
|
+
let unanswered = 0;
|
|
4711
|
+
for (const field of fields) switch (form.responsesByFieldId[field.id]?.state ?? "unanswered") {
|
|
4712
|
+
case "answered":
|
|
4713
|
+
answered++;
|
|
4714
|
+
break;
|
|
4715
|
+
case "skipped":
|
|
4716
|
+
skipped++;
|
|
4717
|
+
break;
|
|
4718
|
+
case "aborted":
|
|
4719
|
+
aborted++;
|
|
4720
|
+
break;
|
|
4721
|
+
case "unanswered":
|
|
4722
|
+
default:
|
|
4723
|
+
unanswered++;
|
|
4724
|
+
break;
|
|
4725
|
+
}
|
|
4726
|
+
return {
|
|
4727
|
+
total: fields.length,
|
|
4728
|
+
answered,
|
|
4729
|
+
skipped,
|
|
4730
|
+
aborted,
|
|
4731
|
+
unanswered
|
|
4732
|
+
};
|
|
4733
|
+
}
|
|
4734
|
+
/**
|
|
4735
|
+
* Compute statistics grouped by role.
|
|
4736
|
+
*/
|
|
4737
|
+
function computeStatsByRole(form) {
|
|
4738
|
+
const allFields = getAllFields(form);
|
|
4739
|
+
const roles = getFieldRoles(form);
|
|
4740
|
+
const result = {};
|
|
4741
|
+
for (const role of roles) result[role] = computeFieldStats(form, allFields.filter((f) => f.role === role));
|
|
4742
|
+
return result;
|
|
4743
|
+
}
|
|
4744
|
+
/**
|
|
4745
|
+
* Format percentage with one decimal place.
|
|
4746
|
+
*/
|
|
4747
|
+
function formatPercent(numerator, denominator) {
|
|
4748
|
+
if (denominator === 0) return "0%";
|
|
4749
|
+
return `${Math.round(numerator / denominator * 100)}%`;
|
|
4750
|
+
}
|
|
4751
|
+
/**
|
|
4752
|
+
* Format status report for console output.
|
|
4753
|
+
*/
|
|
4754
|
+
function formatConsoleReport$1(report, useColors) {
|
|
4755
|
+
const lines = [];
|
|
4756
|
+
const bold = useColors ? pc.bold : (s) => s;
|
|
4757
|
+
const dim = useColors ? pc.dim : (s) => s;
|
|
4758
|
+
const cyan = useColors ? pc.cyan : (s) => s;
|
|
4759
|
+
const green = useColors ? pc.green : (s) => s;
|
|
4760
|
+
const yellow = useColors ? pc.yellow : (s) => s;
|
|
4761
|
+
const red = useColors ? pc.red : (s) => s;
|
|
4762
|
+
lines.push(bold(cyan(`Form Status: ${basename(report.path)}`)));
|
|
4763
|
+
lines.push("");
|
|
4764
|
+
const overall = report.overall;
|
|
4765
|
+
const overallPercent = formatPercent(overall.answered, overall.total);
|
|
4766
|
+
lines.push(`${bold("Overall:")} ${overall.answered}/${overall.total} fields filled (${overallPercent})`);
|
|
4767
|
+
lines.push(` ${green("✓")} Complete: ${overall.answered}`);
|
|
4768
|
+
lines.push(` ${dim("○")} Empty: ${overall.unanswered}`);
|
|
4769
|
+
if (overall.skipped > 0) lines.push(` ${yellow("⊘")} Skipped: ${overall.skipped}`);
|
|
4770
|
+
if (overall.aborted > 0) lines.push(` ${red("✗")} Aborted: ${overall.aborted}`);
|
|
4771
|
+
lines.push("");
|
|
4772
|
+
lines.push(bold("By Role:"));
|
|
4773
|
+
const roles = Object.keys(report.byRole).sort((a, b) => {
|
|
4774
|
+
if (a === USER_ROLE) return -1;
|
|
4775
|
+
if (b === USER_ROLE) return 1;
|
|
4776
|
+
if (a === AGENT_ROLE) return -1;
|
|
4777
|
+
if (b === AGENT_ROLE) return 1;
|
|
4778
|
+
return a.localeCompare(b);
|
|
4779
|
+
});
|
|
4780
|
+
for (const role of roles) {
|
|
4781
|
+
const stats = report.byRole[role];
|
|
4782
|
+
if (!stats) continue;
|
|
4783
|
+
const percent = formatPercent(stats.answered, stats.total);
|
|
4784
|
+
const needsAttention = role === USER_ROLE && stats.unanswered > 0 ? yellow(" ← needs attention") : "";
|
|
4785
|
+
lines.push(` ${role}: ${stats.answered}/${stats.total} filled (${percent})${needsAttention}`);
|
|
4786
|
+
}
|
|
4787
|
+
lines.push("");
|
|
4788
|
+
if (report.runMode) {
|
|
4789
|
+
const source = report.runModeSource === "explicit" ? "explicit" : "inferred";
|
|
4790
|
+
lines.push(`${bold("Run Mode:")} ${report.runMode} (${source})`);
|
|
4791
|
+
} else lines.push(`${bold("Run Mode:")} ${dim("unknown")}`);
|
|
4792
|
+
if (report.suggestedCommand) lines.push(`${bold("Suggested:")} ${cyan(report.suggestedCommand)}`);
|
|
4793
|
+
return lines.join("\n");
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
4796
|
+
* Generate a suggested command based on status.
|
|
4797
|
+
*/
|
|
4798
|
+
function getSuggestedCommand(report) {
|
|
4799
|
+
const { overall, byRole, runMode, path } = report;
|
|
4800
|
+
const filename = basename(path);
|
|
4801
|
+
if (overall.total > 0 && overall.answered === overall.total) return null;
|
|
4802
|
+
const userStats = byRole[USER_ROLE];
|
|
4803
|
+
if (userStats && userStats.unanswered > 0) return `markform fill ${filename} --interactive`;
|
|
4804
|
+
if (runMode === "research") return `markform research ${filename}`;
|
|
4805
|
+
return `markform run ${filename}`;
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Register the status command.
|
|
4809
|
+
*/
|
|
4810
|
+
function registerStatusCommand(program) {
|
|
4811
|
+
program.command("status <file>").description("Display form fill status with per-role breakdown").action(async (file, _options, cmd) => {
|
|
4812
|
+
const ctx = getCommandContext(cmd);
|
|
4813
|
+
try {
|
|
4814
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
4815
|
+
const content = await readFile$1(file);
|
|
4816
|
+
logVerbose(ctx, "Parsing form...");
|
|
4817
|
+
const form = parseForm(content);
|
|
4818
|
+
logVerbose(ctx, "Computing status...");
|
|
4819
|
+
const overall = computeFieldStats(form, getAllFields(form));
|
|
4820
|
+
const byRole = computeStatsByRole(form);
|
|
4821
|
+
const runModeResult = determineRunMode(form);
|
|
4822
|
+
let runMode = null;
|
|
4823
|
+
let runModeSource = "unknown";
|
|
4824
|
+
if (runModeResult.success) {
|
|
4825
|
+
runMode = runModeResult.runMode;
|
|
4826
|
+
runModeSource = runModeResult.source;
|
|
4827
|
+
}
|
|
4828
|
+
const report = {
|
|
4829
|
+
path: file,
|
|
4830
|
+
runMode,
|
|
4831
|
+
runModeSource,
|
|
4832
|
+
overall,
|
|
4833
|
+
byRole,
|
|
4834
|
+
suggestedCommand: null
|
|
4835
|
+
};
|
|
4836
|
+
report.suggestedCommand = getSuggestedCommand(report);
|
|
4837
|
+
const output = formatOutput(ctx, report, (data, useColors) => formatConsoleReport$1(data, useColors));
|
|
4838
|
+
console.log(output);
|
|
4839
|
+
} catch (error) {
|
|
4840
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
4841
|
+
process.exit(1);
|
|
4842
|
+
}
|
|
4843
|
+
});
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4052
4846
|
//#endregion
|
|
4053
4847
|
//#region src/cli/commands/validate.ts
|
|
4054
4848
|
/**
|
|
@@ -4183,12 +4977,13 @@ function withColoredHelp(cmd) {
|
|
|
4183
4977
|
*/
|
|
4184
4978
|
function createProgram() {
|
|
4185
4979
|
const program = withColoredHelp(new Command());
|
|
4186
|
-
program.name("markform").description("Agent-friendly, human-readable, editable forms").version(
|
|
4980
|
+
program.name("markform").description("Agent-friendly, human-readable, editable forms").version(CLI_VERSION, "--version", "output the version number").showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`).option("--overwrite", "Overwrite existing field values (default: continue/skip filled)");
|
|
4187
4981
|
registerReadmeCommand(program);
|
|
4188
4982
|
registerDocsCommand(program);
|
|
4189
4983
|
registerSpecCommand(program);
|
|
4190
4984
|
registerApisCommand(program);
|
|
4191
4985
|
registerApplyCommand(program);
|
|
4986
|
+
registerBrowseCommand(program);
|
|
4192
4987
|
registerDumpCommand(program);
|
|
4193
4988
|
registerExamplesCommand(program);
|
|
4194
4989
|
registerExportCommand(program);
|
|
@@ -4198,7 +4993,10 @@ function createProgram() {
|
|
|
4198
4993
|
registerRenderCommand(program);
|
|
4199
4994
|
registerReportCommand(program);
|
|
4200
4995
|
registerResearchCommand(program);
|
|
4996
|
+
registerRunCommand(program);
|
|
4997
|
+
registerSchemaCommand(program);
|
|
4201
4998
|
registerServeCommand(program);
|
|
4999
|
+
registerStatusCommand(program);
|
|
4202
5000
|
registerValidateCommand(program);
|
|
4203
5001
|
return program;
|
|
4204
5002
|
}
|