markform 0.1.6 → 0.1.8
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 +464 -207
- package/dist/ai-sdk.d.mts +1 -2
- package/dist/ai-sdk.mjs +3 -2
- package/dist/{apply-DMQl-VVd.mjs → apply-BUU2QcJ2.mjs} +130 -23
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +3 -6
- package/dist/{cli-CXjkdym_.mjs → cli-BZh25bvy.mjs} +1380 -632
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +2 -6
- package/dist/coreTypes-BSPJ9H27.d.mts +3253 -0
- package/dist/{coreTypes-pyctKRgc.mjs → coreTypes-DJtu8OOp.mjs} +26 -4
- package/dist/index.d.mts +112 -5
- package/dist/index.mjs +6 -5
- package/dist/{session-uF0e6m6k.mjs → session-CmHdAPyg.mjs} +3 -2
- package/dist/session-DSTNiHza.mjs +4 -0
- package/dist/{shared-u22MtBRo.mjs → shared-C9yW5FLZ.mjs} +2 -1
- package/dist/{shared-DRlgu2ZJ.mjs → shared-DQ6y3Ggc.mjs} +3 -1
- package/dist/{src-o_5TSoHQ.mjs → src-kUggXhN1.mjs} +519 -98
- package/docs/markform-apis.md +30 -1
- package/docs/markform-reference.md +65 -6
- package/docs/markform-spec.md +2 -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 +16 -56
- package/examples/movie-research/movie-research-demo.form.md +60 -0
- package/examples/rejection-test/rejection-test-mock-filled.form.md +41 -0
- package/examples/rejection-test/rejection-test-mock-filled.report.md +15 -0
- package/examples/rejection-test/rejection-test-mock-filled.schema.json +59 -0
- package/examples/rejection-test/rejection-test-mock-filled.yml +13 -0
- package/examples/rejection-test/rejection-test.form.md +35 -0
- package/examples/rejection-test/rejection-test.session.yaml +88 -0
- package/examples/simple/simple-mock-filled.report.md +96 -0
- package/examples/simple/simple-mock-filled.schema.json +374 -0
- package/examples/simple/simple-mock-filled.yml +87 -0
- package/examples/simple/simple-skipped-filled.report.md +90 -0
- package/examples/simple/simple-skipped-filled.schema.json +374 -0
- package/examples/simple/simple-skipped-filled.yml +77 -0
- package/examples/simple/simple-with-skips.session.yaml +3 -3
- package/examples/simple/simple.form.md +1 -0
- package/examples/simple/simple.schema.json +374 -0
- package/examples/simple/simple.session.yaml +3 -3
- 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
- package/examples/movie-research/movie-research-minimal.form.md +0 -68
|
@@ -1,19 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
1
|
+
|
|
2
|
+
import { L as PatchSchema } from "./coreTypes-DJtu8OOp.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-BUU2QcJ2.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-kUggXhN1.mjs";
|
|
5
|
+
import { n as serializeSession } from "./session-CmHdAPyg.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
8
|
import { Command } from "commander";
|
|
8
9
|
import pc from "picocolors";
|
|
10
|
+
import { exec, execSync, spawn } from "node:child_process";
|
|
9
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
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
|
|
17
64
|
//#region src/cli/lib/paths.ts
|
|
18
65
|
/**
|
|
19
66
|
* Path utilities for CLI commands.
|
|
@@ -145,7 +192,7 @@ function formatState$2(state, useColors) {
|
|
|
145
192
|
/**
|
|
146
193
|
* Format apply report for console output.
|
|
147
194
|
*/
|
|
148
|
-
function formatConsoleReport$
|
|
195
|
+
function formatConsoleReport$3(report, useColors) {
|
|
149
196
|
const lines = [];
|
|
150
197
|
const bold = useColors ? pc.bold : (s) => s;
|
|
151
198
|
const dim = useColors ? pc.dim : (s) => s;
|
|
@@ -227,7 +274,7 @@ function registerApplyCommand(program) {
|
|
|
227
274
|
structure: result.structureSummary,
|
|
228
275
|
progress: result.progressSummary,
|
|
229
276
|
issues: result.issues
|
|
230
|
-
}, (data, useColors) => formatConsoleReport$
|
|
277
|
+
}, (data, useColors) => formatConsoleReport$3(data, useColors));
|
|
231
278
|
console.error(output);
|
|
232
279
|
process.exit(1);
|
|
233
280
|
}
|
|
@@ -239,7 +286,7 @@ function registerApplyCommand(program) {
|
|
|
239
286
|
structure: result.structureSummary,
|
|
240
287
|
progress: result.progressSummary,
|
|
241
288
|
issues: result.issues
|
|
242
|
-
}, (data, useColors) => formatConsoleReport$
|
|
289
|
+
}, (data, useColors) => formatConsoleReport$3(data, useColors));
|
|
243
290
|
if (options.output) {
|
|
244
291
|
await writeFile(options.output, output);
|
|
245
292
|
logSuccess(ctx, `Report written to ${options.output}`);
|
|
@@ -258,6 +305,357 @@ function registerApplyCommand(program) {
|
|
|
258
305
|
});
|
|
259
306
|
}
|
|
260
307
|
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/cli/lib/fileViewer.ts
|
|
310
|
+
/**
|
|
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
|
|
317
|
+
*/
|
|
318
|
+
/**
|
|
319
|
+
* Check if stdout is an interactive terminal.
|
|
320
|
+
*/
|
|
321
|
+
function isInteractive$3() {
|
|
322
|
+
return process.stdout.isTTY === true;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Apply terminal formatting to markdown content.
|
|
326
|
+
* Colorizes headers, code blocks, and other elements.
|
|
327
|
+
*/
|
|
328
|
+
function formatMarkdown$3(content) {
|
|
329
|
+
const lines = content.split("\n");
|
|
330
|
+
const formatted = [];
|
|
331
|
+
let inCodeBlock = false;
|
|
332
|
+
for (const line of lines) {
|
|
333
|
+
if (line.startsWith("```")) {
|
|
334
|
+
inCodeBlock = !inCodeBlock;
|
|
335
|
+
formatted.push(pc.dim(line));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (inCodeBlock) {
|
|
339
|
+
formatted.push(pc.cyan(line));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (line.startsWith("# ")) {
|
|
343
|
+
formatted.push(pc.bold(pc.magenta(line)));
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (line.startsWith("## ")) {
|
|
347
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (line.startsWith("### ")) {
|
|
351
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (line.startsWith("#### ")) {
|
|
355
|
+
formatted.push(pc.bold(line));
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
359
|
+
return pc.yellow(code);
|
|
360
|
+
});
|
|
361
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
362
|
+
return pc.bold(text);
|
|
363
|
+
});
|
|
364
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
365
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
366
|
+
});
|
|
367
|
+
formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
|
|
368
|
+
const attrsPart = attrs.trim() ? ` ${pc.dim(attrs.trim())}` : "";
|
|
369
|
+
return `${pc.dim("{% ")}${pc.green(tag)}${attrsPart} ${pc.dim("%}")}`;
|
|
370
|
+
});
|
|
371
|
+
formatted.push(formattedLine);
|
|
372
|
+
}
|
|
373
|
+
return formatted.join("\n");
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Apply terminal formatting to YAML content.
|
|
377
|
+
*/
|
|
378
|
+
function formatYaml(content) {
|
|
379
|
+
const lines = content.split("\n");
|
|
380
|
+
const formatted = [];
|
|
381
|
+
for (const line of lines) {
|
|
382
|
+
if (line.trim().startsWith("#")) {
|
|
383
|
+
formatted.push(pc.dim(line));
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
|
|
387
|
+
if (match) {
|
|
388
|
+
const [, indent, key, colon, value] = match;
|
|
389
|
+
formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (line.trim().startsWith("-")) {
|
|
393
|
+
formatted.push(pc.green(line));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
formatted.push(line);
|
|
397
|
+
}
|
|
398
|
+
return formatted.join("\n");
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Format file content based on extension.
|
|
402
|
+
*/
|
|
403
|
+
function formatContent(content, filename) {
|
|
404
|
+
if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
|
|
405
|
+
if (filename.endsWith(".md")) return formatMarkdown$3(content);
|
|
406
|
+
return content;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Display content using system pager (less) if available.
|
|
410
|
+
* Falls back to console.log if not interactive or pager unavailable.
|
|
411
|
+
*
|
|
412
|
+
* @returns Promise that resolves when viewing is complete
|
|
413
|
+
*/
|
|
414
|
+
async function displayWithPager(content, title) {
|
|
415
|
+
if (!isInteractive$3()) {
|
|
416
|
+
console.log(content);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
|
|
420
|
+
return new Promise((resolve$1) => {
|
|
421
|
+
const pager = spawn("less", [
|
|
422
|
+
"-R",
|
|
423
|
+
"-S",
|
|
424
|
+
"-X",
|
|
425
|
+
"-F",
|
|
426
|
+
"-K"
|
|
427
|
+
], { stdio: [
|
|
428
|
+
"pipe",
|
|
429
|
+
"inherit",
|
|
430
|
+
"inherit"
|
|
431
|
+
] });
|
|
432
|
+
pager.on("error", () => {
|
|
433
|
+
console.log(header);
|
|
434
|
+
console.log("");
|
|
435
|
+
console.log(content);
|
|
436
|
+
console.log("");
|
|
437
|
+
resolve$1();
|
|
438
|
+
});
|
|
439
|
+
pager.on("close", () => {
|
|
440
|
+
resolve$1();
|
|
441
|
+
});
|
|
442
|
+
pager.stdin.write(header + "\n\n");
|
|
443
|
+
pager.stdin.write(content);
|
|
444
|
+
pager.stdin.end();
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Load and display a file with formatting and pagination.
|
|
449
|
+
*/
|
|
450
|
+
async function viewFile(filePath) {
|
|
451
|
+
const content = readFileSync(filePath, "utf-8");
|
|
452
|
+
const filename = basename(filePath);
|
|
453
|
+
await displayWithPager(formatContent(content, filename), filename);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Show an interactive file viewer chooser.
|
|
457
|
+
*
|
|
458
|
+
* Presents a list of files to view:
|
|
459
|
+
* - "Show report:" for the report output (.report.md) at the top
|
|
460
|
+
* - "Show source:" for other files (.form.md, .raw.md, .yml)
|
|
461
|
+
* - "Quit" at the bottom
|
|
462
|
+
*
|
|
463
|
+
* Loops until the user selects Quit.
|
|
464
|
+
*
|
|
465
|
+
* @param files Array of file options to display
|
|
466
|
+
*/
|
|
467
|
+
async function showFileViewerChooser(files) {
|
|
468
|
+
if (!isInteractive$3()) return;
|
|
469
|
+
console.log("");
|
|
470
|
+
const reportFile = files.find((f) => f.path.endsWith(".report.md"));
|
|
471
|
+
const sourceFiles = files.filter((f) => !f.path.endsWith(".report.md"));
|
|
472
|
+
while (true) {
|
|
473
|
+
const options = [];
|
|
474
|
+
if (reportFile) options.push({
|
|
475
|
+
value: reportFile.path,
|
|
476
|
+
label: `Show report: ${pc.green(basename(reportFile.path))}`,
|
|
477
|
+
hint: reportFile.hint ?? ""
|
|
478
|
+
});
|
|
479
|
+
for (const file of sourceFiles) options.push({
|
|
480
|
+
value: file.path,
|
|
481
|
+
label: `Show source: ${pc.green(basename(file.path))}`,
|
|
482
|
+
hint: file.hint ?? ""
|
|
483
|
+
});
|
|
484
|
+
options.push({
|
|
485
|
+
value: "quit",
|
|
486
|
+
label: "Quit",
|
|
487
|
+
hint: ""
|
|
488
|
+
});
|
|
489
|
+
const selection = await p.select({
|
|
490
|
+
message: "View files:",
|
|
491
|
+
options
|
|
492
|
+
});
|
|
493
|
+
if (p.isCancel(selection) || selection === "quit") break;
|
|
494
|
+
await viewFile(selection);
|
|
495
|
+
console.log("");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/cli/commands/browse.ts
|
|
501
|
+
/**
|
|
502
|
+
* Browse command - Interactive file browser for the forms directory.
|
|
503
|
+
*
|
|
504
|
+
* Provides a menu-based interface for viewing files with syntax highlighting
|
|
505
|
+
* and pagination. Shows .form.md, .report.md, .yml, and .schema.json files.
|
|
506
|
+
*
|
|
507
|
+
* Usage:
|
|
508
|
+
* markform browse # Browse all files in forms/
|
|
509
|
+
* markform browse --filter=foo # Only show files matching pattern
|
|
510
|
+
*/
|
|
511
|
+
/** File extensions we support viewing */
|
|
512
|
+
const VIEWABLE_EXTENSIONS = [
|
|
513
|
+
".form.md",
|
|
514
|
+
".report.md",
|
|
515
|
+
".yml",
|
|
516
|
+
".yaml",
|
|
517
|
+
".schema.json",
|
|
518
|
+
".raw.md"
|
|
519
|
+
];
|
|
520
|
+
/**
|
|
521
|
+
* Check if a file has a viewable extension.
|
|
522
|
+
*/
|
|
523
|
+
function isViewableFile(filename) {
|
|
524
|
+
return VIEWABLE_EXTENSIONS.some((ext) => filename.endsWith(ext));
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get the display extension for sorting/grouping.
|
|
528
|
+
*/
|
|
529
|
+
function getExtension(filename) {
|
|
530
|
+
for (const ext of VIEWABLE_EXTENSIONS) if (filename.endsWith(ext)) return ext;
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Scan forms directory for viewable files.
|
|
535
|
+
*/
|
|
536
|
+
function scanFormsDirectory$1(formsDir, filter) {
|
|
537
|
+
const entries = [];
|
|
538
|
+
try {
|
|
539
|
+
const files = readdirSync(formsDir);
|
|
540
|
+
for (const file of files) {
|
|
541
|
+
if (!isViewableFile(file)) continue;
|
|
542
|
+
if (filter && !file.toLowerCase().includes(filter.toLowerCase())) continue;
|
|
543
|
+
const fullPath = join(formsDir, file);
|
|
544
|
+
try {
|
|
545
|
+
const stat = statSync(fullPath);
|
|
546
|
+
if (stat.isFile()) entries.push({
|
|
547
|
+
path: fullPath,
|
|
548
|
+
filename: file,
|
|
549
|
+
mtime: stat.mtime,
|
|
550
|
+
extension: getExtension(file)
|
|
551
|
+
});
|
|
552
|
+
} catch {}
|
|
553
|
+
}
|
|
554
|
+
} catch {}
|
|
555
|
+
entries.sort((a, b) => {
|
|
556
|
+
const timeDiff = b.mtime.getTime() - a.mtime.getTime();
|
|
557
|
+
if (timeDiff !== 0) return timeDiff;
|
|
558
|
+
return a.filename.localeCompare(b.filename);
|
|
559
|
+
});
|
|
560
|
+
return entries;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Get extension hint for display.
|
|
564
|
+
*/
|
|
565
|
+
function getExtensionHint(ext) {
|
|
566
|
+
switch (ext) {
|
|
567
|
+
case ".form.md": return "markform source";
|
|
568
|
+
case ".report.md": return "output report";
|
|
569
|
+
case ".yml":
|
|
570
|
+
case ".yaml": return "YAML values";
|
|
571
|
+
case ".schema.json": return "JSON Schema";
|
|
572
|
+
case ".raw.md": return "raw markdown";
|
|
573
|
+
default: return "";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Format file entry for menu display.
|
|
578
|
+
*/
|
|
579
|
+
function formatFileLabel(entry) {
|
|
580
|
+
return `${entry.extension === ".report.md" ? pc.green("*") : " "} ${entry.filename}`;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Browse specific output files after a form run.
|
|
584
|
+
*
|
|
585
|
+
* This is a convenience function for the examples workflow that shows
|
|
586
|
+
* the standard output files (report, yml, form, schema) for a completed form.
|
|
587
|
+
*
|
|
588
|
+
* @param basePath - Base path of the output (e.g., "forms/movie-research-demo-filled1")
|
|
589
|
+
*/
|
|
590
|
+
async function browseOutputFiles(basePath) {
|
|
591
|
+
const outputExtensions = [
|
|
592
|
+
".report.md",
|
|
593
|
+
".yml",
|
|
594
|
+
".form.md",
|
|
595
|
+
".schema.json"
|
|
596
|
+
];
|
|
597
|
+
const files = [];
|
|
598
|
+
for (const ext of outputExtensions) {
|
|
599
|
+
const fullPath = basePath + ext;
|
|
600
|
+
try {
|
|
601
|
+
statSync(fullPath);
|
|
602
|
+
files.push({
|
|
603
|
+
path: fullPath,
|
|
604
|
+
label: basename(fullPath),
|
|
605
|
+
hint: getExtensionHint(ext)
|
|
606
|
+
});
|
|
607
|
+
} catch {}
|
|
608
|
+
}
|
|
609
|
+
if (files.length === 0) return;
|
|
610
|
+
await showFileViewerChooser(files);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Register the browse command.
|
|
614
|
+
*/
|
|
615
|
+
function registerBrowseCommand(program) {
|
|
616
|
+
program.command("browse").description("Browse and view files in the forms directory").option("--filter <pattern>", "Only show files matching pattern").action(async (options, cmd) => {
|
|
617
|
+
const ctx = getCommandContext(cmd);
|
|
618
|
+
try {
|
|
619
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
620
|
+
p.intro(pc.bgCyan(pc.black(" markform browse ")));
|
|
621
|
+
const entries = scanFormsDirectory$1(formsDir, options.filter);
|
|
622
|
+
if (entries.length === 0) {
|
|
623
|
+
if (options.filter) p.log.warn(`No files matching "${options.filter}" found in ${formatPath(formsDir)}`);
|
|
624
|
+
else p.log.warn(`No viewable files found in ${formatPath(formsDir)}`);
|
|
625
|
+
console.log("");
|
|
626
|
+
console.log(`Run ${pc.cyan("'markform examples'")} to get started.`);
|
|
627
|
+
p.outro("");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
console.log(pc.dim(`Found ${entries.length} file(s) in ${formatPath(formsDir)}`));
|
|
631
|
+
const menuOptions = entries.map((entry) => ({
|
|
632
|
+
value: entry.path,
|
|
633
|
+
label: formatFileLabel(entry),
|
|
634
|
+
hint: getExtensionHint(entry.extension)
|
|
635
|
+
}));
|
|
636
|
+
menuOptions.push({
|
|
637
|
+
value: "quit",
|
|
638
|
+
label: "Quit",
|
|
639
|
+
hint: ""
|
|
640
|
+
});
|
|
641
|
+
while (true) {
|
|
642
|
+
const selection = await p.select({
|
|
643
|
+
message: "Select a file to view:",
|
|
644
|
+
options: menuOptions
|
|
645
|
+
});
|
|
646
|
+
if (p.isCancel(selection)) break;
|
|
647
|
+
if (selection === "quit") break;
|
|
648
|
+
await viewFile(selection);
|
|
649
|
+
console.log("");
|
|
650
|
+
}
|
|
651
|
+
p.outro("");
|
|
652
|
+
} catch (error) {
|
|
653
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
261
659
|
//#endregion
|
|
262
660
|
//#region src/cli/commands/docs.ts
|
|
263
661
|
/**
|
|
@@ -285,7 +683,7 @@ function loadDocs() {
|
|
|
285
683
|
* Apply basic terminal formatting to markdown content.
|
|
286
684
|
* Colorizes headers, code blocks, and other elements for better readability.
|
|
287
685
|
*/
|
|
288
|
-
function formatMarkdown$
|
|
686
|
+
function formatMarkdown$2(content, useColors) {
|
|
289
687
|
if (!useColors) return content;
|
|
290
688
|
const lines = content.split("\n");
|
|
291
689
|
const formatted = [];
|
|
@@ -328,7 +726,7 @@ function formatMarkdown$3(content, useColors) {
|
|
|
328
726
|
/**
|
|
329
727
|
* Check if stdout is an interactive terminal.
|
|
330
728
|
*/
|
|
331
|
-
function isInteractive$
|
|
729
|
+
function isInteractive$2() {
|
|
332
730
|
return process.stdout.isTTY === true;
|
|
333
731
|
}
|
|
334
732
|
/**
|
|
@@ -344,7 +742,7 @@ function registerDocsCommand(program) {
|
|
|
344
742
|
program.command("docs").description("Display concise Markform syntax reference (agent-friendly)").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
345
743
|
const ctx = getCommandContext(cmd);
|
|
346
744
|
try {
|
|
347
|
-
displayContent$2(formatMarkdown$
|
|
745
|
+
displayContent$2(formatMarkdown$2(loadDocs(), !options.raw && isInteractive$2() && !ctx.quiet));
|
|
348
746
|
} catch (error) {
|
|
349
747
|
logError(error instanceof Error ? error.message : String(error));
|
|
350
748
|
process.exit(1);
|
|
@@ -361,6 +759,7 @@ function registerDocsCommand(program) {
|
|
|
361
759
|
* - Markform format (.form.md) - canonical form with directives
|
|
362
760
|
* - Raw markdown (.raw.md) - plain readable markdown
|
|
363
761
|
* - YAML values (.yml) - extracted field values
|
|
762
|
+
* - JSON Schema (.schema.json) - form structure for validation/tooling
|
|
364
763
|
*/
|
|
365
764
|
/**
|
|
366
765
|
* Convert field responses to structured format for export (markform-218).
|
|
@@ -426,6 +825,23 @@ function toStructuredValues(form) {
|
|
|
426
825
|
case "url_list":
|
|
427
826
|
exportValue = value.items;
|
|
428
827
|
break;
|
|
828
|
+
case "date":
|
|
829
|
+
exportValue = value.value ?? null;
|
|
830
|
+
break;
|
|
831
|
+
case "year":
|
|
832
|
+
exportValue = value.value ?? null;
|
|
833
|
+
break;
|
|
834
|
+
case "table":
|
|
835
|
+
exportValue = value.rows.map((row) => {
|
|
836
|
+
const rowObj = {};
|
|
837
|
+
for (const [colId, cellResp] of Object.entries(row)) rowObj[colId] = cellResp.value ?? null;
|
|
838
|
+
return rowObj;
|
|
839
|
+
});
|
|
840
|
+
break;
|
|
841
|
+
default: {
|
|
842
|
+
const _exhaustive = value;
|
|
843
|
+
throw new Error(`Unhandled field value kind: ${_exhaustive.kind}`);
|
|
844
|
+
}
|
|
429
845
|
}
|
|
430
846
|
result[fieldId] = {
|
|
431
847
|
state: "answered",
|
|
@@ -449,7 +865,7 @@ function toNotesArray(form) {
|
|
|
449
865
|
* Derive export paths from a base form path.
|
|
450
866
|
* Uses centralized extension constants from settings.ts.
|
|
451
867
|
*
|
|
452
|
-
* Standard exports: report, values (yaml), form.
|
|
868
|
+
* Standard exports: report, values (yaml), form, schema.
|
|
453
869
|
* Raw markdown is available via CLI but not in standard exports.
|
|
454
870
|
*
|
|
455
871
|
* @param basePath - Path to the .form.md file
|
|
@@ -459,7 +875,8 @@ function deriveExportPaths(basePath) {
|
|
|
459
875
|
return {
|
|
460
876
|
reportPath: deriveReportPath(basePath),
|
|
461
877
|
yamlPath: deriveExportPath(basePath, "yaml"),
|
|
462
|
-
formPath: deriveExportPath(basePath, "form")
|
|
878
|
+
formPath: deriveExportPath(basePath, "form"),
|
|
879
|
+
schemaPath: deriveSchemaPath(basePath)
|
|
463
880
|
};
|
|
464
881
|
}
|
|
465
882
|
/**
|
|
@@ -469,6 +886,7 @@ function deriveExportPaths(basePath) {
|
|
|
469
886
|
* - Report format (.report.md) - filtered markdown (excludes instructions, report=false)
|
|
470
887
|
* - YAML values (.yml) - structured format with state and notes
|
|
471
888
|
* - Markform format (.form.md) - canonical form with directives
|
|
889
|
+
* - JSON Schema (.schema.json) - form structure for validation/tooling
|
|
472
890
|
*
|
|
473
891
|
* Note: Raw markdown (.raw.md) is available via CLI `markform export --raw`
|
|
474
892
|
* but is not included in standard multi-format export.
|
|
@@ -491,6 +909,9 @@ async function exportMultiFormat(form, basePath) {
|
|
|
491
909
|
await writeFile(paths.yamlPath, yamlContent);
|
|
492
910
|
const formContent = serialize(form);
|
|
493
911
|
await writeFile(paths.formPath, formContent);
|
|
912
|
+
const schemaResult = formToJsonSchema(form);
|
|
913
|
+
const schemaContent = JSON.stringify(schemaResult.schema, null, 2) + "\n";
|
|
914
|
+
await writeFile(paths.schemaPath, schemaContent);
|
|
494
915
|
return paths;
|
|
495
916
|
}
|
|
496
917
|
|
|
@@ -566,96 +987,6 @@ function registerDumpCommand(program) {
|
|
|
566
987
|
});
|
|
567
988
|
}
|
|
568
989
|
|
|
569
|
-
//#endregion
|
|
570
|
-
//#region src/cli/lib/patchFormat.ts
|
|
571
|
-
/** Maximum characters for a patch value display before truncation */
|
|
572
|
-
const PATCH_VALUE_MAX_LENGTH = 1e3;
|
|
573
|
-
/**
|
|
574
|
-
* Truncate a string to max length with ellipsis if needed.
|
|
575
|
-
*/
|
|
576
|
-
function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
|
|
577
|
-
if (value.length <= maxLength) return value;
|
|
578
|
-
return value.slice(0, maxLength) + "…";
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Format a patch value for display with truncation.
|
|
582
|
-
*/
|
|
583
|
-
function formatPatchValue(patch) {
|
|
584
|
-
switch (patch.op) {
|
|
585
|
-
case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
586
|
-
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
587
|
-
case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
588
|
-
case "set_single_select": return patch.selected ?? "(none)";
|
|
589
|
-
case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
|
|
590
|
-
case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
|
|
591
|
-
case "clear_field": return "(cleared)";
|
|
592
|
-
case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
|
|
593
|
-
case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
|
|
594
|
-
case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
595
|
-
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
596
|
-
case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
597
|
-
case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
598
|
-
case "set_table": return patch.rows.length > 0 ? truncate(`[${patch.rows.length} rows]`) : "(empty)";
|
|
599
|
-
case "add_note": return truncate(`note: ${patch.text}`);
|
|
600
|
-
case "remove_note": return `(remove note ${patch.noteId})`;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Get a short display name for the patch operation type.
|
|
605
|
-
*/
|
|
606
|
-
function formatPatchType(patch) {
|
|
607
|
-
switch (patch.op) {
|
|
608
|
-
case "set_string": return "string";
|
|
609
|
-
case "set_number": return "number";
|
|
610
|
-
case "set_string_list": return "string_list";
|
|
611
|
-
case "set_single_select": return "select";
|
|
612
|
-
case "set_multi_select": return "multi_select";
|
|
613
|
-
case "set_checkboxes": return "checkboxes";
|
|
614
|
-
case "clear_field": return "clear";
|
|
615
|
-
case "skip_field": return "skip";
|
|
616
|
-
case "abort_field": return "abort";
|
|
617
|
-
case "set_url": return "url";
|
|
618
|
-
case "set_url_list": return "url_list";
|
|
619
|
-
case "set_date": return "date";
|
|
620
|
-
case "set_year": return "year";
|
|
621
|
-
case "set_table": return "table";
|
|
622
|
-
case "add_note": return "note";
|
|
623
|
-
case "remove_note": return "remove_note";
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
//#endregion
|
|
628
|
-
//#region src/cli/lib/formatting.ts
|
|
629
|
-
/**
|
|
630
|
-
* Get a short status word from an issue reason.
|
|
631
|
-
*/
|
|
632
|
-
function issueReasonToStatus(reason) {
|
|
633
|
-
switch (reason) {
|
|
634
|
-
case "required_missing": return "missing";
|
|
635
|
-
case "validation_error": return "invalid";
|
|
636
|
-
case "checkbox_incomplete": return "incomplete";
|
|
637
|
-
case "min_items_not_met": return "too-few";
|
|
638
|
-
case "optional_empty": return "empty";
|
|
639
|
-
default: return "issue";
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Format a single issue as "fieldId (status)".
|
|
644
|
-
*/
|
|
645
|
-
function formatIssueBrief(issue) {
|
|
646
|
-
const status = issueReasonToStatus(issue.reason);
|
|
647
|
-
return `${issue.ref} (${status})`;
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Format issues for turn logging - shows count and brief field list.
|
|
651
|
-
* Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
|
|
652
|
-
*/
|
|
653
|
-
function formatTurnIssues(issues, maxShow = 5) {
|
|
654
|
-
const count = issues.length;
|
|
655
|
-
if (count === 0) return "0 issues";
|
|
656
|
-
return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
990
|
//#endregion
|
|
660
991
|
//#region src/cli/examples/exampleRegistry.ts
|
|
661
992
|
/**
|
|
@@ -670,18 +1001,18 @@ function formatTurnIssues(issues, maxShow = 5) {
|
|
|
670
1001
|
* Title and description are loaded dynamically from frontmatter.
|
|
671
1002
|
*/
|
|
672
1003
|
const EXAMPLE_DEFINITIONS = [
|
|
1004
|
+
{
|
|
1005
|
+
id: "movie-research-demo",
|
|
1006
|
+
filename: "movie-research-demo.form.md",
|
|
1007
|
+
path: "movie-research/movie-research-demo.form.md",
|
|
1008
|
+
type: "research"
|
|
1009
|
+
},
|
|
673
1010
|
{
|
|
674
1011
|
id: "simple",
|
|
675
1012
|
filename: "simple.form.md",
|
|
676
1013
|
path: "simple/simple.form.md",
|
|
677
1014
|
type: "fill"
|
|
678
1015
|
},
|
|
679
|
-
{
|
|
680
|
-
id: "movie-research-minimal",
|
|
681
|
-
filename: "movie-research-minimal.form.md",
|
|
682
|
-
path: "movie-research/movie-research-minimal.form.md",
|
|
683
|
-
type: "research"
|
|
684
|
-
},
|
|
685
1016
|
{
|
|
686
1017
|
id: "movie-research-basic",
|
|
687
1018
|
filename: "movie-research-basic.form.md",
|
|
@@ -694,19 +1025,29 @@ const EXAMPLE_DEFINITIONS = [
|
|
|
694
1025
|
path: "movie-research/movie-research-deep.form.md",
|
|
695
1026
|
type: "research"
|
|
696
1027
|
},
|
|
697
|
-
{
|
|
698
|
-
id: "earnings-analysis",
|
|
699
|
-
filename: "earnings-analysis.form.md",
|
|
700
|
-
path: "earnings-analysis/earnings-analysis.form.md",
|
|
701
|
-
type: "research"
|
|
702
|
-
},
|
|
703
1028
|
{
|
|
704
1029
|
id: "startup-deep-research",
|
|
705
1030
|
filename: "startup-deep-research.form.md",
|
|
706
1031
|
path: "startup-deep-research/startup-deep-research.form.md",
|
|
707
1032
|
type: "research"
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: "earnings-analysis",
|
|
1036
|
+
filename: "earnings-analysis.form.md",
|
|
1037
|
+
path: "earnings-analysis/earnings-analysis.form.md",
|
|
1038
|
+
type: "research"
|
|
708
1039
|
}
|
|
709
1040
|
];
|
|
1041
|
+
/** Default example ID for menus (movie-research-demo, index 0) */
|
|
1042
|
+
const DEFAULT_EXAMPLE_ID = "movie-research-demo";
|
|
1043
|
+
/**
|
|
1044
|
+
* Get the canonical order index for an example by filename.
|
|
1045
|
+
* Returns -1 if not found (unknown files sort to the end).
|
|
1046
|
+
*/
|
|
1047
|
+
function getExampleOrder(filename) {
|
|
1048
|
+
const index = EXAMPLE_DEFINITIONS.findIndex((e) => e.filename === filename);
|
|
1049
|
+
return index >= 0 ? index : EXAMPLE_DEFINITIONS.length;
|
|
1050
|
+
}
|
|
710
1051
|
/**
|
|
711
1052
|
* Get the path to the examples directory.
|
|
712
1053
|
* Works both during development and when installed as a package.
|
|
@@ -792,6 +1133,160 @@ function getAllExamplesWithMetadata() {
|
|
|
792
1133
|
});
|
|
793
1134
|
}
|
|
794
1135
|
|
|
1136
|
+
//#endregion
|
|
1137
|
+
//#region src/cli/lib/formatting.ts
|
|
1138
|
+
/**
|
|
1139
|
+
* Color and output formatting utilities for CLI.
|
|
1140
|
+
*/
|
|
1141
|
+
/**
|
|
1142
|
+
* Get a short status word from an issue reason.
|
|
1143
|
+
*/
|
|
1144
|
+
function issueReasonToStatus(reason) {
|
|
1145
|
+
switch (reason) {
|
|
1146
|
+
case "required_missing": return "missing";
|
|
1147
|
+
case "validation_error": return "invalid";
|
|
1148
|
+
case "checkbox_incomplete": return "incomplete";
|
|
1149
|
+
case "min_items_not_met": return "too-few";
|
|
1150
|
+
case "optional_empty": return "empty";
|
|
1151
|
+
default: return "issue";
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Format a single issue as "fieldId (status)".
|
|
1156
|
+
*/
|
|
1157
|
+
function formatIssueBrief(issue) {
|
|
1158
|
+
const status = issueReasonToStatus(issue.reason);
|
|
1159
|
+
return `${issue.ref} (${status})`;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Format issues for turn logging - shows count and brief field list.
|
|
1163
|
+
* Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
|
|
1164
|
+
*/
|
|
1165
|
+
function formatTurnIssues(issues, maxShow = 5) {
|
|
1166
|
+
const count = issues.length;
|
|
1167
|
+
if (count === 0) return "0 issues";
|
|
1168
|
+
return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Format form info for menu label display.
|
|
1172
|
+
* Format: "filename - Title [runMode]"
|
|
1173
|
+
* Example: "movie-research-deep.form.md - Movie Research (Deep) [research]"
|
|
1174
|
+
*/
|
|
1175
|
+
function formatFormLabel(info) {
|
|
1176
|
+
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
1177
|
+
const runModePart = info.runMode ? ` [${info.runMode}]` : "";
|
|
1178
|
+
return `${info.filename}${titlePart}${runModePart}`;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Format form info for menu hint display.
|
|
1182
|
+
* Returns description without parentheses (prompts library adds them).
|
|
1183
|
+
*/
|
|
1184
|
+
function formatFormHint(info) {
|
|
1185
|
+
return info.description ?? "";
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Format form info for log line (e.g., after copying).
|
|
1189
|
+
* Format: "filename - Title" (dimmed title)
|
|
1190
|
+
* Example: "✓ movie-research-deep.form.md - Movie Research (Deep)"
|
|
1191
|
+
*/
|
|
1192
|
+
function formatFormLogLine(info, prefix) {
|
|
1193
|
+
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
1194
|
+
return `${prefix} ${info.filename}${pc.dim(titlePart)}`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/cli/lib/runMode.ts
|
|
1199
|
+
/**
|
|
1200
|
+
* Get the set of unique roles present in a form's fields.
|
|
1201
|
+
*/
|
|
1202
|
+
function getFieldRoles(form) {
|
|
1203
|
+
const allFields = getAllFields(form);
|
|
1204
|
+
return new Set(allFields.map((field) => field.role));
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Validate that run_mode is consistent with form structure.
|
|
1208
|
+
*
|
|
1209
|
+
* Rules:
|
|
1210
|
+
* - interactive: Form MUST have at least one role="user" field
|
|
1211
|
+
* - fill: Form MUST have at least one role="agent" field
|
|
1212
|
+
* - research: Form MUST have at least one role="agent" field
|
|
1213
|
+
*/
|
|
1214
|
+
function validateRunMode(form, runMode) {
|
|
1215
|
+
const roles = getFieldRoles(form);
|
|
1216
|
+
switch (runMode) {
|
|
1217
|
+
case "interactive":
|
|
1218
|
+
if (!roles.has(USER_ROLE)) return {
|
|
1219
|
+
valid: false,
|
|
1220
|
+
error: `run_mode="interactive" but form has no user-role fields. Available roles: ${[...roles].join(", ") || "(none)"}`
|
|
1221
|
+
};
|
|
1222
|
+
break;
|
|
1223
|
+
case "fill":
|
|
1224
|
+
case "research":
|
|
1225
|
+
if (!roles.has(AGENT_ROLE)) return {
|
|
1226
|
+
valid: false,
|
|
1227
|
+
error: `run_mode="${runMode}" but form has no agent-role fields. Available roles: ${[...roles].join(", ") || "(none)"}`
|
|
1228
|
+
};
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
return { valid: true };
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Determine the run mode for a form.
|
|
1235
|
+
*
|
|
1236
|
+
* 1. If explicit run_mode in frontmatter, validate and use it
|
|
1237
|
+
* 2. Otherwise, infer from field roles:
|
|
1238
|
+
* - All user fields → interactive
|
|
1239
|
+
* - All agent fields → fill (or research if isResearchForm)
|
|
1240
|
+
* - Mixed roles → error (require explicit run_mode)
|
|
1241
|
+
*/
|
|
1242
|
+
function determineRunMode(form) {
|
|
1243
|
+
const explicitMode = form.metadata?.runMode;
|
|
1244
|
+
if (explicitMode) {
|
|
1245
|
+
const validation = validateRunMode(form, explicitMode);
|
|
1246
|
+
if (!validation.valid) return {
|
|
1247
|
+
success: false,
|
|
1248
|
+
error: validation.error
|
|
1249
|
+
};
|
|
1250
|
+
return {
|
|
1251
|
+
success: true,
|
|
1252
|
+
runMode: explicitMode,
|
|
1253
|
+
source: "explicit"
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const roles = getFieldRoles(form);
|
|
1257
|
+
if (roles.size === 0) return {
|
|
1258
|
+
success: false,
|
|
1259
|
+
error: "Form has no fields"
|
|
1260
|
+
};
|
|
1261
|
+
if (roles.size === 1 && roles.has(USER_ROLE)) return {
|
|
1262
|
+
success: true,
|
|
1263
|
+
runMode: "interactive",
|
|
1264
|
+
source: "inferred"
|
|
1265
|
+
};
|
|
1266
|
+
if (roles.size === 1 && roles.has(AGENT_ROLE)) {
|
|
1267
|
+
if (isResearchForm(form)) return {
|
|
1268
|
+
success: true,
|
|
1269
|
+
runMode: "research",
|
|
1270
|
+
source: "inferred"
|
|
1271
|
+
};
|
|
1272
|
+
return {
|
|
1273
|
+
success: true,
|
|
1274
|
+
runMode: "fill",
|
|
1275
|
+
source: "inferred"
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
success: false,
|
|
1280
|
+
error: `Cannot determine run mode. Form has roles: ${[...roles].join(", ")}. Add 'run_mode' to frontmatter: interactive, fill, or research.`
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Get a human-readable description of the run mode source.
|
|
1285
|
+
*/
|
|
1286
|
+
function formatRunModeSource(source) {
|
|
1287
|
+
return source === "explicit" ? "from frontmatter" : "inferred from field roles";
|
|
1288
|
+
}
|
|
1289
|
+
|
|
795
1290
|
//#endregion
|
|
796
1291
|
//#region src/cli/lib/versioning.ts
|
|
797
1292
|
/**
|
|
@@ -1432,277 +1927,216 @@ function showInteractiveOutro(patchCount, cancelled) {
|
|
|
1432
1927
|
}
|
|
1433
1928
|
|
|
1434
1929
|
//#endregion
|
|
1435
|
-
//#region src/cli/lib/
|
|
1436
|
-
/**
|
|
1437
|
-
|
|
1438
|
-
*
|
|
1439
|
-
* Provides a modern console experience:
|
|
1440
|
-
* - Syntax highlighting for markdown and YAML
|
|
1441
|
-
* - Pagination using system pager (less) when available
|
|
1442
|
-
* - Fallback to console output when not interactive
|
|
1443
|
-
*/
|
|
1930
|
+
//#region src/cli/lib/patchFormat.ts
|
|
1931
|
+
/** Maximum characters for a patch value display before truncation */
|
|
1932
|
+
const PATCH_VALUE_MAX_LENGTH = 1e3;
|
|
1444
1933
|
/**
|
|
1445
|
-
*
|
|
1934
|
+
* Truncate a string to max length with ellipsis if needed.
|
|
1446
1935
|
*/
|
|
1447
|
-
function
|
|
1448
|
-
|
|
1936
|
+
function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
|
|
1937
|
+
if (value.length <= maxLength) return value;
|
|
1938
|
+
return value.slice(0, maxLength) + "…";
|
|
1449
1939
|
}
|
|
1450
1940
|
/**
|
|
1451
|
-
*
|
|
1452
|
-
* Colorizes headers, code blocks, and other elements.
|
|
1941
|
+
* Format a patch value for display with truncation.
|
|
1453
1942
|
*/
|
|
1454
|
-
function
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
if (line.startsWith("## ")) {
|
|
1473
|
-
formatted.push(pc.bold(pc.blue(line)));
|
|
1474
|
-
continue;
|
|
1475
|
-
}
|
|
1476
|
-
if (line.startsWith("### ")) {
|
|
1477
|
-
formatted.push(pc.bold(pc.cyan(line)));
|
|
1478
|
-
continue;
|
|
1479
|
-
}
|
|
1480
|
-
if (line.startsWith("#### ")) {
|
|
1481
|
-
formatted.push(pc.bold(line));
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1484
|
-
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1485
|
-
return pc.yellow(code);
|
|
1486
|
-
});
|
|
1487
|
-
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
1488
|
-
return pc.bold(text);
|
|
1489
|
-
});
|
|
1490
|
-
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
1491
|
-
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
1492
|
-
});
|
|
1493
|
-
formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
|
|
1494
|
-
return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
|
|
1495
|
-
});
|
|
1496
|
-
formatted.push(formattedLine);
|
|
1943
|
+
function formatPatchValue(patch) {
|
|
1944
|
+
switch (patch.op) {
|
|
1945
|
+
case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1946
|
+
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
1947
|
+
case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
1948
|
+
case "set_single_select": return patch.selected ?? "(none)";
|
|
1949
|
+
case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
|
|
1950
|
+
case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
|
|
1951
|
+
case "clear_field": return "(cleared)";
|
|
1952
|
+
case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
|
|
1953
|
+
case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
|
|
1954
|
+
case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1955
|
+
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
1956
|
+
case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
1957
|
+
case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
1958
|
+
case "set_table": return patch.rows.length > 0 ? truncate(`[${patch.rows.length} rows]`) : "(empty)";
|
|
1959
|
+
case "add_note": return truncate(`note: ${patch.text}`);
|
|
1960
|
+
case "remove_note": return `(remove note ${patch.noteId})`;
|
|
1497
1961
|
}
|
|
1498
|
-
return formatted.join("\n");
|
|
1499
1962
|
}
|
|
1500
1963
|
/**
|
|
1501
|
-
*
|
|
1964
|
+
* Get a short display name for the patch operation type.
|
|
1502
1965
|
*/
|
|
1503
|
-
function
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
formatted.push(line);
|
|
1966
|
+
function formatPatchType(patch) {
|
|
1967
|
+
switch (patch.op) {
|
|
1968
|
+
case "set_string": return "string";
|
|
1969
|
+
case "set_number": return "number";
|
|
1970
|
+
case "set_string_list": return "string_list";
|
|
1971
|
+
case "set_single_select": return "select";
|
|
1972
|
+
case "set_multi_select": return "multi_select";
|
|
1973
|
+
case "set_checkboxes": return "checkboxes";
|
|
1974
|
+
case "clear_field": return "clear";
|
|
1975
|
+
case "skip_field": return "skip";
|
|
1976
|
+
case "abort_field": return "abort";
|
|
1977
|
+
case "set_url": return "url";
|
|
1978
|
+
case "set_url_list": return "url_list";
|
|
1979
|
+
case "set_date": return "date";
|
|
1980
|
+
case "set_year": return "year";
|
|
1981
|
+
case "set_table": return "table";
|
|
1982
|
+
case "add_note": return "note";
|
|
1983
|
+
case "remove_note": return "remove_note";
|
|
1522
1984
|
}
|
|
1523
|
-
return formatted.join("\n");
|
|
1524
|
-
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Format file content based on extension.
|
|
1527
|
-
*/
|
|
1528
|
-
function formatContent(content, filename) {
|
|
1529
|
-
if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
|
|
1530
|
-
if (filename.endsWith(".md")) return formatMarkdown$2(content);
|
|
1531
|
-
return content;
|
|
1532
1985
|
}
|
|
1986
|
+
|
|
1987
|
+
//#endregion
|
|
1988
|
+
//#region src/cli/lib/fillLogging.ts
|
|
1533
1989
|
/**
|
|
1534
|
-
*
|
|
1535
|
-
* Falls back to console.log if not interactive or pager unavailable.
|
|
1990
|
+
* Fill Logging Callbacks - Create FillCallbacks for unified CLI logging.
|
|
1536
1991
|
*
|
|
1537
|
-
*
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
"-F",
|
|
1551
|
-
"-K"
|
|
1552
|
-
], { stdio: [
|
|
1553
|
-
"pipe",
|
|
1554
|
-
"inherit",
|
|
1555
|
-
"inherit"
|
|
1556
|
-
] });
|
|
1557
|
-
pager.on("error", () => {
|
|
1558
|
-
console.log(header);
|
|
1559
|
-
console.log("");
|
|
1560
|
-
console.log(content);
|
|
1561
|
-
console.log("");
|
|
1562
|
-
resolve$1();
|
|
1563
|
-
});
|
|
1564
|
-
pager.on("close", () => {
|
|
1565
|
-
resolve$1();
|
|
1566
|
-
});
|
|
1567
|
-
pager.stdin.write(header + "\n\n");
|
|
1568
|
-
pager.stdin.write(content);
|
|
1569
|
-
pager.stdin.end();
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Load and display a file with formatting and pagination.
|
|
1992
|
+
* Provides consistent turn-by-turn logging across all CLI commands that
|
|
1993
|
+
* run form-filling (fill, run, examples). API consumers can also use
|
|
1994
|
+
* these callbacks or implement their own.
|
|
1995
|
+
*
|
|
1996
|
+
* Default output (always shown unless --quiet):
|
|
1997
|
+
* - Turn numbers with issues list (field IDs + issue types)
|
|
1998
|
+
* - Patches per turn (field ID + value)
|
|
1999
|
+
* - Completion status
|
|
2000
|
+
*
|
|
2001
|
+
* Verbose output (--verbose flag):
|
|
2002
|
+
* - Token counts per turn
|
|
2003
|
+
* - Tool call start/end with timing
|
|
2004
|
+
* - Detailed stats and LLM metadata
|
|
1574
2005
|
*/
|
|
1575
|
-
async function viewFile(filePath) {
|
|
1576
|
-
const content = readFileSync(filePath, "utf-8");
|
|
1577
|
-
const filename = basename(filePath);
|
|
1578
|
-
await displayWithPager(formatContent(content, filename), filename);
|
|
1579
|
-
}
|
|
1580
2006
|
/**
|
|
1581
|
-
*
|
|
2007
|
+
* Create FillCallbacks that produce standard CLI logging output.
|
|
1582
2008
|
*
|
|
1583
|
-
*
|
|
1584
|
-
* -
|
|
1585
|
-
* -
|
|
1586
|
-
* -
|
|
2009
|
+
* Default output (always shown unless --quiet):
|
|
2010
|
+
* - Turn numbers with issues list (field IDs + issue types)
|
|
2011
|
+
* - Patches per turn (field ID + value)
|
|
2012
|
+
* - Completion status
|
|
1587
2013
|
*
|
|
1588
|
-
*
|
|
2014
|
+
* Verbose output (--verbose flag):
|
|
2015
|
+
* - Token counts per turn
|
|
2016
|
+
* - Tool call start/end with timing
|
|
2017
|
+
* - Detailed stats and LLM metadata
|
|
1589
2018
|
*
|
|
1590
|
-
*
|
|
2019
|
+
* This is used by fill, run, and examples commands for consistent output.
|
|
2020
|
+
*
|
|
2021
|
+
* @param ctx - Command context for verbose/quiet flags
|
|
2022
|
+
* @param options - Optional spinner for tool progress
|
|
2023
|
+
* @returns FillCallbacks with all logging implemented
|
|
2024
|
+
*
|
|
2025
|
+
* @example
|
|
2026
|
+
* ```typescript
|
|
2027
|
+
* const callbacks = createFillLoggingCallbacks(ctx, { spinner });
|
|
2028
|
+
* const result = await fillForm({
|
|
2029
|
+
* form: formMarkdown,
|
|
2030
|
+
* model: 'anthropic/claude-sonnet-4-5',
|
|
2031
|
+
* enableWebSearch: true,
|
|
2032
|
+
* callbacks,
|
|
2033
|
+
* });
|
|
2034
|
+
* ```
|
|
1591
2035
|
*/
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
2036
|
+
function createFillLoggingCallbacks(ctx, options = {}) {
|
|
2037
|
+
return {
|
|
2038
|
+
onIssuesIdentified: ({ turnNumber, issues }) => {
|
|
2039
|
+
logInfo(ctx, `${pc.bold(`Turn ${turnNumber}:`)} ${formatTurnIssues(issues)}`);
|
|
2040
|
+
},
|
|
2041
|
+
onPatchesGenerated: ({ patches, stats }) => {
|
|
2042
|
+
logInfo(ctx, ` -> ${pc.yellow(String(patches.length))} patch(es):`);
|
|
2043
|
+
for (const patch of patches) {
|
|
2044
|
+
const typeName = formatPatchType(patch);
|
|
2045
|
+
const value = formatPatchValue(patch);
|
|
2046
|
+
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
2047
|
+
if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
2048
|
+
else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
|
|
2049
|
+
}
|
|
2050
|
+
if (stats && ctx.verbose) {
|
|
2051
|
+
logVerbose(ctx, ` Tokens: in=${stats.inputTokens ?? 0} out=${stats.outputTokens ?? 0}`);
|
|
2052
|
+
if (stats.toolCalls && stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
|
|
2053
|
+
}
|
|
2054
|
+
},
|
|
2055
|
+
onTurnComplete: ({ isComplete }) => {
|
|
2056
|
+
if (isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
2057
|
+
},
|
|
2058
|
+
onToolStart: ({ name }) => {
|
|
2059
|
+
if (name.includes("search")) options.spinner?.message(`Web search...`);
|
|
2060
|
+
logVerbose(ctx, ` Tool started: ${name}`);
|
|
2061
|
+
},
|
|
2062
|
+
onToolEnd: ({ name, durationMs, error }) => {
|
|
2063
|
+
if (error) logVerbose(ctx, ` Tool ${name} failed: ${error} (${durationMs}ms)`);
|
|
2064
|
+
else logVerbose(ctx, ` Tool ${name} completed (${durationMs}ms)`);
|
|
2065
|
+
},
|
|
2066
|
+
onLlmCallStart: ({ model }) => {
|
|
2067
|
+
logVerbose(ctx, ` LLM call: ${model}`);
|
|
2068
|
+
},
|
|
2069
|
+
onLlmCallEnd: ({ model, inputTokens, outputTokens }) => {
|
|
2070
|
+
logVerbose(ctx, ` LLM response: ${model} (in=${inputTokens} out=${outputTokens})`);
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
1622
2073
|
}
|
|
1623
2074
|
|
|
1624
2075
|
//#endregion
|
|
1625
|
-
//#region src/cli/commands/
|
|
2076
|
+
//#region src/cli/commands/run.ts
|
|
1626
2077
|
/**
|
|
1627
|
-
*
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
|
|
1638
|
-
console.log("");
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
/**
|
|
1642
|
-
* Display API availability status at startup.
|
|
2078
|
+
* Run command - Interactive launcher for running forms.
|
|
2079
|
+
*
|
|
2080
|
+
* Provides a menu-based interface for selecting and running forms
|
|
2081
|
+
* from the forms directory. Automatically detects run mode based
|
|
2082
|
+
* on frontmatter or field roles.
|
|
2083
|
+
*
|
|
2084
|
+
* Usage:
|
|
2085
|
+
* markform run # Browse forms, select, run
|
|
2086
|
+
* markform run movie.form.md # Run specific form directly
|
|
2087
|
+
* markform run --limit=50 # Override menu limit
|
|
1643
2088
|
*/
|
|
1644
|
-
function showApiStatus() {
|
|
1645
|
-
console.log("API Status:");
|
|
1646
|
-
for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1647
|
-
const info = getProviderInfo(provider);
|
|
1648
|
-
const hasKey = !!process.env[info.envVar];
|
|
1649
|
-
const status = hasKey ? pc.green("✓") : "○";
|
|
1650
|
-
const envVar = hasKey ? info.envVar : pc.yellow(info.envVar);
|
|
1651
|
-
console.log(` ${status} ${provider} (${envVar})`);
|
|
1652
|
-
}
|
|
1653
|
-
console.log("");
|
|
1654
|
-
}
|
|
1655
2089
|
/**
|
|
1656
|
-
*
|
|
2090
|
+
* Scan forms directory for .form.md files.
|
|
1657
2091
|
*/
|
|
1658
|
-
function
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2092
|
+
function scanFormsDirectory(formsDir) {
|
|
2093
|
+
const entries = [];
|
|
2094
|
+
try {
|
|
2095
|
+
const files = readdirSync(formsDir);
|
|
2096
|
+
for (const file of files) {
|
|
2097
|
+
if (!file.endsWith(".form.md")) continue;
|
|
2098
|
+
const fullPath = join(formsDir, file);
|
|
2099
|
+
try {
|
|
2100
|
+
const stat = statSync(fullPath);
|
|
2101
|
+
if (stat.isFile()) entries.push({
|
|
2102
|
+
path: fullPath,
|
|
2103
|
+
filename: file,
|
|
2104
|
+
mtime: stat.mtime
|
|
2105
|
+
});
|
|
2106
|
+
} catch {}
|
|
2107
|
+
}
|
|
2108
|
+
} catch {}
|
|
2109
|
+
entries.sort((a, b) => {
|
|
2110
|
+
const orderDiff = getExampleOrder(a.filename) - getExampleOrder(b.filename);
|
|
2111
|
+
if (orderDiff !== 0) return orderDiff;
|
|
2112
|
+
return a.filename.localeCompare(b.filename);
|
|
1673
2113
|
});
|
|
1674
|
-
return
|
|
2114
|
+
return entries;
|
|
1675
2115
|
}
|
|
1676
2116
|
/**
|
|
1677
|
-
*
|
|
1678
|
-
*/
|
|
1679
|
-
async function
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
|
|
1692
|
-
}
|
|
1693
|
-
});
|
|
1694
|
-
if (p.isCancel(customModel)) return null;
|
|
1695
|
-
return customModel;
|
|
2117
|
+
* Load form metadata for menu display.
|
|
2118
|
+
*/
|
|
2119
|
+
async function enrichFormEntry(entry) {
|
|
2120
|
+
try {
|
|
2121
|
+
const form = parseForm(await readFile$1(entry.path));
|
|
2122
|
+
const runModeResult = determineRunMode(form);
|
|
2123
|
+
return {
|
|
2124
|
+
...entry,
|
|
2125
|
+
title: form.schema.title,
|
|
2126
|
+
description: form.schema.description,
|
|
2127
|
+
runMode: runModeResult.success ? runModeResult.runMode : void 0
|
|
2128
|
+
};
|
|
2129
|
+
} catch {
|
|
2130
|
+
return entry;
|
|
1696
2131
|
}
|
|
1697
|
-
return selection;
|
|
1698
2132
|
}
|
|
1699
2133
|
/**
|
|
1700
|
-
* Build model options
|
|
2134
|
+
* Build model options for the select prompt.
|
|
1701
2135
|
*/
|
|
1702
|
-
function
|
|
2136
|
+
function buildModelOptions(webSearchOnly) {
|
|
1703
2137
|
const options = [];
|
|
1704
2138
|
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
1705
|
-
if (!hasWebSearchSupport(provider)) continue;
|
|
2139
|
+
if (webSearchOnly && !hasWebSearchSupport(provider)) continue;
|
|
1706
2140
|
const info = getProviderInfo(provider);
|
|
1707
2141
|
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
|
|
1708
2142
|
for (const model of models) options.push({
|
|
@@ -1719,22 +2153,23 @@ function buildWebSearchModelOptions() {
|
|
|
1719
2153
|
return options;
|
|
1720
2154
|
}
|
|
1721
2155
|
/**
|
|
1722
|
-
* Prompt user to select a model
|
|
2156
|
+
* Prompt user to select a model.
|
|
1723
2157
|
*/
|
|
1724
|
-
async function
|
|
1725
|
-
const modelOptions =
|
|
1726
|
-
if (modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
|
|
2158
|
+
async function promptForModel(webSearchRequired) {
|
|
2159
|
+
const modelOptions = buildModelOptions(webSearchRequired);
|
|
2160
|
+
if (webSearchRequired && modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
|
|
2161
|
+
const message = webSearchRequired ? "Select LLM model (web search required):" : "Select LLM model:";
|
|
1727
2162
|
const selection = await p.select({
|
|
1728
|
-
message
|
|
2163
|
+
message,
|
|
1729
2164
|
options: modelOptions
|
|
1730
2165
|
});
|
|
1731
2166
|
if (p.isCancel(selection)) return null;
|
|
1732
2167
|
if (selection === "custom") {
|
|
1733
2168
|
const customModel = await p.text({
|
|
1734
2169
|
message: "Model ID (provider/model-id):",
|
|
1735
|
-
placeholder: "
|
|
2170
|
+
placeholder: "anthropic/claude-sonnet-4-20250514",
|
|
1736
2171
|
validate: (value) => {
|
|
1737
|
-
if (!value.includes("/")) return "Format: provider/model-id (e.g.,
|
|
2172
|
+
if (!value.includes("/")) return "Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)";
|
|
1738
2173
|
}
|
|
1739
2174
|
});
|
|
1740
2175
|
if (p.isCancel(customModel)) return null;
|
|
@@ -1743,305 +2178,396 @@ async function promptForWebSearchModel() {
|
|
|
1743
2178
|
return selection;
|
|
1744
2179
|
}
|
|
1745
2180
|
/**
|
|
1746
|
-
*
|
|
1747
|
-
*
|
|
2181
|
+
* Collect user input interactively (without exporting).
|
|
2182
|
+
* Returns true if successful, false if cancelled.
|
|
1748
2183
|
*/
|
|
1749
|
-
async function
|
|
1750
|
-
const
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
resolveSpinner.stop(`✓ Model resolved: ${modelId}`);
|
|
1761
|
-
} catch (error) {
|
|
1762
|
-
resolveSpinner.error("Model resolution failed");
|
|
1763
|
-
throw error;
|
|
1764
|
-
}
|
|
1765
|
-
const harnessConfig = {
|
|
1766
|
-
maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
1767
|
-
maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
|
|
1768
|
-
maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
|
|
1769
|
-
targetRoles: [AGENT_ROLE],
|
|
1770
|
-
fillMode: "continue"
|
|
1771
|
-
};
|
|
1772
|
-
console.log("");
|
|
1773
|
-
console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
|
|
1774
|
-
const harness = createHarness(form, harnessConfig);
|
|
1775
|
-
const agent = createLiveAgent({
|
|
1776
|
-
model,
|
|
1777
|
-
provider,
|
|
1778
|
-
targetRole: AGENT_ROLE,
|
|
1779
|
-
enableWebSearch: true
|
|
1780
|
-
});
|
|
1781
|
-
p.log.step(pc.bold("Agent fill in progress..."));
|
|
1782
|
-
let stepResult = harness.step();
|
|
1783
|
-
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1784
|
-
console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
1785
|
-
const llmSpinner = createSpinner({
|
|
1786
|
-
type: "api",
|
|
1787
|
-
provider: providerName,
|
|
1788
|
-
model: modelName,
|
|
1789
|
-
turnNumber: stepResult.turnNumber
|
|
1790
|
-
});
|
|
1791
|
-
let response;
|
|
1792
|
-
try {
|
|
1793
|
-
response = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1794
|
-
llmSpinner.stop();
|
|
1795
|
-
} catch (error) {
|
|
1796
|
-
llmSpinner.error("LLM call failed");
|
|
1797
|
-
throw error;
|
|
1798
|
-
}
|
|
1799
|
-
const { patches, stats } = response;
|
|
1800
|
-
for (const patch of patches) {
|
|
1801
|
-
const typeName = formatPatchType(patch);
|
|
1802
|
-
const value = formatPatchValue(patch);
|
|
1803
|
-
const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
|
|
1804
|
-
if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
|
|
1805
|
-
else console.log(` (${typeName}) = ${pc.green(value)}`);
|
|
1806
|
-
}
|
|
1807
|
-
stepResult = harness.apply(patches, stepResult.issues);
|
|
1808
|
-
const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
|
|
1809
|
-
console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
|
|
1810
|
-
if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
|
|
2184
|
+
async function collectUserInput(form) {
|
|
2185
|
+
const targetRoles = [USER_ROLE];
|
|
2186
|
+
const inspectResult = inspect(form, { targetRoles });
|
|
2187
|
+
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
2188
|
+
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
2189
|
+
if (uniqueFieldIds.size === 0) return true;
|
|
2190
|
+
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
2191
|
+
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
2192
|
+
if (cancelled) {
|
|
2193
|
+
showInteractiveOutro(0, true);
|
|
2194
|
+
return false;
|
|
1811
2195
|
}
|
|
1812
|
-
if (
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
return {
|
|
1816
|
-
success: stepResult.isComplete,
|
|
1817
|
-
turnCount: harness.getTurnNumber()
|
|
1818
|
-
};
|
|
2196
|
+
if (patches.length > 0) applyPatches(form, patches);
|
|
2197
|
+
showInteractiveOutro(patches.length, false);
|
|
2198
|
+
return true;
|
|
1819
2199
|
}
|
|
1820
2200
|
/**
|
|
1821
|
-
* Run
|
|
1822
|
-
*
|
|
1823
|
-
* @param preselectedId Optional example ID to pre-select
|
|
1824
|
-
* @param formsDirOverride Optional forms directory override from CLI option
|
|
2201
|
+
* Run interactive fill workflow.
|
|
2202
|
+
* @returns ExportResult with paths to output files, or undefined if cancelled/no fields
|
|
1825
2203
|
*/
|
|
1826
|
-
async function
|
|
2204
|
+
async function runInteractiveWorkflow(form, filePath, formsDir) {
|
|
2205
|
+
const startTime = Date.now();
|
|
2206
|
+
const targetRoles = [USER_ROLE];
|
|
2207
|
+
const inspectResult = inspect(form, { targetRoles });
|
|
2208
|
+
const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
|
|
2209
|
+
const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
|
|
2210
|
+
if (uniqueFieldIds.size === 0) {
|
|
2211
|
+
p.log.info("No user-role fields to fill.");
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
2215
|
+
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
2216
|
+
if (cancelled) {
|
|
2217
|
+
showInteractiveOutro(0, true);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (patches.length > 0) applyPatches(form, patches);
|
|
2221
|
+
await ensureFormsDir(formsDir);
|
|
2222
|
+
const exportResult = await exportMultiFormat(form, generateVersionedPathInFormsDir(filePath, formsDir));
|
|
2223
|
+
showInteractiveOutro(patches.length, false);
|
|
2224
|
+
console.log("");
|
|
2225
|
+
p.log.success("Outputs:");
|
|
2226
|
+
console.log(` ${formatPath(exportResult.reportPath)} ${pc.dim("(output report)")}`);
|
|
2227
|
+
console.log(` ${formatPath(exportResult.yamlPath)} ${pc.dim("(output values)")}`);
|
|
2228
|
+
console.log(` ${formatPath(exportResult.formPath)} ${pc.dim("(filled markform source)")}`);
|
|
2229
|
+
console.log(` ${formatPath(exportResult.schemaPath)} ${pc.dim("(JSON Schema)")}`);
|
|
2230
|
+
logTiming({
|
|
2231
|
+
verbose: false,
|
|
2232
|
+
format: "console",
|
|
2233
|
+
dryRun: false,
|
|
2234
|
+
quiet: false,
|
|
2235
|
+
overwrite: false
|
|
2236
|
+
}, "Fill time", Date.now() - startTime);
|
|
2237
|
+
return exportResult;
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Run agent fill workflow using fillForm with logging callbacks.
|
|
2241
|
+
* @returns ExportResult with paths to output files
|
|
2242
|
+
*/
|
|
2243
|
+
async function runAgentFillWorkflow(form, modelId, formsDir, filePath, isResearch, overwrite, ctx) {
|
|
1827
2244
|
const startTime = Date.now();
|
|
1828
|
-
|
|
1829
|
-
const
|
|
2245
|
+
const maxTurns = DEFAULT_MAX_TURNS;
|
|
2246
|
+
const maxPatchesPerTurn = isResearch ? DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN : DEFAULT_MAX_PATCHES_PER_TURN;
|
|
2247
|
+
const maxIssuesPerTurn = isResearch ? DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN : DEFAULT_MAX_ISSUES_PER_TURN;
|
|
2248
|
+
logVerbose(ctx, `Config: max_turns=${maxTurns}, max_issues_per_turn=${maxIssuesPerTurn}, max_patches_per_turn=${maxPatchesPerTurn}`);
|
|
2249
|
+
const callbacks = createFillLoggingCallbacks(ctx);
|
|
2250
|
+
const workflowLabel = isResearch ? "Research" : "Agent fill";
|
|
2251
|
+
p.log.step(pc.bold(`${workflowLabel} in progress...`));
|
|
2252
|
+
const result = await fillForm({
|
|
2253
|
+
form,
|
|
2254
|
+
model: modelId,
|
|
2255
|
+
maxTurns,
|
|
2256
|
+
maxPatchesPerTurn,
|
|
2257
|
+
maxIssuesPerTurn,
|
|
2258
|
+
targetRoles: [AGENT_ROLE],
|
|
2259
|
+
fillMode: overwrite ? "overwrite" : "continue",
|
|
2260
|
+
enableWebSearch: isResearch,
|
|
2261
|
+
callbacks
|
|
2262
|
+
});
|
|
2263
|
+
if (result.status.ok) p.log.success(pc.green(`Form completed in ${result.turns} turn(s)`));
|
|
2264
|
+
else if (result.status.reason === "max_turns") p.log.warn(pc.yellow(`Max turns reached (${maxTurns})`));
|
|
2265
|
+
else throw new Error(result.status.message ?? `Fill failed: ${result.status.reason}`);
|
|
1830
2266
|
await ensureFormsDir(formsDir);
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2267
|
+
const outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
|
|
2268
|
+
const exportResult = await exportMultiFormat(result.form, outputPath);
|
|
2269
|
+
console.log("");
|
|
2270
|
+
p.log.success(`${workflowLabel} complete. Outputs:`);
|
|
2271
|
+
console.log(` ${formatPath(exportResult.reportPath)} ${pc.dim("(output report)")}`);
|
|
2272
|
+
console.log(` ${formatPath(exportResult.yamlPath)} ${pc.dim("(output values)")}`);
|
|
2273
|
+
console.log(` ${formatPath(exportResult.formPath)} ${pc.dim("(filled markform source)")}`);
|
|
2274
|
+
console.log(` ${formatPath(exportResult.schemaPath)} ${pc.dim("(JSON Schema)")}`);
|
|
2275
|
+
logTiming(ctx, isResearch ? "Research time" : "Fill time", Date.now() - startTime);
|
|
2276
|
+
return exportResult;
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Run a form directly (callable from other commands).
|
|
2280
|
+
* This executes the same workflow as `markform run <file>`.
|
|
2281
|
+
*
|
|
2282
|
+
* @param selectedPath - Path to the form file
|
|
2283
|
+
* @param formsDir - Directory for output files
|
|
2284
|
+
* @param overwrite - Whether to overwrite existing field values
|
|
2285
|
+
* @param ctx - Optional command context for logging (defaults to non-verbose/quiet)
|
|
2286
|
+
* @returns ExportResult with paths to output files, or undefined if cancelled/no output
|
|
2287
|
+
*/
|
|
2288
|
+
async function runForm(selectedPath, formsDir, overwrite, ctx) {
|
|
2289
|
+
const effectiveCtx = ctx ?? {
|
|
2290
|
+
verbose: false,
|
|
2291
|
+
quiet: false,
|
|
2292
|
+
dryRun: false,
|
|
2293
|
+
format: "console",
|
|
2294
|
+
overwrite
|
|
2295
|
+
};
|
|
2296
|
+
const form = parseForm(await readFile$1(selectedPath));
|
|
2297
|
+
const runModeResult = determineRunMode(form);
|
|
2298
|
+
if (!runModeResult.success) throw new Error(runModeResult.error);
|
|
2299
|
+
const { runMode } = runModeResult;
|
|
2300
|
+
switch (runMode) {
|
|
2301
|
+
case "interactive": return runInteractiveWorkflow(form, selectedPath, formsDir);
|
|
2302
|
+
case "fill":
|
|
2303
|
+
case "research": {
|
|
2304
|
+
const isResearch = runMode === "research";
|
|
2305
|
+
if (!await collectUserInput(form)) {
|
|
2306
|
+
p.cancel("Cancelled.");
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
const modelId = await promptForModel(isResearch);
|
|
2310
|
+
if (!modelId) {
|
|
2311
|
+
p.cancel("Cancelled.");
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
return runAgentFillWorkflow(form, modelId, formsDir, selectedPath, isResearch, overwrite, effectiveCtx);
|
|
1846
2315
|
}
|
|
1847
|
-
selectedId = selection;
|
|
1848
2316
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
if (
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Register the run command.
|
|
2320
|
+
*/
|
|
2321
|
+
function registerRunCommand(program) {
|
|
2322
|
+
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) => {
|
|
2323
|
+
const ctx = getCommandContext(cmd);
|
|
2324
|
+
try {
|
|
2325
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2326
|
+
const limit = options.limit ? parseInt(options.limit, 10) : MAX_FORMS_IN_MENU;
|
|
2327
|
+
let selectedPath;
|
|
2328
|
+
if (file) {
|
|
2329
|
+
selectedPath = file.startsWith("/") ? file : join(formsDir, file);
|
|
2330
|
+
if (!selectedPath.endsWith(".form.md") && !selectedPath.endsWith(".md")) selectedPath = `${selectedPath}.form.md`;
|
|
2331
|
+
} else {
|
|
2332
|
+
p.intro(pc.bgCyan(pc.black(" markform run ")));
|
|
2333
|
+
const entries = scanFormsDirectory(formsDir);
|
|
2334
|
+
if (entries.length === 0) {
|
|
2335
|
+
p.log.warn(`No forms found in ${formatPath(formsDir)}`);
|
|
2336
|
+
console.log("");
|
|
2337
|
+
console.log(`Run ${pc.cyan("'markform examples'")} to get started.`);
|
|
2338
|
+
p.outro("");
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
const entriesToShow = entries.slice(0, limit);
|
|
2342
|
+
const enrichedEntries = await Promise.all(entriesToShow.map(enrichFormEntry));
|
|
2343
|
+
const menuOptions = enrichedEntries.map((entry) => ({
|
|
2344
|
+
value: entry.path,
|
|
2345
|
+
label: formatFormLabel(entry),
|
|
2346
|
+
hint: formatFormHint(entry)
|
|
2347
|
+
}));
|
|
2348
|
+
const defaultExample = getExampleById(DEFAULT_EXAMPLE_ID);
|
|
2349
|
+
const initialValue = enrichedEntries.find((e) => e.filename === defaultExample?.filename)?.path;
|
|
2350
|
+
if (entries.length > limit) console.log(pc.dim(`Showing ${limit} of ${entries.length} forms`));
|
|
2351
|
+
const selection = await p.select({
|
|
2352
|
+
message: "Select a form to run:",
|
|
2353
|
+
options: menuOptions,
|
|
2354
|
+
initialValue
|
|
2355
|
+
});
|
|
2356
|
+
if (p.isCancel(selection)) {
|
|
2357
|
+
p.cancel("Cancelled.");
|
|
2358
|
+
process.exit(0);
|
|
2359
|
+
}
|
|
2360
|
+
selectedPath = selection;
|
|
2361
|
+
}
|
|
2362
|
+
logVerbose(ctx, `Reading form: ${selectedPath}`);
|
|
2363
|
+
const form = parseForm(await readFile$1(selectedPath));
|
|
2364
|
+
const runModeResult = determineRunMode(form);
|
|
2365
|
+
if (!runModeResult.success) {
|
|
2366
|
+
logError(runModeResult.error);
|
|
2367
|
+
process.exit(1);
|
|
2368
|
+
}
|
|
2369
|
+
const { runMode, source } = runModeResult;
|
|
2370
|
+
logInfo(ctx, `Run mode: ${runMode} (${formatRunModeSource(source)})`);
|
|
2371
|
+
switch (runMode) {
|
|
2372
|
+
case "interactive":
|
|
2373
|
+
await runInteractiveWorkflow(form, selectedPath, formsDir);
|
|
2374
|
+
break;
|
|
2375
|
+
case "fill":
|
|
2376
|
+
case "research": {
|
|
2377
|
+
const isResearch = runMode === "research";
|
|
2378
|
+
if (!await collectUserInput(form)) {
|
|
2379
|
+
p.cancel("Cancelled.");
|
|
2380
|
+
process.exit(0);
|
|
2381
|
+
}
|
|
2382
|
+
const modelId = await promptForModel(isResearch);
|
|
2383
|
+
if (!modelId) {
|
|
2384
|
+
p.cancel("Cancelled.");
|
|
2385
|
+
process.exit(0);
|
|
2386
|
+
}
|
|
2387
|
+
await runAgentFillWorkflow(form, modelId, formsDir, selectedPath, isResearch, ctx.overwrite, ctx);
|
|
2388
|
+
break;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
if (!file) p.outro("Happy form filling!");
|
|
2392
|
+
} catch (error) {
|
|
2393
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2394
|
+
process.exit(1);
|
|
1861
2395
|
}
|
|
1862
2396
|
});
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
//#endregion
|
|
2400
|
+
//#region src/cli/commands/examples.ts
|
|
2401
|
+
/**
|
|
2402
|
+
* Examples command - Copy bundled example forms to the forms directory.
|
|
2403
|
+
*
|
|
2404
|
+
* This command provides a simple way to get started with markform by
|
|
2405
|
+
* copying bundled example forms to your local forms directory.
|
|
2406
|
+
*
|
|
2407
|
+
* Usage:
|
|
2408
|
+
* markform examples # Copy all bundled examples to ./forms/
|
|
2409
|
+
* markform examples --list # List bundled examples (no copy)
|
|
2410
|
+
* markform examples --name=foo # Copy specific example only
|
|
2411
|
+
*/
|
|
2412
|
+
/**
|
|
2413
|
+
* Print non-interactive list of examples.
|
|
2414
|
+
*/
|
|
2415
|
+
function printExamplesList() {
|
|
2416
|
+
console.log(pc.bold("Available examples:\n"));
|
|
2417
|
+
const examples = getAllExamplesWithMetadata();
|
|
2418
|
+
for (const example of examples) {
|
|
2419
|
+
const typeLabel = example.type === "research" ? pc.magenta("[research]") : pc.blue("[fill]");
|
|
2420
|
+
console.log(` ${pc.cyan(example.id)} ${typeLabel}`);
|
|
2421
|
+
console.log(` ${pc.bold(example.title ?? example.id)}`);
|
|
2422
|
+
console.log(` ${example.description ?? "No description"}`);
|
|
2423
|
+
console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
|
|
2424
|
+
console.log("");
|
|
1866
2425
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Copy an example form to the forms directory.
|
|
2429
|
+
*
|
|
2430
|
+
* @returns true if copied, false if skipped
|
|
2431
|
+
*/
|
|
2432
|
+
async function copyExample(exampleId, formsDir, overwrite, _quiet) {
|
|
2433
|
+
const example = getExampleById(exampleId);
|
|
2434
|
+
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
2435
|
+
const outputPath = join(formsDir, example.filename);
|
|
1869
2436
|
if (existsSync(outputPath)) {
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
p.cancel("Cancelled.");
|
|
1876
|
-
process.exit(0);
|
|
1877
|
-
}
|
|
2437
|
+
if (!overwrite) return {
|
|
2438
|
+
copied: false,
|
|
2439
|
+
skipped: true,
|
|
2440
|
+
path: outputPath
|
|
2441
|
+
};
|
|
1878
2442
|
}
|
|
1879
|
-
|
|
2443
|
+
await writeFile(outputPath, loadExampleContent(exampleId));
|
|
2444
|
+
return {
|
|
2445
|
+
copied: true,
|
|
2446
|
+
skipped: false,
|
|
2447
|
+
path: outputPath
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Scan forms directory and enrich entries with metadata.
|
|
2452
|
+
*/
|
|
2453
|
+
async function getFormEntries(formsDir) {
|
|
2454
|
+
const entries = [];
|
|
1880
2455
|
try {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
logTiming({
|
|
1899
|
-
verbose: false,
|
|
1900
|
-
format: "console",
|
|
1901
|
-
dryRun: false,
|
|
1902
|
-
quiet: false
|
|
1903
|
-
}, "Total time", Date.now() - startTime);
|
|
1904
|
-
p.outro("Form scaffolded with no fields to fill.");
|
|
1905
|
-
return;
|
|
1906
|
-
}
|
|
1907
|
-
} else {
|
|
1908
|
-
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
1909
|
-
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
1910
|
-
if (cancelled) {
|
|
1911
|
-
showInteractiveOutro(0, true);
|
|
1912
|
-
process.exit(1);
|
|
2456
|
+
const files = readdirSync(formsDir);
|
|
2457
|
+
for (const file of files) {
|
|
2458
|
+
if (!file.endsWith(".form.md")) continue;
|
|
2459
|
+
const fullPath = join(formsDir, file);
|
|
2460
|
+
try {
|
|
2461
|
+
if (statSync(fullPath).isFile()) {
|
|
2462
|
+
const form = parseForm(await readFile$1(fullPath));
|
|
2463
|
+
const runModeResult = determineRunMode(form);
|
|
2464
|
+
entries.push({
|
|
2465
|
+
path: fullPath,
|
|
2466
|
+
filename: file,
|
|
2467
|
+
title: form.schema.title,
|
|
2468
|
+
description: form.schema.description,
|
|
2469
|
+
runMode: runModeResult.success ? runModeResult.runMode : void 0
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
} catch {}
|
|
1913
2473
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2474
|
+
} catch {}
|
|
2475
|
+
return entries;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Copy all examples to the forms directory.
|
|
2479
|
+
* Returns { copied, skipped } counts for the caller to handle prompts.
|
|
2480
|
+
*/
|
|
2481
|
+
async function copyAllExamples(formsDir, overwrite, quiet) {
|
|
2482
|
+
const examples = getAllExamplesWithMetadata();
|
|
2483
|
+
const total = examples.length;
|
|
2484
|
+
if (!quiet) {
|
|
2485
|
+
console.log(`Copying ${total} example forms to ${formatPath(formsDir)}...`);
|
|
1917
2486
|
console.log("");
|
|
1918
|
-
p.log.success("Outputs:");
|
|
1919
|
-
console.log(` ${formatPath(userFillOutputs.reportPath)} ${pc.dim("(output report)")}`);
|
|
1920
|
-
console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(output values)")}`);
|
|
1921
|
-
console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(filled markform source)")}`);
|
|
1922
|
-
logTiming({
|
|
1923
|
-
verbose: false,
|
|
1924
|
-
format: "console",
|
|
1925
|
-
dryRun: false,
|
|
1926
|
-
quiet: false
|
|
1927
|
-
}, "Fill time", Date.now() - startTime);
|
|
1928
2487
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2488
|
+
let copied = 0;
|
|
2489
|
+
let skipped = 0;
|
|
2490
|
+
for (const example of examples) {
|
|
2491
|
+
const result = await copyExample(example.id, formsDir, overwrite, quiet);
|
|
2492
|
+
if (result.copied) {
|
|
2493
|
+
copied++;
|
|
2494
|
+
if (!quiet) console.log(formatFormLogLine(example, ` ${pc.green("✓")}`));
|
|
2495
|
+
} else if (result.skipped) {
|
|
2496
|
+
skipped++;
|
|
2497
|
+
if (!quiet) console.log(`${formatFormLogLine(example, ` ${pc.yellow("○")}`)} ${pc.dim("(exists, skipped)")}`);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
if (!quiet) {
|
|
1932
2501
|
console.log("");
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2502
|
+
if (skipped > 0) console.log(pc.yellow(`Skipped ${skipped} existing file(s). Use --overwrite to replace them.`));
|
|
2503
|
+
console.log(pc.green(`Done! Copied ${copied} example form(s) to ${formatPath(formsDir)}`));
|
|
2504
|
+
}
|
|
2505
|
+
return {
|
|
2506
|
+
copied,
|
|
2507
|
+
skipped
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Show form selection menu and return the selected path.
|
|
2512
|
+
*/
|
|
2513
|
+
async function showFormMenu(formsDir) {
|
|
2514
|
+
const entries = await getFormEntries(formsDir);
|
|
2515
|
+
if (entries.length === 0) return null;
|
|
2516
|
+
const sortedEntries = [...entries].sort((a, b) => {
|
|
2517
|
+
return getExampleOrder(a.filename) - getExampleOrder(b.filename);
|
|
2518
|
+
});
|
|
2519
|
+
const defaultExample = getExampleById(DEFAULT_EXAMPLE_ID);
|
|
2520
|
+
const defaultIndex = sortedEntries.findIndex((e) => e.filename === defaultExample?.filename);
|
|
2521
|
+
const menuOptions = sortedEntries.map((entry) => ({
|
|
2522
|
+
value: entry.path,
|
|
2523
|
+
label: formatFormLabel(entry),
|
|
2524
|
+
hint: formatFormHint(entry)
|
|
2525
|
+
}));
|
|
2526
|
+
const initialValue = (defaultIndex >= 0 ? sortedEntries[defaultIndex] : void 0)?.path;
|
|
2527
|
+
const selection = await p.select({
|
|
2528
|
+
message: "Select a form to run:",
|
|
2529
|
+
options: menuOptions,
|
|
2530
|
+
initialValue
|
|
2531
|
+
});
|
|
2532
|
+
if (p.isCancel(selection)) return null;
|
|
2533
|
+
return selection;
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Copy a specific example to the forms directory.
|
|
2537
|
+
*/
|
|
2538
|
+
async function copySingleExample(exampleId, formsDir, overwrite, quiet) {
|
|
2539
|
+
const example = getExampleById(exampleId);
|
|
2540
|
+
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
2541
|
+
if (!quiet) console.log(`Copying ${example.filename} to ${formatPath(formsDir)}...`);
|
|
2542
|
+
const result = await copyExample(exampleId, formsDir, overwrite, quiet);
|
|
2543
|
+
if (result.copied) {
|
|
2544
|
+
if (!quiet) {
|
|
2545
|
+
console.log(formatFormLogLine(example, ` ${pc.green("✓")}`));
|
|
1941
2546
|
console.log("");
|
|
1942
|
-
|
|
1943
|
-
console.log(`
|
|
1944
|
-
console.log(cliCommand);
|
|
1945
|
-
if (userFillOutputs) await showFileViewerChooser([
|
|
1946
|
-
{
|
|
1947
|
-
path: userFillOutputs.reportPath,
|
|
1948
|
-
label: "Report",
|
|
1949
|
-
hint: "output report"
|
|
1950
|
-
},
|
|
1951
|
-
{
|
|
1952
|
-
path: userFillOutputs.yamlPath,
|
|
1953
|
-
label: "Values",
|
|
1954
|
-
hint: "output values"
|
|
1955
|
-
},
|
|
1956
|
-
{
|
|
1957
|
-
path: userFillOutputs.formPath,
|
|
1958
|
-
label: "Form",
|
|
1959
|
-
hint: "filled markform source"
|
|
1960
|
-
}
|
|
1961
|
-
]);
|
|
1962
|
-
p.outro("Happy form filling!");
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
|
|
1966
|
-
if (!modelId) {
|
|
1967
|
-
p.cancel("Cancelled.");
|
|
1968
|
-
process.exit(0);
|
|
2547
|
+
console.log(pc.green("Done!"));
|
|
2548
|
+
console.log(`Run ${pc.cyan(`'markform run ${example.filename}'`)} to try it.`);
|
|
1969
2549
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
initialValue: agentDefaultFilename,
|
|
1974
|
-
validate: (value) => {
|
|
1975
|
-
if (!value.trim()) return "Filename is required";
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
if (p.isCancel(agentFilenameResult)) {
|
|
1979
|
-
p.cancel("Cancelled.");
|
|
1980
|
-
process.exit(0);
|
|
1981
|
-
}
|
|
1982
|
-
const agentOutputPath = join(formsDir, agentFilenameResult);
|
|
1983
|
-
const agentStartTime = Date.now();
|
|
1984
|
-
const timingLabel = isResearchExample ? "Research time" : "Agent fill time";
|
|
1985
|
-
const configOverrides = isResearchExample ? {
|
|
1986
|
-
maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
|
|
1987
|
-
maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
|
|
1988
|
-
} : void 0;
|
|
1989
|
-
try {
|
|
1990
|
-
const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
|
|
1991
|
-
logTiming({
|
|
1992
|
-
verbose: false,
|
|
1993
|
-
format: "console",
|
|
1994
|
-
dryRun: false,
|
|
1995
|
-
quiet: false
|
|
1996
|
-
}, timingLabel, Date.now() - agentStartTime);
|
|
1997
|
-
const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, agentOutputPath);
|
|
1998
|
-
console.log("");
|
|
1999
|
-
const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
|
|
2000
|
-
p.log.success(successMessage);
|
|
2001
|
-
console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
|
|
2002
|
-
console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
|
|
2003
|
-
console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
|
|
2004
|
-
if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
|
|
2005
|
-
await showFileViewerChooser([
|
|
2006
|
-
{
|
|
2007
|
-
path: reportPath,
|
|
2008
|
-
label: "Report",
|
|
2009
|
-
hint: "output report"
|
|
2010
|
-
},
|
|
2011
|
-
{
|
|
2012
|
-
path: yamlPath,
|
|
2013
|
-
label: "Values",
|
|
2014
|
-
hint: "output values"
|
|
2015
|
-
},
|
|
2016
|
-
{
|
|
2017
|
-
path: formPath,
|
|
2018
|
-
label: "Form",
|
|
2019
|
-
hint: "filled markform source"
|
|
2020
|
-
}
|
|
2021
|
-
]);
|
|
2022
|
-
} catch (error) {
|
|
2023
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2024
|
-
const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
|
|
2025
|
-
p.log.error(`${failMessage}: ${message}`);
|
|
2550
|
+
} else if (result.skipped) {
|
|
2551
|
+
if (!quiet) {
|
|
2552
|
+
console.log(`${formatFormLogLine(example, ` ${pc.yellow("○")}`)} ${pc.dim("(exists, skipped)")}`);
|
|
2026
2553
|
console.log("");
|
|
2027
|
-
console.log(
|
|
2028
|
-
const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
|
|
2029
|
-
console.log(retryCommand);
|
|
2554
|
+
console.log(pc.yellow(`File already exists. Use --overwrite to replace it.`));
|
|
2030
2555
|
}
|
|
2031
2556
|
}
|
|
2032
|
-
p.outro("Happy form filling!");
|
|
2033
2557
|
}
|
|
2034
2558
|
/**
|
|
2035
2559
|
* Register the examples command.
|
|
2036
2560
|
*/
|
|
2037
2561
|
function registerExamplesCommand(program) {
|
|
2038
|
-
program.command("examples").description("
|
|
2562
|
+
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) => {
|
|
2039
2563
|
const ctx = getCommandContext(cmd);
|
|
2040
2564
|
try {
|
|
2041
2565
|
if (options.list) {
|
|
2042
2566
|
printExamplesList();
|
|
2043
2567
|
return;
|
|
2044
2568
|
}
|
|
2569
|
+
const formsDir = getFormsDir(ctx.formsDir);
|
|
2570
|
+
await ensureFormsDir(formsDir);
|
|
2045
2571
|
if (options.name) {
|
|
2046
2572
|
if (!getExampleById(options.name)) {
|
|
2047
2573
|
logError(`Unknown example: ${options.name}`);
|
|
@@ -2049,8 +2575,35 @@ function registerExamplesCommand(program) {
|
|
|
2049
2575
|
for (const ex of EXAMPLE_DEFINITIONS) console.log(` ${ex.id}`);
|
|
2050
2576
|
process.exit(1);
|
|
2051
2577
|
}
|
|
2578
|
+
await copySingleExample(options.name, formsDir, ctx.overwrite, ctx.quiet);
|
|
2579
|
+
} else {
|
|
2580
|
+
const { copied, skipped } = await copyAllExamples(formsDir, ctx.overwrite, ctx.quiet);
|
|
2581
|
+
if (!ctx.quiet && (copied > 0 || skipped > 0)) {
|
|
2582
|
+
console.log("");
|
|
2583
|
+
const wantToRun = await p.confirm({
|
|
2584
|
+
message: "Do you want to try running a form?",
|
|
2585
|
+
initialValue: true
|
|
2586
|
+
});
|
|
2587
|
+
if (p.isCancel(wantToRun) || !wantToRun) {
|
|
2588
|
+
console.log("");
|
|
2589
|
+
console.log(`Run ${pc.cyan("'markform run'")} to select and run a form later.`);
|
|
2590
|
+
} else {
|
|
2591
|
+
const selectedPath = await showFormMenu(formsDir);
|
|
2592
|
+
if (selectedPath) {
|
|
2593
|
+
console.log("");
|
|
2594
|
+
const exportResult = await runForm(selectedPath, formsDir, ctx.overwrite);
|
|
2595
|
+
if (exportResult) {
|
|
2596
|
+
console.log("");
|
|
2597
|
+
const wantToBrowse = await p.confirm({
|
|
2598
|
+
message: "Would you like to view the output files?",
|
|
2599
|
+
initialValue: true
|
|
2600
|
+
});
|
|
2601
|
+
if (!p.isCancel(wantToBrowse) && wantToBrowse) await browseOutputFiles(exportResult.formPath.replace(/\.form\.md$/, ""));
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2052
2606
|
}
|
|
2053
|
-
await runInteractiveFlow(options.name, ctx.formsDir);
|
|
2054
2607
|
} catch (error) {
|
|
2055
2608
|
logError(error instanceof Error ? error.message : String(error));
|
|
2056
2609
|
process.exit(1);
|
|
@@ -2256,13 +2809,14 @@ function registerFillCommand(program) {
|
|
|
2256
2809
|
logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
|
|
2257
2810
|
showInteractiveOutro(patches.length, false);
|
|
2258
2811
|
} else {
|
|
2259
|
-
const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, outputPath$1);
|
|
2812
|
+
const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(form, outputPath$1);
|
|
2260
2813
|
showInteractiveOutro(patches.length, false);
|
|
2261
2814
|
console.log("");
|
|
2262
2815
|
p.log.success("Outputs:");
|
|
2263
2816
|
console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
|
|
2264
2817
|
console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
|
|
2265
2818
|
console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
|
|
2819
|
+
console.log(` ${formatPath(schemaPath)} ${pc.dim("(JSON Schema)")}`);
|
|
2266
2820
|
}
|
|
2267
2821
|
logTiming(ctx, "Fill time", durationMs$1);
|
|
2268
2822
|
if (patches.length > 0) {
|
|
@@ -2543,7 +3097,7 @@ function formatFieldValue(value, useColors) {
|
|
|
2543
3097
|
/**
|
|
2544
3098
|
* Format inspect report for console output.
|
|
2545
3099
|
*/
|
|
2546
|
-
function formatConsoleReport$
|
|
3100
|
+
function formatConsoleReport$2(report, useColors) {
|
|
2547
3101
|
const lines = [];
|
|
2548
3102
|
const bold = useColors ? pc.bold : (s) => s;
|
|
2549
3103
|
const dim = useColors ? pc.dim : (s) => s;
|
|
@@ -2656,7 +3210,7 @@ function registerInspectCommand(program) {
|
|
|
2656
3210
|
severity: issue.severity,
|
|
2657
3211
|
blockedBy: issue.blockedBy
|
|
2658
3212
|
}))
|
|
2659
|
-
}, (data, useColors) => formatConsoleReport$
|
|
3213
|
+
}, (data, useColors) => formatConsoleReport$2(data, useColors));
|
|
2660
3214
|
console.log(output);
|
|
2661
3215
|
} catch (error) {
|
|
2662
3216
|
logError(error instanceof Error ? error.message : String(error));
|
|
@@ -4097,15 +4651,16 @@ function registerResearchCommand(program) {
|
|
|
4097
4651
|
logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
|
|
4098
4652
|
logInfo(ctx, `Turns: ${result.totalTurns}`);
|
|
4099
4653
|
if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
|
|
4100
|
-
const { reportPath, yamlPath, formPath } = await exportMultiFormat(result.form, outputPath);
|
|
4654
|
+
const { reportPath, yamlPath, formPath, schemaPath } = await exportMultiFormat(result.form, outputPath);
|
|
4101
4655
|
logSuccess(ctx, "Outputs:");
|
|
4102
4656
|
console.log(` ${reportPath} ${pc.dim("(output report)")}`);
|
|
4103
4657
|
console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
|
|
4104
4658
|
console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
|
|
4659
|
+
console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
|
|
4105
4660
|
if (options.transcript && result.transcript) {
|
|
4106
|
-
const { serializeSession: serializeSession$1 } = await import("./session-
|
|
4661
|
+
const { serializeSession: serializeSession$1 } = await import("./session-DSTNiHza.mjs");
|
|
4107
4662
|
const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
|
|
4108
|
-
const { writeFile: writeFile$1 } = await import("./shared-
|
|
4663
|
+
const { writeFile: writeFile$1 } = await import("./shared-C9yW5FLZ.mjs");
|
|
4109
4664
|
await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
|
|
4110
4665
|
logInfo(ctx, `Transcript: ${transcriptPath}`);
|
|
4111
4666
|
}
|
|
@@ -4117,6 +4672,195 @@ function registerResearchCommand(program) {
|
|
|
4117
4672
|
});
|
|
4118
4673
|
}
|
|
4119
4674
|
|
|
4675
|
+
//#endregion
|
|
4676
|
+
//#region src/cli/commands/schema.ts
|
|
4677
|
+
const VALID_DRAFTS = [
|
|
4678
|
+
"2020-12",
|
|
4679
|
+
"2019-09",
|
|
4680
|
+
"draft-07"
|
|
4681
|
+
];
|
|
4682
|
+
/**
|
|
4683
|
+
* Register the schema command.
|
|
4684
|
+
*/
|
|
4685
|
+
function registerSchemaCommand(program) {
|
|
4686
|
+
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) => {
|
|
4687
|
+
const ctx = getCommandContext(cmd);
|
|
4688
|
+
try {
|
|
4689
|
+
const draft = options.draft ?? "2020-12";
|
|
4690
|
+
if (!VALID_DRAFTS.includes(draft)) throw new Error(`Invalid draft version: ${options.draft}. Valid options: ${VALID_DRAFTS.join(", ")}`);
|
|
4691
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
4692
|
+
const content = await readFile$1(file);
|
|
4693
|
+
logVerbose(ctx, "Parsing form...");
|
|
4694
|
+
const form = parseForm(content);
|
|
4695
|
+
logVerbose(ctx, "Generating JSON Schema...");
|
|
4696
|
+
const result = formToJsonSchema(form, {
|
|
4697
|
+
includeExtensions: !options.pure,
|
|
4698
|
+
draft
|
|
4699
|
+
});
|
|
4700
|
+
if (ctx.format === "yaml") console.log(YAML.stringify(result.schema));
|
|
4701
|
+
else if (options.compact) console.log(JSON.stringify(result.schema));
|
|
4702
|
+
else console.log(JSON.stringify(result.schema, null, 2));
|
|
4703
|
+
} catch (error) {
|
|
4704
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
4705
|
+
process.exit(1);
|
|
4706
|
+
}
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4710
|
+
//#endregion
|
|
4711
|
+
//#region src/cli/commands/status.ts
|
|
4712
|
+
/**
|
|
4713
|
+
* Status command - Display form fill status with per-role breakdown.
|
|
4714
|
+
*
|
|
4715
|
+
* Provides:
|
|
4716
|
+
* - Overall progress counts
|
|
4717
|
+
* - Per-role fill statistics
|
|
4718
|
+
* - Run mode (explicit or inferred)
|
|
4719
|
+
* - Suggested next command
|
|
4720
|
+
*/
|
|
4721
|
+
/**
|
|
4722
|
+
* Compute field statistics from a list of fields.
|
|
4723
|
+
*/
|
|
4724
|
+
function computeFieldStats(form, fields) {
|
|
4725
|
+
let answered = 0;
|
|
4726
|
+
let skipped = 0;
|
|
4727
|
+
let aborted = 0;
|
|
4728
|
+
let unanswered = 0;
|
|
4729
|
+
for (const field of fields) switch (form.responsesByFieldId[field.id]?.state ?? "unanswered") {
|
|
4730
|
+
case "answered":
|
|
4731
|
+
answered++;
|
|
4732
|
+
break;
|
|
4733
|
+
case "skipped":
|
|
4734
|
+
skipped++;
|
|
4735
|
+
break;
|
|
4736
|
+
case "aborted":
|
|
4737
|
+
aborted++;
|
|
4738
|
+
break;
|
|
4739
|
+
case "unanswered":
|
|
4740
|
+
default:
|
|
4741
|
+
unanswered++;
|
|
4742
|
+
break;
|
|
4743
|
+
}
|
|
4744
|
+
return {
|
|
4745
|
+
total: fields.length,
|
|
4746
|
+
answered,
|
|
4747
|
+
skipped,
|
|
4748
|
+
aborted,
|
|
4749
|
+
unanswered
|
|
4750
|
+
};
|
|
4751
|
+
}
|
|
4752
|
+
/**
|
|
4753
|
+
* Compute statistics grouped by role.
|
|
4754
|
+
*/
|
|
4755
|
+
function computeStatsByRole(form) {
|
|
4756
|
+
const allFields = getAllFields(form);
|
|
4757
|
+
const roles = getFieldRoles(form);
|
|
4758
|
+
const result = {};
|
|
4759
|
+
for (const role of roles) result[role] = computeFieldStats(form, allFields.filter((f) => f.role === role));
|
|
4760
|
+
return result;
|
|
4761
|
+
}
|
|
4762
|
+
/**
|
|
4763
|
+
* Format percentage with one decimal place.
|
|
4764
|
+
*/
|
|
4765
|
+
function formatPercent(numerator, denominator) {
|
|
4766
|
+
if (denominator === 0) return "0%";
|
|
4767
|
+
return `${Math.round(numerator / denominator * 100)}%`;
|
|
4768
|
+
}
|
|
4769
|
+
/**
|
|
4770
|
+
* Format status report for console output.
|
|
4771
|
+
*/
|
|
4772
|
+
function formatConsoleReport$1(report, useColors) {
|
|
4773
|
+
const lines = [];
|
|
4774
|
+
const bold = useColors ? pc.bold : (s) => s;
|
|
4775
|
+
const dim = useColors ? pc.dim : (s) => s;
|
|
4776
|
+
const cyan = useColors ? pc.cyan : (s) => s;
|
|
4777
|
+
const green = useColors ? pc.green : (s) => s;
|
|
4778
|
+
const yellow = useColors ? pc.yellow : (s) => s;
|
|
4779
|
+
const red = useColors ? pc.red : (s) => s;
|
|
4780
|
+
lines.push(bold(cyan(`Form Status: ${basename(report.path)}`)));
|
|
4781
|
+
lines.push("");
|
|
4782
|
+
const overall = report.overall;
|
|
4783
|
+
const overallPercent = formatPercent(overall.answered, overall.total);
|
|
4784
|
+
lines.push(`${bold("Overall:")} ${overall.answered}/${overall.total} fields filled (${overallPercent})`);
|
|
4785
|
+
lines.push(` ${green("✓")} Complete: ${overall.answered}`);
|
|
4786
|
+
lines.push(` ${dim("○")} Empty: ${overall.unanswered}`);
|
|
4787
|
+
if (overall.skipped > 0) lines.push(` ${yellow("⊘")} Skipped: ${overall.skipped}`);
|
|
4788
|
+
if (overall.aborted > 0) lines.push(` ${red("✗")} Aborted: ${overall.aborted}`);
|
|
4789
|
+
lines.push("");
|
|
4790
|
+
lines.push(bold("By Role:"));
|
|
4791
|
+
const roles = Object.keys(report.byRole).sort((a, b) => {
|
|
4792
|
+
if (a === USER_ROLE) return -1;
|
|
4793
|
+
if (b === USER_ROLE) return 1;
|
|
4794
|
+
if (a === AGENT_ROLE) return -1;
|
|
4795
|
+
if (b === AGENT_ROLE) return 1;
|
|
4796
|
+
return a.localeCompare(b);
|
|
4797
|
+
});
|
|
4798
|
+
for (const role of roles) {
|
|
4799
|
+
const stats = report.byRole[role];
|
|
4800
|
+
if (!stats) continue;
|
|
4801
|
+
const percent = formatPercent(stats.answered, stats.total);
|
|
4802
|
+
const needsAttention = role === USER_ROLE && stats.unanswered > 0 ? yellow(" ← needs attention") : "";
|
|
4803
|
+
lines.push(` ${role}: ${stats.answered}/${stats.total} filled (${percent})${needsAttention}`);
|
|
4804
|
+
}
|
|
4805
|
+
lines.push("");
|
|
4806
|
+
if (report.runMode) {
|
|
4807
|
+
const source = report.runModeSource === "explicit" ? "explicit" : "inferred";
|
|
4808
|
+
lines.push(`${bold("Run Mode:")} ${report.runMode} (${source})`);
|
|
4809
|
+
} else lines.push(`${bold("Run Mode:")} ${dim("unknown")}`);
|
|
4810
|
+
if (report.suggestedCommand) lines.push(`${bold("Suggested:")} ${cyan(report.suggestedCommand)}`);
|
|
4811
|
+
return lines.join("\n");
|
|
4812
|
+
}
|
|
4813
|
+
/**
|
|
4814
|
+
* Generate a suggested command based on status.
|
|
4815
|
+
*/
|
|
4816
|
+
function getSuggestedCommand(report) {
|
|
4817
|
+
const { overall, byRole, runMode, path } = report;
|
|
4818
|
+
const filename = basename(path);
|
|
4819
|
+
if (overall.total > 0 && overall.answered === overall.total) return null;
|
|
4820
|
+
const userStats = byRole[USER_ROLE];
|
|
4821
|
+
if (userStats && userStats.unanswered > 0) return `markform fill ${filename} --interactive`;
|
|
4822
|
+
if (runMode === "research") return `markform research ${filename}`;
|
|
4823
|
+
return `markform run ${filename}`;
|
|
4824
|
+
}
|
|
4825
|
+
/**
|
|
4826
|
+
* Register the status command.
|
|
4827
|
+
*/
|
|
4828
|
+
function registerStatusCommand(program) {
|
|
4829
|
+
program.command("status <file>").description("Display form fill status with per-role breakdown").action(async (file, _options, cmd) => {
|
|
4830
|
+
const ctx = getCommandContext(cmd);
|
|
4831
|
+
try {
|
|
4832
|
+
logVerbose(ctx, `Reading file: ${file}`);
|
|
4833
|
+
const content = await readFile$1(file);
|
|
4834
|
+
logVerbose(ctx, "Parsing form...");
|
|
4835
|
+
const form = parseForm(content);
|
|
4836
|
+
logVerbose(ctx, "Computing status...");
|
|
4837
|
+
const overall = computeFieldStats(form, getAllFields(form));
|
|
4838
|
+
const byRole = computeStatsByRole(form);
|
|
4839
|
+
const runModeResult = determineRunMode(form);
|
|
4840
|
+
let runMode = null;
|
|
4841
|
+
let runModeSource = "unknown";
|
|
4842
|
+
if (runModeResult.success) {
|
|
4843
|
+
runMode = runModeResult.runMode;
|
|
4844
|
+
runModeSource = runModeResult.source;
|
|
4845
|
+
}
|
|
4846
|
+
const report = {
|
|
4847
|
+
path: file,
|
|
4848
|
+
runMode,
|
|
4849
|
+
runModeSource,
|
|
4850
|
+
overall,
|
|
4851
|
+
byRole,
|
|
4852
|
+
suggestedCommand: null
|
|
4853
|
+
};
|
|
4854
|
+
report.suggestedCommand = getSuggestedCommand(report);
|
|
4855
|
+
const output = formatOutput(ctx, report, (data, useColors) => formatConsoleReport$1(data, useColors));
|
|
4856
|
+
console.log(output);
|
|
4857
|
+
} catch (error) {
|
|
4858
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
4859
|
+
process.exit(1);
|
|
4860
|
+
}
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4120
4864
|
//#endregion
|
|
4121
4865
|
//#region src/cli/commands/validate.ts
|
|
4122
4866
|
/**
|
|
@@ -4251,12 +4995,13 @@ function withColoredHelp(cmd) {
|
|
|
4251
4995
|
*/
|
|
4252
4996
|
function createProgram() {
|
|
4253
4997
|
const program = withColoredHelp(new Command());
|
|
4254
|
-
program.name("markform").description("Agent-friendly, human-readable, editable forms").version(
|
|
4998
|
+
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)");
|
|
4255
4999
|
registerReadmeCommand(program);
|
|
4256
5000
|
registerDocsCommand(program);
|
|
4257
5001
|
registerSpecCommand(program);
|
|
4258
5002
|
registerApisCommand(program);
|
|
4259
5003
|
registerApplyCommand(program);
|
|
5004
|
+
registerBrowseCommand(program);
|
|
4260
5005
|
registerDumpCommand(program);
|
|
4261
5006
|
registerExamplesCommand(program);
|
|
4262
5007
|
registerExportCommand(program);
|
|
@@ -4266,7 +5011,10 @@ function createProgram() {
|
|
|
4266
5011
|
registerRenderCommand(program);
|
|
4267
5012
|
registerReportCommand(program);
|
|
4268
5013
|
registerResearchCommand(program);
|
|
5014
|
+
registerRunCommand(program);
|
|
5015
|
+
registerSchemaCommand(program);
|
|
4269
5016
|
registerServeCommand(program);
|
|
5017
|
+
registerStatusCommand(program);
|
|
4270
5018
|
registerValidateCommand(program);
|
|
4271
5019
|
return program;
|
|
4272
5020
|
}
|